mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
379c2aaa3c | ||
|
|
40b471f1e3 | ||
|
|
cfbdf22121 | ||
|
|
d8c264b501 | ||
|
|
c3e782ef73 | ||
|
|
2a3e0d669f | ||
|
|
9b52e83fd7 | ||
|
|
8886e063e6 | ||
|
|
6642176de5 | ||
|
|
3de745bd33 | ||
|
|
9aeeb37ccf | ||
|
|
5ae18032fc | ||
|
|
224d71753b | ||
|
|
315c0f5dc6 | ||
|
|
57df5f61ba | ||
|
|
ac8dc84f3c | ||
|
|
69fba2ebbe | ||
|
|
405c7f0d20 | ||
|
|
e6e8bb0e95 | ||
|
|
db74129eb0 | ||
|
|
8f02541657 | ||
|
|
1244ec1332 | ||
|
|
6ebdee6710 | ||
|
|
1031569f47 | ||
|
|
769ecbfe98 | ||
|
|
5ef12391e1 | ||
|
|
99429904d4 | ||
|
|
496d024431 | ||
|
|
5b6c52437e | ||
|
|
ba783b4eea | ||
|
|
81af549ad7 | ||
|
|
3cd795527c | ||
|
|
3f9b44ecc8 | ||
|
|
48e649a95e | ||
|
|
455dbf2b5f | ||
|
|
4d64ac48bc | ||
|
|
c30d2d3473 | ||
|
|
d8d3ac85b6 | ||
|
|
d6ae581652 | ||
|
|
5c8d371552 | ||
|
|
be869f9160 | ||
|
|
9d07e67cf8 | ||
|
|
6c225605c4 | ||
|
|
b7e00f18a4 | ||
|
|
eb8b63fc62 | ||
|
|
6bbf9b94c4 | ||
|
|
f6e7f63a63 | ||
|
|
ad79e57028 | ||
|
|
6fee8382ce | ||
|
|
91144e88ad | ||
|
|
1a72da81b5 | ||
|
|
dae03f9a11 | ||
|
|
abd81155f9 | ||
|
|
75eaa56c15 | ||
|
|
b07352ea6a | ||
|
|
d53d68b7cf | ||
|
|
e7cf071a1c | ||
|
|
45a093fb42 | ||
|
|
7cdaa6044b | ||
|
|
5b8e6fbc38 | ||
|
|
8dec9eaa10 | ||
|
|
5d21beed9a | ||
|
|
86d52a0f7a | ||
|
|
29db102f59 | ||
|
|
134505f716 | ||
|
|
04b6a99a30 | ||
|
|
fae7703b4f | ||
|
|
6b458c897d | ||
|
|
ad3f26be2d | ||
|
|
979fd652cf | ||
|
|
27cfbe5562 | ||
|
|
f490404b7f | ||
|
|
a888e0fa22 | ||
|
|
77e233600a | ||
|
|
f2ee3c984f | ||
|
|
19de386bcc | ||
|
|
5d23a874b1 | ||
|
|
b616188cbf | ||
|
|
b304fd4a34 | ||
|
|
6d47525e4e | ||
|
|
5673831619 | ||
|
|
6b85990fec | ||
|
|
80cd3476bf | ||
|
|
023be3c189 | ||
|
|
5754e8f046 | ||
|
|
7a8ba29186 | ||
|
|
a2a3aba1d2 | ||
|
|
2fc9ca8516 | ||
|
|
19ed82af07 | ||
|
|
88803f16e8 | ||
|
|
77b485d619 | ||
|
|
4ba0a1b57f | ||
|
|
2a48e1d186 | ||
|
|
aa33bb5ec3 | ||
|
|
aa9a7d41bf | ||
|
|
cf4e8e1d87 | ||
|
|
2dca762e4a | ||
|
|
60ec1f962b | ||
|
|
89a6f8c96f | ||
|
|
1c234ce027 | ||
|
|
07318cc9a4 | ||
|
|
a9eda1d9e7 | ||
|
|
9f8ed74e7b | ||
|
|
1469cc2ade | ||
|
|
05316bb34b | ||
|
|
b8f8a8685d | ||
|
|
2bcd27c1e0 | ||
|
|
2beef9be39 | ||
|
|
1b52298c6c | ||
|
|
0b19f11ad0 | ||
|
|
7276e300eb | ||
|
|
66ea77237f | ||
|
|
0309a58643 | ||
|
|
9d3548c27c | ||
|
|
39777ad173 | ||
|
|
98fd1eca69 | ||
|
|
ddd3962078 | ||
|
|
f76b6ef9ab | ||
|
|
1c2fe8201f | ||
|
|
8b7cdac910 | ||
|
|
70bdcbfd8a | ||
|
|
d9ecd9869a | ||
|
|
e07b0e9256 | ||
|
|
cbefa2567a | ||
|
|
bdb3851bb2 | ||
|
|
dbf306379f | ||
|
|
7d2294580b | ||
|
|
9e2879c0ba | ||
|
|
8d454bf7f1 | ||
|
|
b2fb4a261e | ||
|
|
8cdd184a90 | ||
|
|
f19a6da5b8 | ||
|
|
6194989018 | ||
|
|
525f48c472 | ||
|
|
8eb1029393 | ||
|
|
1516fc1902 | ||
|
|
a4f9b5e925 | ||
|
|
3b9e83d4a5 | ||
|
|
3af42082b1 | ||
|
|
f6eabb4912 | ||
|
|
f93982f55e | ||
|
|
82190d531a | ||
|
|
0c20ed9975 | ||
|
|
6b2aa95cc6 | ||
|
|
0b5b897c11 | ||
|
|
730ba57073 | ||
|
|
e60caab180 | ||
|
|
07d6417446 | ||
|
|
e974ce096a | ||
|
|
1b7498820f | ||
|
|
090eeb9b11 | ||
|
|
d8dce93ff4 | ||
|
|
bdf5899dda | ||
|
|
5ca2266f53 | ||
|
|
98af799cb4 | ||
|
|
0caffd8b70 | ||
|
|
be44f523e0 | ||
|
|
17864965d9 | ||
|
|
d41c0ccaea | ||
|
|
d48b9c4df9 | ||
|
|
e7cdd47d8d | ||
|
|
9e0c0fe960 | ||
|
|
10bb2c6a36 | ||
|
|
fff16f3884 | ||
|
|
e60cbb42e3 | ||
|
|
626852401a | ||
|
|
224745bd0a | ||
|
|
04f8c7fad0 | ||
|
|
f6a4b73459 | ||
|
|
9623bc4d58 | ||
|
|
ba2dda5beb | ||
|
|
f3a87777ee | ||
|
|
46f7b74c8c | ||
|
|
86f271a78f | ||
|
|
73e789489b | ||
|
|
504f9342aa | ||
|
|
dbb20c72fe | ||
|
|
f60b9ef7c8 | ||
|
|
e479b6e91d | ||
|
|
5d95e9adc4 | ||
|
|
2abae6ee41 | ||
|
|
6e589a293d | ||
|
|
1c4515731a | ||
|
|
62c319b575 | ||
|
|
5cc250495f | ||
|
|
a18dd29262 | ||
|
|
98d9b54bcd | ||
|
|
5bb0a210b7 | ||
|
|
5b63bd5f7f | ||
|
|
43e8bfd257 | ||
|
|
87369eaf0f | ||
|
|
2c3b364069 | ||
|
|
1416266bcb | ||
|
|
a94677aed0 | ||
|
|
4bf8522985 | ||
|
|
f651769110 | ||
|
|
7be5937fc3 | ||
|
|
9f5acf14db | ||
|
|
b75db874c6 | ||
|
|
afe05454d9 | ||
|
|
b1ed77c26c | ||
|
|
d80162bea2 | ||
|
|
3e80afe713 | ||
|
|
0ad75095a7 | ||
|
|
72e1469968 | ||
|
|
275513bf2c | ||
|
|
a1a9a36f54 | ||
|
|
e84e74ca3a | ||
|
|
4116082cb9 | ||
|
|
7103c29194 | ||
|
|
82c722e6af | ||
|
|
eaf14e2937 | ||
|
|
a20673e78d | ||
|
|
83471ea6d7 | ||
|
|
f70bc0cabb | ||
|
|
8ed1bbbd0f | ||
|
|
3f903f0b18 | ||
|
|
4deb32a9a3 | ||
|
|
49735d9307 | ||
|
|
b6623f65cd | ||
|
|
80292fab72 | ||
|
|
f3bb697ed4 | ||
|
|
6d3e7c6c67 | ||
|
|
e925f75a4a | ||
|
|
b493418ed1 | ||
|
|
a2968af45e | ||
|
|
635fb55514 | ||
|
|
c192c3b8b5 | ||
|
|
3d75e0a731 | ||
|
|
2a0f78d169 | ||
|
|
6477f9c910 | ||
|
|
a451c1ba6f | ||
|
|
76ee07ee6e | ||
|
|
f07a52cb00 | ||
|
|
023b6a1991 | ||
|
|
2cf36534d9 | ||
|
|
54e7dc7c30 | ||
|
|
ee1e91781a | ||
|
|
2da8ced642 | ||
|
|
a397c94892 | ||
|
|
7932259dc4 | ||
|
|
5a11bd9515 | ||
|
|
538f2f1bc0 | ||
|
|
5a1598fd9e | ||
|
|
bf0e1d7c79 | ||
|
|
415edbcfdf | ||
|
|
4bfee31fdb | ||
|
|
7df8b369e4 | ||
|
|
40394ff76e | ||
|
|
27d4c0ba9c | ||
|
|
fd78bc3884 | ||
|
|
d6c4d58a2d | ||
|
|
6a40b30d12 | ||
|
|
da1dd928ef | ||
|
|
f3fa1fa022 | ||
|
|
c75007bb13 | ||
|
|
c4cc499d58 | ||
|
|
db16d8ed4c | ||
|
|
3eb585fe49 | ||
|
|
fb88dd6e1b | ||
|
|
713285b554 | ||
|
|
13b474d302 | ||
|
|
23feb16b0f | ||
|
|
3d75b9e7ee | ||
|
|
da9beb9324 | ||
|
|
803cddd5cb | ||
|
|
5f99cd25ec | ||
|
|
56be69d065 | ||
|
|
168d491038 | ||
|
|
064d51f7be | ||
|
|
116f28a525 | ||
|
|
1ec21c59ce | ||
|
|
77ed0a7c31 | ||
|
|
ce5f69b586 | ||
|
|
3f1a0c0603 | ||
|
|
72f1625257 | ||
|
|
0868bb97f6 | ||
|
|
d5545243c5 | ||
|
|
22c22d1876 | ||
|
|
5b9c726f33 | ||
|
|
eb2ac9197d | ||
|
|
f882626612 | ||
|
|
72983215fc | ||
|
|
2c8b3593d1 | ||
|
|
f16fda2350 | ||
|
|
e4955f351f | ||
|
|
d6f89e3cfd | ||
|
|
9fd434438b | ||
|
|
64fd9ba1c2 | ||
|
|
f1c617ad27 | ||
|
|
f6006358ed | ||
|
|
7164373fb3 | ||
|
|
24ff5c7ea7 | ||
|
|
acc87a9e22 | ||
|
|
7ca3c1e08f | ||
|
|
505d2cb586 | ||
|
|
8c33c6f349 | ||
|
|
b00c7c12c8 | ||
|
|
d94cc35c81 | ||
|
|
86ae5e9cf4 | ||
|
|
140c18c55a | ||
|
|
876cd5e4a5 | ||
|
|
b48fcc42ae | ||
|
|
b920d5f404 | ||
|
|
57983fb4c2 | ||
|
|
b43970eecc | ||
|
|
b0e0945360 | ||
|
|
eabff9005d | ||
|
|
ec0985618c | ||
|
|
34b064fc62 | ||
|
|
122b511d1e | ||
|
|
bc0c4b8489 | ||
|
|
08e90e8182 | ||
|
|
133d82621e | ||
|
|
5980a08533 | ||
|
|
f436d72d1b | ||
|
|
7860cbf59e | ||
|
|
2f23bbe87f | ||
|
|
0cb6857229 | ||
|
|
9541963aa6 | ||
|
|
6552a89ddd | ||
|
|
69a87fe238 | ||
|
|
5d9eabc919 | ||
|
|
5f3d1eaa2f | ||
|
|
53b6eae122 | ||
|
|
d49877ad2e | ||
|
|
8602999056 | ||
|
|
9d3d5164fe | ||
|
|
e20a56bcb0 | ||
|
|
ed2f0685e0 | ||
|
|
34507f606a | ||
|
|
32b0304c2c | ||
|
|
84d96971df | ||
|
|
2f25ac0784 | ||
|
|
f11fd3cb05 | ||
|
|
de2138e84c | ||
|
|
f0e0ea42ce | ||
|
|
89ef90af04 | ||
|
|
5597c81e7c | ||
|
|
37acb4842f | ||
|
|
97630a598a | ||
|
|
3744a97f00 | ||
|
|
c5086f8d6d | ||
|
|
ac9cd4ab84 | ||
|
|
59646e52f9 | ||
|
|
38e037d3a7 | ||
|
|
ca71b27791 | ||
|
|
b7fe29b5cc | ||
|
|
12b8fd0c1d | ||
|
|
474d44c4db | ||
|
|
05f668dfa7 | ||
|
|
93c1f6cd45 | ||
|
|
3e39bb2cf4 | ||
|
|
194a547955 | ||
|
|
e8fd1f2d01 | ||
|
|
8deb47560e | ||
|
|
65b98b4971 | ||
|
|
e652e50d5c | ||
|
|
4e3c1628b2 | ||
|
|
6bb2856403 | ||
|
|
657fd589d7 | ||
|
|
adeee12209 | ||
|
|
7d4da26905 | ||
|
|
c808bfe609 | ||
|
|
b66d02df7c | ||
|
|
0561277dcb | ||
|
|
e9a234094b | ||
|
|
fb81e155b7 | ||
|
|
a6f229d870 | ||
|
|
1eded0468f | ||
|
|
bd087d6096 | ||
|
|
5fd5d688dc | ||
|
|
3b3895f82e | ||
|
|
d09d1ddad1 | ||
|
|
4999f6dc60 | ||
|
|
cd7e29c635 | ||
|
|
5b12f51e98 | ||
|
|
e324e2231c | ||
|
|
73336b297e | ||
|
|
6549344946 | ||
|
|
1763143337 | ||
|
|
a331616a9d | ||
|
|
ca9dbea060 | ||
|
|
9b400622af | ||
|
|
3792d7377e | ||
|
|
1b66d284c4 | ||
|
|
f26586bb3e | ||
|
|
6b3f7a3ec1 | ||
|
|
83b9940733 | ||
|
|
91461381ee | ||
|
|
cd0956eef7 | ||
|
|
fa085cbd64 | ||
|
|
48c8a64c97 | ||
|
|
59db4ac227 | ||
|
|
15208b5c40 | ||
|
|
80a0954030 | ||
|
|
8d3c929740 | ||
|
|
7688e53424 | ||
|
|
843c8b1a6b | ||
|
|
9011918731 | ||
|
|
e20e858ba8 | ||
|
|
9b1967a59e | ||
|
|
cec5d99e13 | ||
|
|
681ec84748 | ||
|
|
2bec3d521b | ||
|
|
e2dacd92ea | ||
|
|
2aa0ae851f | ||
|
|
6f054408da | ||
|
|
a15747ca61 | ||
|
|
939264d30e | ||
|
|
1e1ddfca0f | ||
|
|
34d53edc91 | ||
|
|
c43f58dc3f |
No files matched your search
+13
-2
@@ -115,7 +115,8 @@ GEMINI_LIVE_MODEL=gemini-3.8-live
|
|||||||
# Client Token). Client-exposed by design, like GOOGLE_MAPS_API_KEY.
|
# Client Token). Client-exposed by design, like GOOGLE_MAPS_API_KEY.
|
||||||
MAPILLARY_CLIENT_TOKEN=
|
MAPILLARY_CLIENT_TOKEN=
|
||||||
|
|
||||||
# Optional: OpenSky auth mode (oauth | basic | auto | anon)
|
# Optional: OpenSky auth mode (oauth | auto | anon). OpenSky retired
|
||||||
|
# username/password (basic) sign-in on 2026-03-18; "basic" now loads anonymously.
|
||||||
# Default: oauth — requires client credentials below
|
# Default: oauth — requires client credentials below
|
||||||
# Set to "anon" to skip aircraft auth (rate-limited but works)
|
# Set to "anon" to skip aircraft auth (rate-limited but works)
|
||||||
OPENSKY_AUTH_MODE=oauth
|
OPENSKY_AUTH_MODE=oauth
|
||||||
@@ -128,7 +129,9 @@ OPENSKY_CLIENT_SECRET=
|
|||||||
# Optional paid Patreon tokens raise request limits; there is no free API key.
|
# Optional paid Patreon tokens raise request limits; there is no free API key.
|
||||||
LL2_API_TOKEN=
|
LL2_API_TOKEN=
|
||||||
|
|
||||||
# Optional: OpenSky credentials JSON file path (alternative to above)
|
# Optional: OpenSky credentials JSON file path, used when the two values above
|
||||||
|
# are empty: the credentials.json your OpenSky account page downloads when you
|
||||||
|
# create an API client. Use an absolute path for compatibility with launchers.
|
||||||
# OPENSKY_CREDENTIALS_FILE=/path/to/credentials.json
|
# OPENSKY_CREDENTIALS_FILE=/path/to/credentials.json
|
||||||
|
|
||||||
# Optional: dev server config
|
# Optional: dev server config
|
||||||
@@ -207,6 +210,7 @@ AISSTREAM_API_KEY=
|
|||||||
# CCTV_TFL_ENABLED=1 # set 0 to disable Transport for London JamCams
|
# CCTV_TFL_ENABLED=1 # set 0 to disable Transport for London JamCams
|
||||||
# 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
|
||||||
# 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
|
||||||
@@ -227,6 +231,9 @@ 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_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_CAMERAS_URL= # override the DATA.GOV.HK camera locations XML URL
|
||||||
# CCTV_KINGCOUNTY_ENABLED=1 # set 0 to disable the King County (Washington) road cameras
|
# CCTV_KINGCOUNTY_ENABLED=1 # set 0 to disable the King County (Washington) road cameras
|
||||||
# CCTV_KINGCOUNTY_MAX_SOURCES=125 # cap cameras loaded from the King County catalog
|
# CCTV_KINGCOUNTY_MAX_SOURCES=125 # cap cameras loaded from the King County catalog
|
||||||
# CCTV_WSDOT_ENABLED=1 # set 0 to disable the WSDOT (Washington State) highway cameras
|
# CCTV_WSDOT_ENABLED=1 # set 0 to disable the WSDOT (Washington State) highway cameras
|
||||||
@@ -234,6 +241,7 @@ AISSTREAM_API_KEY=
|
|||||||
# CCTV_VEGVESEN_ENABLED=1 # set 0 to disable the Statens vegvesen (Norway) road cameras
|
# CCTV_VEGVESEN_ENABLED=1 # set 0 to disable the Statens vegvesen (Norway) road cameras
|
||||||
# CCTV_VEGVESEN_MAX_SOURCES=900 # cap cameras loaded from Statens vegvesen (default loads all ~850 live; max 1000)
|
# CCTV_VEGVESEN_MAX_SOURCES=900 # cap cameras loaded from Statens vegvesen (default loads all ~850 live; max 1000)
|
||||||
# CCTV_VEGVESEN_VIDEO=1 # set 0 to show stills instead of live HLS for Statens vegvesen video cameras
|
# CCTV_VEGVESEN_VIDEO=1 # set 0 to show stills instead of live HLS for Statens vegvesen video cameras
|
||||||
|
# CCTV_VEGVESEN_HEIGHTS=1 # set 0 to skip Kartverket ground heights for Statens vegvesen cameras (flat prior instead)
|
||||||
# CCTV_VEGVESEN_URL= # override the Statens vegvesen catalog URL
|
# CCTV_VEGVESEN_URL= # override the Statens vegvesen catalog URL
|
||||||
# CCTV_NYC_ENABLED= # off by default; set 1 only if you hold a NYC DOT data-sharing agreement ([email protected]) for the camera feed
|
# CCTV_NYC_ENABLED= # off by default; set 1 only if you hold a NYC DOT data-sharing agreement ([email protected]) for the camera feed
|
||||||
# CCTV_NYC_MAX_SOURCES=250 # cap cameras loaded from the NYC TMC catalog (nearest borough cores first)
|
# CCTV_NYC_MAX_SOURCES=250 # cap cameras loaded from the NYC TMC catalog (nearest borough cores first)
|
||||||
@@ -245,6 +253,9 @@ AISSTREAM_API_KEY=
|
|||||||
# CCTV_LITHUANIA_MAX_SOURCES=400 # cap Lithuanian cameras; no API key required
|
# CCTV_LITHUANIA_MAX_SOURCES=400 # cap Lithuanian cameras; no API key required
|
||||||
# CCTV_FLORIDA_ENABLED= # off by default; set 1 to load FL511 (Florida) cameras (individual, personal use only; any other use, including a public deployment, needs FDOT's express written consent; see DATA_SOURCES.md)
|
# CCTV_FLORIDA_ENABLED= # off by default; set 1 to load FL511 (Florida) cameras (individual, personal use only; any other use, including a public deployment, needs FDOT's express written consent; see DATA_SOURCES.md)
|
||||||
# CCTV_FLORIDA_MAX_SOURCES=500 # cap cameras loaded from the FL511 catalog (nearest regional anchors first)
|
# CCTV_FLORIDA_MAX_SOURCES=500 # cap cameras loaded from the FL511 catalog (nearest regional anchors first)
|
||||||
|
# CCTV_TAIWAN_ENABLED=1 # set 0 to skip Taiwan Freeway Bureau (MOTC) road cameras
|
||||||
|
# CCTV_TAIWAN_MAX_SOURCES=600 # cap Taiwan freeway cameras (1-2000); no API key required
|
||||||
|
# CCTV_TAIWAN_URL= # override the Freeway Bureau MOTC 2.0 CCTV.xml URL
|
||||||
# CCTV_MAX_SOURCES=4000 # catalog-wide ceiling, shared round-robin across packs
|
# CCTV_MAX_SOURCES=4000 # catalog-wide ceiling, shared round-robin across packs
|
||||||
# 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
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
## What this changes
|
||||||
|
|
||||||
|
<!-- One or two sentences: what changed and why. Link the issue if there is one. -->
|
||||||
|
|
||||||
|
## How I verified it
|
||||||
|
|
||||||
|
- [ ] `npm run build`
|
||||||
|
- [ ] `npm test`
|
||||||
|
- [ ] `npm run test:track` (dev server running)
|
||||||
|
- [ ] Feature gate for the area I touched: <!-- e.g. `node scripts/qa-radio.mjs`, or "none applies" -->
|
||||||
|
|
||||||
|
<!-- Screenshots are welcome for anything visual. -->
|
||||||
|
|
||||||
|
## New layer checklist
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Fill this in if the PR adds a new data layer. Delete the whole section if it doesn't.
|
||||||
|
See "Adding a new layer" in CONTRIBUTING.md.
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
|
||||||
|
<!-- At least one screenshot of the layer on the globe, plus its info card or panel if it has one. -->
|
||||||
|
|
||||||
|
**Recording**
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Preferably 15 to 30 seconds: turn the layer on, move around, open a record.
|
||||||
|
Drag an .mp4 or .mov in here; GitHub accepts up to 10 MB on free accounts.
|
||||||
|
If it's bigger, link to it instead.
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Proposed category**
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Which layer-panel group should it sit in? The current groups are in PANEL_GROUPS
|
||||||
|
in src/ui/layerPanel.js: Movement, Cameras, Infrastructure, Events, Weather, Utilities.
|
||||||
|
If none fits, propose a new group and say why.
|
||||||
|
-->
|
||||||
|
|
||||||
|
Category:
|
||||||
|
|
||||||
|
**Data source and licence**
|
||||||
|
|
||||||
|
- Source (provider and endpoint):
|
||||||
|
- Licence or terms (link):
|
||||||
|
- Attribution shown in the app:
|
||||||
|
- Needs a key? (none / optional / required):
|
||||||
|
- Fetched at runtime, or bundled in the repo:
|
||||||
|
|
||||||
|
<!-- Bundled data must be data we have the right to redistribute. -->
|
||||||
|
|
||||||
|
- [ ] Added a row to `DATA_SOURCES.md`
|
||||||
|
- [ ] Credit is registered in `src/data/dataCredits.js`
|
||||||
|
- [ ] Share-link token assigned with `npm run layer-token:next`, if the layer is shareable
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- [ ] `docs/CURRENT-STATE.md` updated and a `changelog.d/<pr-number>-<slug>.md` fragment added, or this PR doesn't change runtime behaviour
|
||||||
@@ -6,6 +6,15 @@
|
|||||||
never derived: an aircraft that has not transmitted a rate reads as `null`
|
never derived: an aircraft that has not transmitted a rate reads as `null`
|
||||||
rather than a level-flight `0`.
|
rather than a level-flight `0`.
|
||||||
|
|
||||||
|
- Bound how often the GBFS proxy will relay. `/api/gbfs` already restricted
|
||||||
|
where it could go — https, an allowlisted host, a station feed path, no
|
||||||
|
redirects, a 12-second deadline and a 5 MB body cap — but nothing restricted
|
||||||
|
how often, so the dev server would make unlimited requests at a third-party
|
||||||
|
bikeshare operator on any caller's behalf. The route now admits 120
|
||||||
|
requests/minute per client address, refusing the rest with 429 and
|
||||||
|
`Retry-After` before any outbound fetch. The bikeshare layer needs about 15 a
|
||||||
|
minute with every catalogued system in range, so ordinary use is unchanged.
|
||||||
|
|
||||||
- Add nine Ontario transit feeds: TTC, MiWay, Hamilton HSR, Durham Region,
|
- Add nine Ontario transit feeds: TTC, MiWay, Hamilton HSR, Durham Region,
|
||||||
YRT/Viva, Guelph, Burlington, Barrie and Kingston, each carrying its own
|
YRT/Viva, Guelph, Burlington, Barrie and Kingston, each carrying its own
|
||||||
licence quote and attribution. A feed can now name its own modes, so Toronto's
|
licence quote and attribution. A feed can now name its own modes, so Toronto's
|
||||||
|
|||||||
+29
-2
@@ -108,7 +108,7 @@ 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. 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 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.
|
- **🛰️ 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.
|
||||||
- **🎙️ 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).
|
||||||
@@ -116,7 +116,7 @@ The highest-leverage places to jump in:
|
|||||||
## Architecture in one minute
|
## Architecture in one minute
|
||||||
|
|
||||||
- **No framework.** Vanilla JS + [CesiumJS](https://cesium.com/platform/cesiumjs/) + [Vite](https://vitejs.dev/).
|
- **No framework.** Vanilla JS + [CesiumJS](https://cesium.com/platform/cesiumjs/) + [Vite](https://vitejs.dev/).
|
||||||
- **Assembly lives in `src/app/`; standalone defaults live in `src/standalone/`.** UI controllers live in `src/ui/`, layer factories in `src/layers/`, portable sources in `src/sources/`, and application operations in `src/services/`. Existing `src/ui.js` and `src/data/<layer>.js` entries retain compatibility; new code belongs with its focused owner.
|
- **Assembly lives in `src/app/`; standalone defaults live in `src/standalone/`.** UI controllers live in `src/ui/`, layer factories in `src/layers/`, portable sources in `src/sources/`, and application operations in `src/services/`. Existing `src/data/<layer>.js` entries retain compatibility; new code belongs with its focused owner.
|
||||||
- Sources acquire records; renderers own Cesium resources. Import `gods-eye-view/layers/<family>/source` when only a source factory is needed. Common voice controls consume the session interface; protocol adapters own connection details.
|
- Sources acquire records; renderers own Cesium resources. Import `gods-eye-view/layers/<family>/source` when only a source factory is needed. Common voice controls consume the session interface; protocol adapters own connection details.
|
||||||
- **Secrets stay server-side.** Anything needing a private key goes through a local proxy under `server/providers/`. The browser only ever sees the Google Maps key (which you restrict) and ephemeral tokens.
|
- **Secrets stay server-side.** Anything needing a private key goes through a local proxy under `server/providers/`. The browser only ever sees the Google Maps key (which you restrict) and ephemeral tokens.
|
||||||
- `docs/CURRENT-STATE.md` is the authoritative runtime reference — read it first.
|
- `docs/CURRENT-STATE.md` is the authoritative runtime reference — read it first.
|
||||||
@@ -153,6 +153,33 @@ ownership and adoption process.
|
|||||||
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. **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).
|
||||||
|
|
||||||
|
## Adding a new layer
|
||||||
|
|
||||||
|
A new layer changes what everyone sees on the globe, so maintainers need to see
|
||||||
|
it working and know where its data comes from before reviewing the code. PRs
|
||||||
|
that add a layer must include all four of these in the description. The PR
|
||||||
|
template has a section for them.
|
||||||
|
|
||||||
|
1. **Screenshots.** At least one of the layer on the globe, plus its info card
|
||||||
|
or panel if it has one.
|
||||||
|
2. **A screen recording**, preferably 15 to 30 seconds: turn the layer on, move
|
||||||
|
around, and open a record. Drag an `.mp4` or `.mov` into the PR description.
|
||||||
|
GitHub accepts videos up to 10 MB on free accounts, so link to a larger file
|
||||||
|
instead of attaching it.
|
||||||
|
3. **The proposed category.** Say which layer-panel group the layer belongs in.
|
||||||
|
The groups are defined in `PANEL_GROUPS` in `src/ui/layerPanel.js` (today:
|
||||||
|
Movement, Cameras, Infrastructure, Events, Weather and Utilities). A layer
|
||||||
|
that isn't listed there falls under "Other layers". If none of the groups
|
||||||
|
fits, propose a new one and explain why.
|
||||||
|
4. **The data source and licence.** Name the provider and endpoint, link its
|
||||||
|
licence or terms, give the attribution shown in the app, and say whether it
|
||||||
|
needs a key and whether the data is fetched at runtime or bundled. Add the
|
||||||
|
same details to [DATA_SOURCES.md](DATA_SOURCES.md) and register the credit
|
||||||
|
in `src/data/dataCredits.js`.
|
||||||
|
|
||||||
|
Maintainers may hold a layer PR until all four are there.
|
||||||
|
|
||||||
## Maintainers
|
## Maintainers
|
||||||
|
|
||||||
|
|||||||
+14
-2
@@ -63,7 +63,9 @@ How to read this:
|
|||||||
| **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 |
|
||||||
| **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" |
|
||||||
| **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) |
|
||||||
| **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) |
|
||||||
| **City of Toronto Open Data** (`opendata.toronto.ca`) | CCTV camera catalog + frames, Toronto city streets | [Open Government Licence – Toronto](https://www.toronto.ca/city-government/data-research-maps/open-data/open-data-licence/) — attribution REQUIRED | "Contains information licensed under the Open Government Licence – Toronto" |
|
| **City of Toronto Open Data** (`opendata.toronto.ca`) | CCTV camera catalog + frames, Toronto city streets | [Open Government Licence – Toronto](https://www.toronto.ca/city-government/data-research-maps/open-data/open-data-licence/) — attribution REQUIRED | "Contains information licensed under the Open Government Licence – Toronto" |
|
||||||
| **GBFS (Lyft / BCycle / Social Bicycles / PBSC)** | Bikeshare availability | Per-feed (attribution-only) | Credit the operator (e.g. Austin BCycle) + its `license_url` |
|
| **GBFS (Lyft / BCycle / Social Bicycles / PBSC)** | Bikeshare availability | Per-feed (attribution-only) | Credit the operator (e.g. Austin BCycle) + its `license_url` |
|
||||||
@@ -111,6 +113,7 @@ 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).
|
||||||
|
|
||||||
- **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)).
|
||||||
- **OpenSky Network.** Its license is **non-commercial**, and operational use of the REST API in a live product can require a prior written agreement with OpenSky — even for non-profit/government use. If you deploy this commercially, contact OpenSky for your own terms. The flights layer is a toggle and runs anonymously by default.
|
- **OpenSky Network.** Its license is **non-commercial**, and operational use of the REST API in a live product can require a prior written agreement with OpenSky — even for non-profit/government use. If you deploy this commercially, contact OpenSky for your own terms. The flights layer is a toggle and runs anonymously by default.
|
||||||
@@ -153,7 +156,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
|
|||||||
- **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, 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.
|
||||||
- **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 keyless `511on.ca/api/v2/get/cameras` endpoint; 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.
|
- **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.
|
||||||
@@ -167,12 +170,14 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei
|
|||||||
- **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`.
|
||||||
- **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.
|
||||||
- **Statens vegvesen road cameras.** Keyless. The camera list is one GeoJSON request to the OGC API Features view of the official DATEX 3.1 CCTV site table (`ogckart-sn1.atlas.vegvesen.no/ogc/features/v1/collections/datex_3_1:CctvSimple/items`), so no DATEX account is needed; frames are stills on `kamera.atlas.vegvesen.no/api/images/<cameraId>`, and only that exact path per camera is registered. Cameras the feed reports as faulty are dropped. About 135 working cameras also publish live HLS (`kamera.vegvesen.no/public/<cameraId>/manifest.m3u8`, MPEG-TS); those play as live video through the bounded in-memory HLS relay, with the still as the snapshot fallback, and only that exact manifest path per camera is registered. `CCTV_VEGVESEN_VIDEO=0` keeps every camera on stills. `orientationDescription` names the place a camera faces, not a bearing, so it goes into the label and headings use the id-hash fallback at low confidence. By default every live camera loads (~850); `CCTV_VEGVESEN_MAX_SOURCES` (8–1000) lowers the cap, keeping the cameras nearest Oslo, Bergen, Stavanger, Trondheim, Kristiansand, Bodø and Tromsø and `CCTV_VEGVESEN_ENABLED=0` turns the pack off. Frames are fetched at request time and never stored or redistributed. The NLOD attribution is registered in the Data attribution popover.
|
- **Statens vegvesen road cameras.** Keyless. The camera list is one GeoJSON request to the OGC API Features view of the official DATEX 3.1 CCTV site table (`ogckart-sn1.atlas.vegvesen.no/ogc/features/v1/collections/datex_3_1:CctvSimple/items`), so no DATEX account is needed; frames are stills on `kamera.atlas.vegvesen.no/api/images/<cameraId>`, and only that exact path per camera is registered. Cameras the feed reports as faulty are dropped. About 135 working cameras also publish live HLS (`kamera.vegvesen.no/public/<cameraId>/manifest.m3u8`, MPEG-TS); those play as live video through the bounded in-memory HLS relay, with the still as the snapshot fallback, and only that exact manifest path per camera is registered. `CCTV_VEGVESEN_VIDEO=0` keeps every camera on stills. `orientationDescription` names the place a camera faces, not a bearing, so it goes into the label and headings use the id-hash fallback at low confidence. By default every live camera loads (~850); `CCTV_VEGVESEN_MAX_SOURCES` (8–1000) lowers the cap, keeping the cameras nearest Oslo, Bergen, Stavanger, Trondheim, Kristiansand, Bodø and Tromsø and `CCTV_VEGVESEN_ENABLED=0` turns the pack off. Frames are fetched at request time and never stored or redistributed. The NLOD attribution is registered in the Data attribution popover.
|
||||||
|
- **Kartverket ground heights for Statens vegvesen cameras.** The camera feed's points are 2D, so the loader asks Kartverket's keyless point-height API (`ws.geonorge.no/hoydedata/v1/punkt`, CC BY 4.0) for the terrain height at each camera. Requests carry 20 points: the API accepts 50, but above ~20 it answers the extra points from contour lines (`hoydekurver`, metres to ~20 m off) or not at all. Eight requests run at a time, about 36 for the whole country: a cold lookup took 6–13 s in testing (single requests 0.6–9.5 s). Answers are matched by the coordinates the API echoes and recorded only for points that request asked about. Over water the API returns the sea-floor depth (`dybdekurver` or `Havflate`), which is replaced by 0 m, the water surface; for the ~14 bridge cameras that is below the deck, but much closer than the sea floor or the flat prior. Heights are cached in memory by position for the life of the server, and points with no height (a few ferry quays and bridges, or a point a 200 answer leaves out) for 6 h, so the 15-minute catalog refresh only asks about new or moved cameras. The first failing request (a 408, 429 or 5xx, a network error, a request over 10 s, or an unreadable or oversized body) stops the lookup, with no retries, and no new lookup starts until the backoff ends (Retry-After when sent, else 30 min, longer than the refresh; bounded to 1 min–1 h). Any other 4xx marks that request's points as having no height and the lookup carries on. The whole lookup has a 30 s deadline; a lookup cut short by it resumes on the next refresh without a backoff. The lane waits at most 12 s, counted from the start of the loader, and only when more than 10% of the cameras have no answer yet; a few new cameras or expired misses are looked up in the background. A slower lookup keeps filling the cache, and the cameras it had not reached keep the flat prior until the next 15-minute refresh (about 190 of 865 on one slow first run). Only coordinates are sent, with the same User-Agent as the other CCTV requests. `CCTV_VEGVESEN_HEIGHTS=0` turns the lookup off. The Kartverket credit, with a note that heights over water are set to 0 m and heights are rounded to 0.1 m, is shown beside the Statens vegvesen entry in the Data attribution popover.
|
||||||
- **NYC DOT Traffic Management Center cameras.** Off by default. NYC DOT's [developer data feeds page](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) asks developers who want the camera feed to contact `[email protected]` and sign a data-sharing agreement. This project does not hold one that covers downstream installations, so the pack loads only when an operator who holds their own agreement sets `CCTV_NYC_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to NYC DOT. The endpoint needs no key. The camera list is the public `webcams.nyctmc.org/api/cameras` catalog (~980 cameras across the five boroughs); frames are stills at `webcams.nyctmc.org/api/cameras/<uuid>/image`, synthesized from a validated UUID rather than taken from the payload, and only that host is registered. Cameras the feed reports as offline are dropped. The catalog publishes no camera facing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. Once enabled, the 250 cameras nearest the borough cores load; `CCTV_NYC_MAX_SOURCES` (8–400) changes the cap. Frames are fetched at request time and never stored or redistributed. NYC DOT asks every application that uses its data to carry its disclaimer; it is registered verbatim in the Data attribution popover.
|
- **NYC DOT Traffic Management Center cameras.** Off by default. NYC DOT's [developer data feeds page](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) asks developers who want the camera feed to contact `[email protected]` and sign a data-sharing agreement. This project does not hold one that covers downstream installations, so the pack loads only when an operator who holds their own agreement sets `CCTV_NYC_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to NYC DOT. The endpoint needs no key. The camera list is the public `webcams.nyctmc.org/api/cameras` catalog (~980 cameras across the five boroughs); frames are stills at `webcams.nyctmc.org/api/cameras/<uuid>/image`, synthesized from a validated UUID rather than taken from the payload, and only that host is registered. Cameras the feed reports as offline are dropped. The catalog publishes no camera facing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. Once enabled, the 250 cameras nearest the borough cores load; `CCTV_NYC_MAX_SOURCES` (8–400) changes the cap. Frames are fetched at request time and never stored or redistributed. NYC DOT asks every application that uses its data to carry its disclaimer; it is registered verbatim in the Data attribution popover.
|
||||||
- **City of Toronto RESCU cameras.** Keyless. The camera list is the City's public `tmcearthcameras.json` on `opendata.toronto.ca` (dataset "Traffic Cameras", Transportation Services), served as JSONP and unwrapped with an anchored pattern. This is the municipal network the Ontario 511 pack does not carry: downtown arterials plus the Gardiner, DVP, Allen and Lake Shore. The list publishes no frame URL, so frame URLs are BUILT from the documented `CameraImages/loc####.jpg` convention and a strictly-digits camera number, pinning the frame proxy to the portal origin by construction; the list fetch refuses redirects and bounds the body it buffers. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing: the `D1`-`D4` fields are the directions of the *comparison* images (static reference stills captured in 2016 under a different path), not a bearing for the live pan-tilt-zoom frame, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. A camera that is temporarily out of service still answers 200 with a byte-identical "Image Unavailable" placeholder rather than an error (about 6% of the network at any moment), so such a camera reads as healthy; the frame it shows is the agency's, not a synthesized one. The licence requires the attribution statement above. Disable with `CCTV_TORONTO_ENABLED=0`.
|
- **City of Toronto RESCU cameras.** Keyless. The camera list is the City's public `tmcearthcameras.json` on `opendata.toronto.ca` (dataset "Traffic Cameras", Transportation Services), served as JSONP and unwrapped with an anchored pattern. This is the municipal network the Ontario 511 pack does not carry: downtown arterials plus the Gardiner, DVP, Allen and Lake Shore. The list publishes no frame URL, so frame URLs are BUILT from the documented `CameraImages/loc####.jpg` convention and a strictly-digits camera number, pinning the frame proxy to the portal origin by construction; the list fetch refuses redirects and bounds the body it buffers. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing: the `D1`-`D4` fields are the directions of the *comparison* images (static reference stills captured in 2016 under a different path), not a bearing for the live pan-tilt-zoom frame, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. A camera that is temporarily out of service still answers 200 with a byte-identical "Image Unavailable" placeholder rather than an error (about 6% of the network at any moment), so such a camera reads as healthy; the frame it shows is the agency's, not a synthesized one. The licence requires the attribution statement above. Disable with `CCTV_TORONTO_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 sixteen registered 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 and YRT 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 sixteen registered 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 and YRT 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 Browser.** `/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; schema-valid responses whose rows all fail the product's health policy do not count as successful queries. A usable partial cold catalog is explicitly `DEGRADED`, and malformed or empty successful payloads are rejected atomically. 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.** `/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; schema-valid responses whose rows all fail the product's health policy do not count as successful queries. A usable partial cold catalog is explicitly `DEGRADED`, and non-array or empty successful 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.
|
||||||
- **TomTom Traffic.** Optional and BYOK: without `TOMTOM_API_KEY` the traffic layer runs its built-in simulation and no TomTom data (or attribution) appears. With a key, flow vector tiles are fetched through the server-side `/api/tomtom` proxy (120 s cache + a daily tile-budget governor — `TOMTOM_DAILY_TILE_BUDGET`, default 6,000 — sized so a 31-day month stays inside TomTom's 200K monthly free allowance; still a configurable application safety ceiling, not a guarantee; TomTom's [current pricing](https://docs.tomtom.com/pricing/) lists 200K free tile requests per month) and the "Traffic flow data © TomTom" credit is registered in the Data attribution popover the moment live mode activates. TomTom data is served live and cached only transiently (≤120 s TTL under `.gev-cache/`, gitignored) — it is not bundled or redistributed. One 23 KB point-in-time tile snapshot is committed as a decode-test fixture (`src/data/fixtures/`, © TomTom, never served to the app).
|
- **TomTom Traffic.** Optional and BYOK: without `TOMTOM_API_KEY` the traffic layer runs its built-in simulation and no TomTom data (or attribution) appears. With a key, flow vector tiles are fetched through the server-side `/api/tomtom` proxy (120 s cache + a daily tile-budget governor — `TOMTOM_DAILY_TILE_BUDGET`, default 6,000 — sized so a 31-day month stays inside TomTom's 200K monthly free allowance; still a configurable application safety ceiling, not a guarantee; TomTom's [current pricing](https://docs.tomtom.com/pricing/) lists 200K free tile requests per month) and the "Traffic flow data © TomTom" credit is registered in the Data attribution popover the moment live mode activates. TomTom data is served live and cached only transiently (≤120 s TTL under `.gev-cache/`, gitignored) — it is not bundled or redistributed. One 23 KB point-in-time tile snapshot is committed as a decode-test fixture (`src/data/fixtures/`, © TomTom, never served to the app).
|
||||||
- **Routing (OSRM / FOSSGIS).** `/api/route` proxies the public OSRM instances FOSSGIS e.V. runs for the OpenStreetMap community (car, bike, foot profiles). They are keyless and intended for fair, interactive use — the proxy rate-limits clients (60/min, 200/min total), caps route span and response size, caches each route for 10 minutes, and identifies itself in the User-Agent. Heavy or automated use must run its own OSRM. Routes are computed from OpenStreetMap data (ODbL); the Directions layer and voice routes never redistribute them. A route the router cannot find is reported as such; the Directions layer draws nothing in that case, and voice routes fall back to a clearly labeled straight line.
|
- **Routing (OSRM / FOSSGIS).** `/api/route` proxies the public OSRM instances FOSSGIS e.V. runs for the OpenStreetMap community (car, bike, foot profiles). They are keyless and intended for fair, interactive use — the proxy rate-limits clients (60/min, 200/min total), caps route span and response size, caches each route for 10 minutes, and identifies itself in the User-Agent. Heavy or automated use must run its own OSRM. Routes are computed from OpenStreetMap data (ODbL); the Directions layer and voice routes never redistribute them. A route the router cannot find is reported as such; the Directions layer draws nothing in that case, and voice routes fall back to a clearly labeled straight line.
|
||||||
- **Re:Earth Terrain.** Keyless (no API key). Used two ways: (1) `src/mapStackController.js` swaps in a `Cesium.CesiumTerrainProvider` pointed at Re:Earth's `cesium-mesh/ellipsoid` quantized-mesh endpoint for globe stacks without a Cesium ion token (e.g. OSM), replacing a flat `EllipsoidTerrainProvider`; falls back to the flat provider if the endpoint can't be reached. (2) The server-side `/api/terrain/heights` proxy (disk-cached, serve-stale) resolves per-point ellipsoidal ground height for entity placement. Both are best-effort with a keyless-safe fallback (bundled EGM96 geoid math) if Re:Earth is unreachable.
|
- **Re:Earth Terrain.** Keyless (no API key). Used two ways: (1) `src/mapStackController.js` swaps in a `Cesium.CesiumTerrainProvider` pointed at Re:Earth's `cesium-mesh/ellipsoid` quantized-mesh endpoint for globe stacks without a Cesium ion token (e.g. OSM), replacing a flat `EllipsoidTerrainProvider`; falls back to the flat provider if the endpoint can't be reached. (2) The server-side `/api/terrain/heights` proxy (disk-cached, serve-stale) resolves per-point ellipsoidal ground height for entity placement. Both are best-effort with a keyless-safe fallback (bundled EGM96 geoid math) if Re:Earth is unreachable.
|
||||||
@@ -200,6 +205,8 @@ The [Bhote Koshi event pack](public/events/bhote-koshi-2026/README.md), under `p
|
|||||||
| **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) |
|
||||||
| **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) |
|
||||||
|
| **Yale Bright Star Catalogue** (9,096 stars, sky background) | `sky/` | **Public domain** | ✅ (no restrictions) | "Yale Bright Star Catalogue (Hoffleit & Warren, via CDS)" (courtesy credit) |
|
||||||
|
| **ESO Milky Way panorama** (eso0932a, blurred cube map, sky background) | `sky/` | **CC BY 4.0** | ✅ (attribution; changes stated) | "ESO/S. Brunier" |
|
||||||
| **CCTV ground heights** (3,445 cameras) | `cctv_ground_heights/` | Precomputed camera placement heights, aligned to work with Google Photorealistic 3D Tiles (folder README) | — | — |
|
| **CCTV ground heights** (3,445 cameras) | `cctv_ground_heights/` | Precomputed camera placement heights, aligned to work with Google Photorealistic 3D Tiles (folder README) | — | — |
|
||||||
| **CCTV Camera Database** model-enrichment extract v2.26.0 (17 camera-model records) | `src/layers/cctv/modelSpecs.js` | **CC0-1.0** (public domain dedication) | ✅ (no restrictions) | "Specs: cctv-database.com" (courtesy — not legally required) |
|
| **CCTV Camera Database** model-enrichment extract v2.26.0 (17 camera-model records) | `src/layers/cctv/modelSpecs.js` | **CC0-1.0** (public domain dedication) | ✅ (no restrictions) | "Specs: cctv-database.com" (courtesy — not legally required) |
|
||||||
|
|
||||||
@@ -272,6 +279,11 @@ 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).
|
||||||
|
|
||||||
|
### Sky background (`sky/`)
|
||||||
|
|
||||||
|
Sources, licences, the changes CC BY asks to state, hashes and build commands
|
||||||
|
are in [`sky/README.md`](src/data/local_data/sky/README.md).
|
||||||
|
|
||||||
### DataSF Analysis Neighborhoods (`neighborhoods/`)
|
### DataSF Analysis Neighborhoods (`neighborhoods/`)
|
||||||
|
|
||||||
`neighborhoods/san-francisco.json` bundles the City & County of San Francisco's official
|
`neighborhoods/san-francisco.json` bundles the City & County of San Francisco's official
|
||||||
|
|||||||
@@ -177,6 +177,16 @@ Publishing it wider is the [Sharing an instance](#-sharing-an-instance) opt-in,
|
|||||||
|
|
||||||
When you widen the publish, also set `GEV_KEY_SETUP_TRUSTED_PEERS=none` so the launcher trusts no gateway address.
|
When you widen the publish, also set `GEV_KEY_SETUP_TRUSTED_PEERS=none` so the launcher trusts no gateway address.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GEV_KEY_SETUP_TRUSTED_PEERS=none docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
Compose forwards `GEV_KEY_SETUP_TRUSTED_PEERS` and `GEV_ALLOWED_HOSTS` from your
|
||||||
|
shell or the project's `.env`; an exported shell value takes precedence. You can
|
||||||
|
also put `GEV_KEY_SETUP_TRUSTED_PEERS=none` in `.env` for a persistent setting.
|
||||||
|
Unset or blank trusted peers retain automatic gateway discovery; `none` explicitly
|
||||||
|
disables it. The startup log confirms that no additional peers are trusted.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Running on Linux, a NAS, or behind a reverse proxy</summary>
|
<summary>Running on Linux, a NAS, or behind a reverse proxy</summary>
|
||||||
|
|
||||||
@@ -333,6 +343,10 @@ Thirty tools, four jobs — the commands below come straight from the product's
|
|||||||
|
|
||||||
**🎥 Direct it** — drone-operator camera verbs:
|
**🎥 Direct it** — drone-operator camera verbs:
|
||||||
|
|
||||||
|
“Rotate the globe” spins Earth to the right continuously; ask for left, a speed,
|
||||||
|
or stop. If it refuses, follow its reset-and-rotate or further-zoom guidance;
|
||||||
|
camera input stops rotation.
|
||||||
|
|
||||||
> 🗣️ _"Take me to Tokyo."_ · _"Orbit around this area slowly."_ · _"Draw the walking route from the Capitol to Zilker Park."_ → _"Fly the route we just drew."_ · _"Zoom out to a globe view."_
|
> 🗣️ _"Take me to Tokyo."_ · _"Orbit around this area slowly."_ · _"Draw the walking route from the Capitol to Zilker Park."_ → _"Fly the route we just drew."_ · _"Zoom out to a globe view."_
|
||||||
|
|
||||||
**🖊️ Annotate it** — a whiteboard over the real world:
|
**🖊️ Annotate it** — a whiteboard over the real world:
|
||||||
@@ -381,7 +395,7 @@ Twenty layers and map sources. **Eighteen have a keyless path.** Some offer addi
|
|||||||
| 🌍 **Earthquakes** | Global seismic activity, last 24h | USGS | 🟢 |
|
| 🌍 **Earthquakes** | Global seismic activity, last 24h | USGS | 🟢 |
|
||||||
| ◍ **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) · 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), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed) and opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 |
|
| 📹 **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), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed) and opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent). Positions are published; 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 | 🟢 |
|
||||||
@@ -425,7 +439,7 @@ _The Space Missions layer replaying a Falcon 9 ascent — labeled `RECONSTRUCTED
|
|||||||
|
|
||||||
**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 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.
|
||||||
|
|
||||||
**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 under the slider; 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. 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`.
|
||||||
|
|
||||||
@@ -478,7 +492,7 @@ How the globe handles live data:
|
|||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── main.js # Bootstrap: Google 3D tiles, layer registration
|
├── main.js # Bootstrap: Google 3D tiles, layer registration
|
||||||
├── ui.js # Runtime UI — panels, HUD, styles, control facade
|
├── ui/ # Runtime UI — panels, HUD, styles, controls
|
||||||
├── hud.js # Intelligence HUD + AI scene summary
|
├── hud.js # Intelligence HUD + AI scene summary
|
||||||
├── keySetup.js # POWER UP panel — in-app provider keys (dev server only)
|
├── keySetup.js # POWER UP panel — in-app provider keys (dev server only)
|
||||||
├── mapStackController.js # Basemap switching — Google 3D / Esri / OSM / ion stacks
|
├── mapStackController.js # Basemap switching — Google 3D / Esri / OSM / ion stacks
|
||||||
|
|||||||
+8
-1
@@ -69,11 +69,13 @@ The data proxies under `server/providers/` are written so the browser cannot tur
|
|||||||
- **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.
|
||||||
- **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.
|
||||||
|
- **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.
|
||||||
|
- **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 terrain route (`/api/terrain/heights`) uses the same cross-site checks, minus the forwarding-header refusal: it guards only the per-client quota and point cache, not a key or a bill, and the map needs it to render behind a reverse proxy.
|
- **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 terrain route (`/api/terrain/heights`) uses the same cross-site checks, minus the forwarding-header refusal: it guards only the per-client quota and point cache, not a key or a bill, and the map needs it to render behind a reverse proxy. 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.
|
||||||
@@ -98,6 +100,11 @@ The dev server is a **key broker**: every server-side key above is spendable by
|
|||||||
`compose.yaml` publishes on `127.0.0.1`: **widen that mapping and every LAN
|
`compose.yaml` publishes on `127.0.0.1`: **widen that mapping and every LAN
|
||||||
client arrives from the same gateway address, which hands them Provider
|
client arrives from the same gateway address, which hands them Provider
|
||||||
Settings too.** Treat publishing beyond loopback as the LAN opt-in above.
|
Settings too.** Treat publishing beyond loopback as the LAN opt-in above.
|
||||||
|
Compose forwards `GEV_KEY_SETUP_TRUSTED_PEERS` and `GEV_ALLOWED_HOSTS` from
|
||||||
|
the shell or project `.env` before the launcher runs. Set trusted peers to
|
||||||
|
`none` when widening the publish: it becomes an explicit empty list before
|
||||||
|
Vite loads configuration, so the gateway cannot regain Provider Settings
|
||||||
|
access. Unset or blank values preserve automatic gateway discovery.
|
||||||
- **Provider-side budgets are the real backstop.** For hard spend protection, configure limits where the money is: OpenAI platform usage limits, Google Cloud budget alerts + per-API quotas, and equivalent controls for any other keyed provider.
|
- **Provider-side budgets are the real backstop.** For hard spend protection, configure limits where the money is: OpenAI platform usage limits, Google Cloud budget alerts + per-API quotas, and equivalent controls for any other keyed provider.
|
||||||
- **Pinokio LAN and Cloudflare sharing are refused.** The current supported
|
- **Pinokio LAN and Cloudflare sharing are refused.** The current supported
|
||||||
Pinokio release re-reads sharing state when an app registers its Open URL and
|
Pinokio release re-reads sharing state when an app registers its Open URL and
|
||||||
|
|||||||
+72
-1
@@ -47,6 +47,75 @@ export const BROWSER_HEADERS = Object.freeze({
|
|||||||
'X-Content-Type-Options': 'nosniff',
|
'X-Content-Type-Options': 'nosniff',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content-Security-Policy for every `/api` response. API routes return data,
|
||||||
|
* never a document, so a response opened directly in a tab gets no origin and
|
||||||
|
* can load nothing.
|
||||||
|
*/
|
||||||
|
export const API_CSP = "default-src 'none'; sandbox";
|
||||||
|
|
||||||
|
/** Response headers every `/api` response carries. */
|
||||||
|
export const API_HEADERS = Object.freeze({
|
||||||
|
'Content-Security-Policy': API_CSP,
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a request reaches an `/api` mount, by the router's own rule: the
|
||||||
|
* prefix compared without case and followed by the end, `/` or `.`. The router
|
||||||
|
* reads an absolute-form target's path without resolving dot segments, so the
|
||||||
|
* path is taken here the same way (scheme and authority dropped, backslashes as
|
||||||
|
* slashes), alongside the raw and the fully parsed path; any match counts.
|
||||||
|
*/
|
||||||
|
function isApiPath(url) {
|
||||||
|
const raw = String(url || '/');
|
||||||
|
const target = raw
|
||||||
|
.trim()
|
||||||
|
.split(/[?#]/, 1)[0]
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/^[a-z][a-z0-9+.-]*:(?:\/\/[^/]*)?/i, '');
|
||||||
|
let parsed = '';
|
||||||
|
try {
|
||||||
|
parsed = new URL(raw, 'http://localhost').pathname;
|
||||||
|
} catch {
|
||||||
|
/* the unparsed paths still decide */
|
||||||
|
}
|
||||||
|
return [raw.split(/[?#]/, 1)[0], target, parsed].some((path) =>
|
||||||
|
/^\/api(?:$|[/.])/i.test(path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vite plugin that sets API_HEADERS on every API response before any other
|
||||||
|
* middleware runs, so success, error, partial, refused and unknown-route
|
||||||
|
* answers all carry them. Vite's `server.headers` and `preview.headers` only
|
||||||
|
* reach the documents and assets Vite serves itself. A route may still add
|
||||||
|
* headers; a route's own Content-Security-Policy is sent as an additional
|
||||||
|
* policy, which can only restrict further, never replace this one.
|
||||||
|
*/
|
||||||
|
export function apiResponseHeadersPlugin() {
|
||||||
|
const install = (server) => {
|
||||||
|
server.middlewares.use((req, res, next) => {
|
||||||
|
if (!isApiPath(req.url)) return next();
|
||||||
|
for (const [name, value] of Object.entries(API_HEADERS))
|
||||||
|
res.setHeader(name, value);
|
||||||
|
const setHeader = res.setHeader.bind(res);
|
||||||
|
res.setHeader = (name, value) =>
|
||||||
|
String(name).toLowerCase() === 'content-security-policy' &&
|
||||||
|
String(value) !== API_CSP
|
||||||
|
? setHeader(name, `${API_CSP}, ${value}`)
|
||||||
|
: setHeader(name, value);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
name: 'api-response-headers',
|
||||||
|
enforce: 'pre',
|
||||||
|
configureServer: install,
|
||||||
|
configurePreviewServer: install,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Build browser assets with explicit inputs; never load environment or providers. */
|
/** Build browser assets with explicit inputs; never load environment or providers. */
|
||||||
export function createBrowserViteConfig({
|
export function createBrowserViteConfig({
|
||||||
plugins = [],
|
plugins = [],
|
||||||
@@ -61,7 +130,9 @@ export function createBrowserViteConfig({
|
|||||||
} = {}) {
|
} = {}) {
|
||||||
return {
|
return {
|
||||||
plugins: [
|
plugins: [
|
||||||
// First, so no provider route answers a Host the server does not allow.
|
// First, so every API response, including a Host refusal, carries them.
|
||||||
|
apiResponseHeadersPlugin(),
|
||||||
|
// Next, so no provider route answers a Host the server does not allow.
|
||||||
hostCheckPlugin(),
|
hostCheckPlugin(),
|
||||||
cesium(),
|
cesium(),
|
||||||
applicationHtmlPlugin(),
|
applicationHtmlPlugin(),
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Report actual outcomes when voice enables or disables CCTV through the shared
|
||||||
|
layer lifecycle, including failed and cancelled changes (manjunath22466,
|
||||||
|
#1205).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Keep NASA FIRMS fire and ALPR camera labels English in voice tool results
|
||||||
|
(`track_entity`, `get_entity_context`, pointer context); the voice card still
|
||||||
|
shows them in the interface language (daikaginza, #1207).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Avoid starting or queuing a reply when the local channel rejects a typed OpenAI
|
||||||
|
command, while preserving an earlier accepted deferred input
|
||||||
|
(Manjunath, #1220).
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
- Add continuous globe rotation with direction and speed controls and a globe-fit
|
||||||
|
check before starting (manjunath22466, #1225).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- The first-run card can be closed on a phone or tablet. "ESC to dismiss" is
|
||||||
|
now a button that closes the card the same way ESC does; touch screens read
|
||||||
|
DISMISS, and phones no longer hide it, so choosing a mission is not the only
|
||||||
|
way out (KnottyDyes, #1226).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Load Ontario 511 cameras again. 511on.ca's camera endpoint now requires a
|
||||||
|
free developer key; set `ONTARIO_511_API_KEY` to enable the pack, which is
|
||||||
|
otherwise skipped with one log line saying where to get a key
|
||||||
|
(quanticsoul4772, #1228).
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
- OpenSky sign-in matches what OpenSky accepts since 2026-03-18. The server
|
||||||
|
now reads `OPENSKY_CREDENTIALS_FILE` (the `credentials.json` OpenSky's
|
||||||
|
account page downloads) on every platform, not only through the macOS
|
||||||
|
launcher, and no longer sends retired username/password Basic auth:
|
||||||
|
`OPENSKY_AUTH_MODE=basic` loads anonymously with one warning saying what to
|
||||||
|
set instead (quanticsoul4772, #1229).
|
||||||
|
- Credential-file warnings omit parser excerpts and filesystem error details;
|
||||||
|
the path example also works with the macOS launchers.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Harden the Tarktee (Estonia) DATEX2 camera catalog fetches: refuse redirects,
|
||||||
|
cancel both response streams when either response fails or overflows the 4 MB
|
||||||
|
byte cap, and deduplicate camera IDs before per-pack prioritization
|
||||||
|
(yaka0007, #1233).
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- Serve the camera catalog while it refreshes. An expired catalog made every
|
||||||
|
camera request wait for the slowest pack to reload; it is now answered at
|
||||||
|
once while one background refresh runs, so only the first load waits. Estonia's
|
||||||
|
slow Tarktee feeds get 30 s instead of 15 s, so the pack no longer drops out
|
||||||
|
on about a third of loads (quanticsoul4772, #1234).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Require a geocoded containment anchor for ambiguous analyst region names,
|
||||||
|
preserve boundary provenance and cancellation safety, and retain exact-name
|
||||||
|
precedence for natural outlines (manjunath22466, #1235).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Add Taiwan's national freeway cameras from the Freeway Bureau, MOTC to CCTV:
|
||||||
|
~1,800 cameras read at runtime from the official MOTC 2.0 open-data table,
|
||||||
|
with on-demand snapshots and attribution. No API key is required; set
|
||||||
|
`CCTV_TAIWAN_ENABLED=0` to skip it (419vive, #1237).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- `get_traffic_flow` flags stale cached TomTom tiles and reports the oldest
|
||||||
|
tile fetch time (`stale`, `oldest_tile_at`), with pending freshness metadata
|
||||||
|
released on failed or cancelled tile reads (quanticsoul4772, #1239).
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- Replace the blocky JPEG sky around the globe with the 9,096 stars of the
|
||||||
|
Yale Bright Star Catalogue, drawn as sharp points where they are at the
|
||||||
|
scene time, over a soft Milky Way from ESO/S. Brunier (CC BY 4.0). The Sun
|
||||||
|
and Moon stay; the anime style hides the Milky Way. Assets: 296 kB (gekh,
|
||||||
|
#1243).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- The terrain-heights bounds tests no longer read the checkout's
|
||||||
|
`.gev-cache/terrain-heights.json`. Each test gets a cache file of its own, so
|
||||||
|
`npm test` no longer fails after the dev server has cached terrain near
|
||||||
|
Austin (KnottyDyes, #1244).
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
- Prevent malformed Overpass filters from blocking the local server, and reject
|
||||||
|
cross-site or excessive query requests before reading their bodies
|
||||||
|
(asturksever, #1264; fixes #1248).
|
||||||
|
- Release unread responses from the optional Google-token probe so keyless
|
||||||
|
browser validation can settle; the Overpass gate recognizes only that probe's
|
||||||
|
expected 404 as an unavailable capability (asturksever, #1264).
|
||||||
|
- Make tracking arrival checks respect the aircraft update cadence while checking
|
||||||
|
orientation on the first reveal, avoiding false failures at faster render
|
||||||
|
rates (asturksever, #1264).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Honor Docker Compose trusted-peer and allowed-host settings from the shell
|
||||||
|
or project `.env`, and let `GEV_KEY_SETUP_TRUSTED_PEERS=none` disable gateway
|
||||||
|
access to Provider Settings before Vite starts (asturksever, #1265).
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
- Normalize station tags and retain valid mixed rows; skipped malformed rows still report degraded coverage (asturksever, #1268).
|
||||||
|
- Correct Radio QA push-to-talk setup to target the mounted voice controller (asturksever, #1268).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- Coalesce refreshes and cool down opensky failures (asturksever, #1269).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- Respect provider cooldowns and bound tle groups (asturksever, #1270).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- Reject inherited style keys during restore (asturksever, #1271).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- Retire unavailable history and keep local trails (asturksever, #1273).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- Scan the kml root delimiter in bounded time (asturksever, #1274).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Preserve enable during first-load replacement (asturksever, #1275).
|
||||||
|
- Release disabled weather camera listeners and make teardown resource leaks fail the browser probe (asturksever, #1275).
|
||||||
|
- Keep visible-page weather teardown probes focused through the browser testing API (asturksever, #1275).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- Bound each height request to one upstream chunk (asturksever, #1276).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- Reserve singleflight before disk cache reads (asturksever, #1278).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- Throttle mission hover picks with a trailing check for the last pointer position, and clear stale cursors (asturksever, #1279).
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
- Hold rendering only for animated content (asturksever, #1281).
|
||||||
|
- Exercise current vector road sources and published camera bounds in Traffic fixture QA (asturksever, #1281).
|
||||||
|
- Establish browser-level foreground visibility for Traffic QA and reject hidden-page samples (asturksever, #1281).
|
||||||
|
- Supply CORS headers on cross-origin traffic fixtures and report browser exceptions without disabling browser security (asturksever, #1281).
|
||||||
|
- Isolate the Traffic keyless browser scenario from the keyed page’s workers and intercepted cache (asturksever, #1281).
|
||||||
|
- Restore minimized test windows before screenshots so native compositor pauses cannot stall browser acceptance (asturksever, #1281).
|
||||||
|
- Intercept only Traffic provider URLs, leaving Cesium worker and module loading untouched (asturksever, #1281).
|
||||||
|
- Settle an unfinished Traffic height correction when the last animated point leaves, so the layer stops reporting loading once its render hold is released (asturksever, #1281).
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
- Pause periodic network updates in hidden tabs; the overdue update on return restarts the polling schedule, so the next poll follows one interval later even when that update fails (asturksever, #1282).
|
||||||
|
- Report and guard browser visibility during Transit QA so hidden-page polling pauses cannot masquerade as playback regressions (asturksever, #1282).
|
||||||
|
- Require each live city to supply its own feed vehicles in Transit QA; retained vehicles cannot qualify another city (asturksever, #1282).
|
||||||
|
- Bound the Transit card-anchor probe by observed poll coverage instead of a fixed 20-second window (asturksever, #1282).
|
||||||
|
- Keep visible-page Transit probes focused through the browser testing API (asturksever, #1282).
|
||||||
|
- Add a real-browser regression gate for hidden-tab periodic work, explicit refresh, resume and teardown (asturksever, #1282).
|
||||||
|
- Restore minimized test windows before screenshots so native compositor pauses cannot stall browser acceptance (asturksever, #1282).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Report cached CelesTrak satellite records as stale in the Data Layers row,
|
||||||
|
preserving separate core and dense catalog provenance through refresh,
|
||||||
|
removal and lifecycle cleanup (rudycelekli, #1283).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Keep aircraft route labels on the scheduled airport leg instead of accepting
|
||||||
|
its distant great-circle extensions, preserving generous endpoint tolerance
|
||||||
|
across the signed-angle branch cut (rudycelekli, #1284).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- The Street Level SINCE note sits on the value's line, in its type and a
|
||||||
|
size smaller: with KartaView lit it reads ANY DATE (by upload date). It
|
||||||
|
had lost its size variable in the Street Level slim-down and rendered
|
||||||
|
larger than the value, on a line of its own (asturksever, #1286).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Give ordinary sliders a shared native control and consistent track, thumb,
|
||||||
|
value, focus and disabled styling while preserving feature behavior and
|
||||||
|
specialized controls (mazy1998, #1287).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- A Street Level provider whose `zoomFor` still answers `{zoom, kind}`, the
|
||||||
|
shape before the core went slim, draws its lines again instead of a silent
|
||||||
|
blank; an overview level or any answer that is not a whole zoom shows the
|
||||||
|
zoom-in hint (asturksever, #1288).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Add Simplified Chinese (zh-CN) to the language picker, with localized dates
|
||||||
|
and readable Chinese labels. Based on translations by @abiyeenzo (#914)
|
||||||
|
and @EDIT-Aeterna (#979).
|
||||||
|
(German, @gekh, #1298).
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- Unify segmented selectors for model coverage, detection allocation, drawing
|
||||||
|
shape, Street View and imagery comparison while preserving their existing
|
||||||
|
behavior. Keep compact labels such as FILL and FLAT on one line, and let
|
||||||
|
collapsed Weather release space to the open rail panel. Reserve the full mode
|
||||||
|
button height in Recent Imagery so keyboard focus is not clipped (mazy1998, #1301).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Give ordinary Scene and CCTV actions shared native buttons with consistent
|
||||||
|
default, destructive and compact styling while preserving their existing
|
||||||
|
behavior and specialized controls (mazy1998, #1302).
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
- `get_wind` flags stale forecasts (`stale`) when the wind proxy serves a
|
||||||
|
cached model run (quanticsoul4772, #1303).
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
- Point distances wrap longitude differences across the antimeridian, in voice
|
||||||
|
proximity checks and `places_nearby` (quanticsoul4772, #1304).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Share native dropdown and numeric-field styling across Scene, CCTV, Display
|
||||||
|
and Radio controls, with a shared CCTV number-input builder that preserves
|
||||||
|
existing precision, validation and commit behavior (mazy1998, #1305).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Share read-only status badge presentation across CCTV source/frame labels,
|
||||||
|
calibration provenance and Weather coverage, preserving status meanings,
|
||||||
|
palettes, translations and announcements while preparing the view contract
|
||||||
|
for a future Vue migration (mazy1998, #1306).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Clarify shared field CSS ownership: CCTV owns calibration editor sizing,
|
||||||
|
while Radio and Cyber supply explicit field palettes without changing the
|
||||||
|
current appearance or native control behavior (mazy1998, #1309).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- Validate provider response formats, strengthen camera source checks, and apply consistent API response protections (samehkhamis, #1317).
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- Remove dead code across the repository: three modules nothing imports
|
||||||
|
(`src/ui.js`, `src/standalone/ui.js`, `src/data/localLayers.js`), exports
|
||||||
|
and locals nothing reads, a duplicate copy of the bundled dams data and an
|
||||||
|
unused ship model. The app no longer builds an unused default wind layer
|
||||||
|
and Bhote Koshi locator layer when it loads. Package exports are unchanged
|
||||||
|
(samehkhamis, #1321).
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
- Export the KartaView and Panoramax Street Level providers, so a host composing
|
||||||
|
its own provider list can offer the keyless providers without Mapillary
|
||||||
|
(samehkhamis, #1324).
|
||||||
|
- A Mapillary source can take a `graphEndpoint`, a Graph API base that adds the
|
||||||
|
token itself (such as a same-origin proxy). Its sequence lookups go there
|
||||||
|
through the source's transport, and the photo viewer uses MapillaryJS's
|
||||||
|
Graph provider at that endpoint, so no client token is needed in the page
|
||||||
|
(samehkhamis, #1324).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Add a phone layout for viewports 720px and narrower: the globe stays full
|
||||||
|
screen and a bottom tab bar (Layers, Cameras, Context, Search, More) opens one
|
||||||
|
panel sheet at a time, keeping attribution visible and scene STOP reachable.
|
||||||
|
Adapted from the phone tab-shell in #203 by blearymoth (samehkhamis, #1325).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Evict terrain-height cache entries in fetch-time order without repeatedly
|
||||||
|
sorting the full cache, while keeping refreshed points newest and ordering
|
||||||
|
restored entries by their fetch time (daikaginza, #1326).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Improve touch selection of aircraft and vessels through the shared contact
|
||||||
|
picker, reveal tracked aircraft details when opening Context, support downward
|
||||||
|
swipe dismissal of phone drawers while preserving scrolling and controls, and
|
||||||
|
enlarge the phone logo (samehkhamis, #1338).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Read a hemisphere letter that is separated from its number, so a search for
|
||||||
|
`40.7128 N 74.0060 W` flies there instead of falling through to the geocoders
|
||||||
|
(Rudy Celekli, @rudycelekli, #1340).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Reject impossible KartaView photo timestamps, so 31 February is unknown
|
||||||
|
instead of rolling forward into March and passing the imagery date filter
|
||||||
|
(Rudy Celekli, @rudycelekli, #1341).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Keep an impossible wildfire acquisition date off the detection clock, so a
|
||||||
|
fire is not labeled with a later day it did not happen
|
||||||
|
(Rudy Celekli, @rudycelekli, #1342).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Label NHC cyclone classifications by their published meanings: `PTC` is a
|
||||||
|
post-tropical cyclone, `PC` a potential tropical cyclone, and subtropical
|
||||||
|
systems arrive as `STS` and `STD` (rudycelekli, #1349).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Convert NHC cyclone movement speed from miles per hour to knots, so
|
||||||
|
`movement.speedKt` in cyclone query results is no longer about 15% high
|
||||||
|
(rudycelekli, #1350).
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- Keep a tracked vessel's speed, course, heading and fix time when AISStream
|
||||||
|
static data arrives; the static message only merges name, type, destination
|
||||||
|
and IMO (rudycelekli, #1351).
|
||||||
|
- Apply changed static metadata immediately, retaining known fields when a
|
||||||
|
later static report omits them, without refreshing the vessel's position
|
||||||
|
(samehkhamis, #1351).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Read a destination heading that rounds to north as 000. 359.6° was shown as
|
||||||
|
360° on the destination bearing while the compass tape already uses 000
|
||||||
|
(Rudy Celekli, @rudycelekli, #1352).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Count a transit report that is 90 seconds old as one minute. The card kept
|
||||||
|
seconds through 89s, then rounded 90s up to "Reported 2 min ago"
|
||||||
|
(Rudy Celekli, @rudycelekli, #1353).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Drop the decimal when fire radiative power rounds to 10 MW. A 9.96 MW
|
||||||
|
detection was labeled 10.0 MW because the one-decimal cutoff ran before
|
||||||
|
rounding (Rudy Celekli, @rudycelekli, #1354).
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- Aircraft 3D models (Flights, Military Flights, Local ADS-B) skip Cesium's
|
||||||
|
dynamic environment map, a sky render and GPU readback per model. With
|
||||||
|
~150 models in the scene at Atlanta on Google 3D, GPU time per frame at
|
||||||
|
rest falls from 14.3-14.8 ms to 12.1-12.5 ms; a same-model comparison
|
||||||
|
showed no visible change (quanticsoul4772, #1366).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Turning a layer off frees the memory its entities held. Cesium kept
|
||||||
|
removed layers' draw commands in reused per-pass arrays, so Data Centers
|
||||||
|
kept 116 MiB after it was switched off, submarine cables 74 MiB and dams
|
||||||
|
30 MiB; they now keep 13, 9 and 9 MiB (quanticsoul4772, #1367).
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- Increase touch-first globe zoom sensitivity to three times the default while
|
||||||
|
retaining Cesium collision and distance limits.
|
||||||
|
- Reuse one delayed display timestamp per civil-aircraft fleet update and cache
|
||||||
|
extrapolation coordinate frames until their fix coordinates change. Preserve
|
||||||
|
contact counts, movement cadence, tracked motion and ground corrections.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- Reduce repeated work in aircraft detection candidates by caching unchanged
|
||||||
|
callsign/registration text and reusing resolved fleet models. Metadata updates,
|
||||||
|
live positions and tracked-aircraft/model handoffs remain immediate
|
||||||
|
(samehkhamis, #1379).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- The HUD's AI place summary drops a reply that arrives after the camera has
|
||||||
|
started moving, so a summary asked for before a flight no longer names the
|
||||||
|
old place mid-flight. Cancelled or partially typed summaries retry even if the place
|
||||||
|
context stays the same (quanticsoul4772, #1384).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Show a route leg of 59.5 seconds or more as 1 min. The route summary and step
|
||||||
|
cards kept seconds below 60, then rounded 59.6 s up to "60 s"
|
||||||
|
(Rudy Celekli, @rudycelekli, #1385).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Show a route leg of 995 m or more as 1.0 km. The route summary and step
|
||||||
|
cards kept meters below 1000, then rounded 996 m up to "1000 m"
|
||||||
|
(Rudy Celekli, @rudycelekli, #1386).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Show a Contacts entry 999.5 m or more away as 1.0 km. The panel kept meters
|
||||||
|
below 1000, then rounded 999.6 m up to "1000 m"
|
||||||
|
(Rudy Celekli, @rudycelekli, #1387).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Show a drawn line of 999.5 m or more as 1.0 km. The draw hint kept metres
|
||||||
|
below 1000, then rounded 999.6 m up to "1000 m"
|
||||||
|
(Rudy Celekli, @rudycelekli, #1388).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Show cockpit wind from 359.5° or more as 0°, not 360°. Rounding 359.6°
|
||||||
|
printed "N · 360°" on the local wind direction
|
||||||
|
(Rudy Celekli, @rudycelekli, #1389).
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- Report a fire detection's acquisition time as a UTC timestamp in
|
||||||
|
`get_active_fires`. The tool joined FIRMS's unpadded date and time, so a
|
||||||
|
00:45 UTC detection read "2026-01-01 45"; it now reads
|
||||||
|
`2026-01-01T00:45:00.000Z`. Missing or invalid dates and times remain
|
||||||
|
unknown rather than rolling into another calendar day
|
||||||
|
(Rudy Celekli, @rudycelekli, #1395).
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Show a locally heard aircraft tracking 359.5° or more as TRK 0°. The Local
|
||||||
|
ADS-B card rounded the track without wrapping, so 359.7° read "TRK 360°"
|
||||||
|
(Rudy Celekli, @rudycelekli, #1396).
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- Add a Scope checkbox and extend feathering to 200%, retaining scope settings
|
||||||
|
across HUD layouts, share links and scenes. First run opens with Scope ON at
|
||||||
|
100% feather; explicit settings and historical links retain their values. Give
|
||||||
|
Tactical, Operator and Minimal text black backplates only when Scope is OFF,
|
||||||
|
including vertical metadata, without changing the original text color, glow or
|
||||||
|
opacity. Scope ON retains its original HUD presentation (bilawalsidhu, #1397).
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
- Give each Statens vegvesen camera its own ground height from Kartverket's
|
||||||
|
keyless point-height API (CC BY 4.0) instead of one flat 150 m prior, so
|
||||||
|
mountain-pass cameras no longer sit hundreds of metres below the terrain
|
||||||
|
where neither the point-height prior nor the ground snap resolves. About 850
|
||||||
|
of ~855 cameras get a height; the rest keep the old prior. Requests carry 20
|
||||||
|
points, the most the API answers from its 1 m terrain model, eight at a
|
||||||
|
time; a cold lookup takes about 6–13 s. Cameras over water use the water
|
||||||
|
surface rather than the sea-floor depth the API returns there; for bridge
|
||||||
|
cameras that is still below the deck. Heights are cached for good and misses
|
||||||
|
for 6 h, so a refresh only asks about new or moved cameras. After a
|
||||||
|
failing request (a 408, 429 or 5xx, a network error, a request over 10 s or
|
||||||
|
an unreadable body) the lookup stops and backs off (Retry-After, else
|
||||||
|
30 min, longer than the refresh). The lane waits at most 12 s from its
|
||||||
|
start, and only on a cold cache; a slower first lookup leaves the cameras it
|
||||||
|
had not reached on the flat prior until the next 15-minute refresh, and
|
||||||
|
keeps filling the cache meanwhile.
|
||||||
|
`CCTV_VEGVESEN_HEIGHTS=0` turns it off. Follow-up to #929 (trygvehhh, #945).
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
- Add Hong Kong Transport Department traffic snapshot cameras as a keyless CCTV
|
||||||
|
source pack: the public DATA.GOV.HK English locations XML, frames pinned to
|
||||||
|
`tdcctv.data.one.gov.hk`, with DATA.GOV.HK Terms of Use attribution. By
|
||||||
|
default nearly the full TD catalog (~1,013 cameras; cap 1,100) loads,
|
||||||
|
prioritized across Hong Kong Island, Kowloon and the New Territories. Headings
|
||||||
|
use the shared id-hash fallback at low confidence. `CCTV_HK_MAX_SOURCES` sets
|
||||||
|
the cap (8–1,200) and `CCTV_HK_ENABLED=0` turns the pack off (JCY2515, #990).
|
||||||
@@ -5,6 +5,11 @@ services:
|
|||||||
# lands in the bind-mounted repo, so run as you, not root. UID/GID are
|
# lands in the bind-mounted repo, so run as you, not root. UID/GID are
|
||||||
# unset on Docker Desktop, where the 1000 fallback is ignored anyway.
|
# unset on Docker Desktop, where the 1000 fallback is ignored anyway.
|
||||||
user: "${UID:-1000}:${GID:-1000}"
|
user: "${UID:-1000}:${GID:-1000}"
|
||||||
|
# Forward explicit shell/.env policy before the launcher discovers a gateway.
|
||||||
|
# Unset values stay unset so the default loopback-only install still works.
|
||||||
|
environment:
|
||||||
|
- GEV_KEY_SETUP_TRUSTED_PEERS
|
||||||
|
- GEV_ALLOWED_HOSTS
|
||||||
ports:
|
ports:
|
||||||
# Publish to loopback only. Docker's default ("4173:4173") would bind
|
# Publish to loopback only. Docker's default ("4173:4173") would bind
|
||||||
# every host interface, which quietly turns the LAN warning in the README
|
# every host interface, which quietly turns the LAN warning in the README
|
||||||
|
|||||||
+43
-7
@@ -79,7 +79,6 @@ moves it. Do not label all ingestion modules platform-independent by filename.
|
|||||||
|
|
||||||
| Retained entry | Owner and current reason |
|
| Retained entry | Owner and current reason |
|
||||||
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `src/ui.js` | UI facade; direct callers retain standalone StyleManager defaults. Normal assembly uses `src/ui/composition.js` |
|
|
||||||
| `src/data/manager.js` | Lifecycle/panel compatibility and existing tests; normal assembly constructs lifecycle and presentation separately |
|
| `src/data/manager.js` | Lifecycle/panel compatibility and existing tests; normal assembly constructs lifecycle and presentation separately |
|
||||||
| `src/data/<layer>.js` | Legacy default instances; new assembly calls layer factories with supplied sources |
|
| `src/data/<layer>.js` | Legacy default instances; new assembly calls layer factories with supplied sources |
|
||||||
| `src/data/localGeojson.js` | Infrastructure compatibility factories; catalog imports services from `src/app/localGeojsonServices.js` directly |
|
| `src/data/localGeojson.js` | Infrastructure compatibility factories; catalog imports services from `src/app/localGeojsonServices.js` directly |
|
||||||
@@ -87,9 +86,8 @@ moves it. Do not label all ingestion modules platform-independent by filename.
|
|||||||
| `src/app/sources.js`, `src/services/application.js` | Existing default/override API; current application construction supplies its own instances |
|
| `src/app/sources.js`, `src/services/application.js` | Existing default/override API; current application construction supplies its own instances |
|
||||||
| `server/providers/local.js` | Existing Node composition barrel and provider helper re-exports |
|
| `server/providers/local.js` | Existing Node composition barrel and provider helper re-exports |
|
||||||
|
|
||||||
The two exact standalone-import exceptions are `src/ui.js` to
|
The one exact standalone-import exception is `server/providers/local.js` to
|
||||||
`src/standalone/catalog.js` and `server/providers/local.js` to
|
`server/standalone/key-setup.js`. It does not authorize new compatibility
|
||||||
`server/standalone/key-setup.js`. They do not authorize new compatibility
|
|
||||||
back-edges. Voice actions use plain `src/data/feedState.js`, not the manager
|
back-edges. Voice actions use plain `src/data/feedState.js`, not the manager
|
||||||
facade. Settings filesystem hardening belongs in
|
facade. Settings filesystem hardening belongs in
|
||||||
`server/standalone/key-setup-hardening.mjs`; its settings policy is unchanged.
|
`server/standalone/key-setup-hardening.mjs`; its settings policy is unchanged.
|
||||||
@@ -237,7 +235,7 @@ DOM elements and callbacks; importing it creates no browser state. `destroy()`
|
|||||||
removes owned listeners and cancels pending work without changing saved state
|
removes owned listeners and cancels pending work without changing saved state
|
||||||
or moving focus. Call it before removing or replacing the controls.
|
or moving focus. Call it before removing or replacing the controls.
|
||||||
|
|
||||||
`src/ui.js` retains panel layout, persistence, share restoration and application
|
`src/ui/applicationShell.js` retains panel layout, persistence, share restoration and application
|
||||||
reactions to state changes. Map Source selection and Location draft cleanup are
|
reactions to state changes. Map Source selection and Location draft cleanup are
|
||||||
provided through callbacks. The component imports no globe, data, application
|
provided through callbacks. The component imports no globe, data, application
|
||||||
or server modules. Package checks and scoped formatting cover this entry.
|
or server modules. Package checks and scoped formatting cover this entry.
|
||||||
@@ -374,6 +372,15 @@ renderer, persistence or services. The facade retains panel visibility, share
|
|||||||
restore claims and render scheduling. Both controls are destroyed before the
|
restore claims and render scheduling. Both controls are destroyed before the
|
||||||
facade's asynchronous teardown can yield.
|
facade's asynchronous teardown can yield.
|
||||||
|
|
||||||
|
## Ordinary action buttons
|
||||||
|
|
||||||
|
`src/ui/actionButton.js` is a native, listener-free view helper used by Scene
|
||||||
|
presentation and sharing-toolbar owners. It receives translated text and explicit
|
||||||
|
presentation/accessibility properties. Feature controllers own action dispatch,
|
||||||
|
state, persistence and cleanup. Static Scenes/CCTV templates use the same CSS
|
||||||
|
surface and preserve IDs and layout hooks. The helper imports no application,
|
||||||
|
renderer, storage or services; it belongs to the consuming package closures.
|
||||||
|
|
||||||
## Display controls
|
## Display controls
|
||||||
|
|
||||||
`ui/display` owns Display button, selector and slider subscriptions. It receives
|
`ui/display` owns Display button, selector and slider subscriptions. It receives
|
||||||
@@ -461,8 +468,8 @@ release listeners; pending action feedback is limited to its current owner.
|
|||||||
## UI assembly and styles
|
## UI assembly and styles
|
||||||
|
|
||||||
`ui/shell` assembles controls from supplied existing layer, navigation, terrain,
|
`ui/shell` assembles controls from supplied existing layer, navigation, terrain,
|
||||||
rendering, HUD and share operations. `src/standalone/ui.js` provides the running
|
rendering, HUD and share operations. `src/standalone/controls.js` provides the running
|
||||||
application's instances; `src/ui.js` remains the compatibility entry. The shell
|
application's instances. The shell
|
||||||
imports no standalone bootstrap or concrete live layer implementation.
|
imports no standalone bootstrap or concrete live layer implementation.
|
||||||
Panel layout, position/drag, notices, recording and deferred UI work have separate
|
Panel layout, position/drag, notices, recording and deferred UI work have separate
|
||||||
owners with synchronous cleanup. Existing scene, share and HUD engines retain
|
owners with synchronous cleanup. Existing scene, share and HUD engines retain
|
||||||
@@ -729,3 +736,32 @@ sources supply `{records, droppedCount, status, saturated}`; viewport filtering,
|
|||||||
exact-bound retry and rendering stay in the layer. Legacy cache saturation is
|
exact-bound retry and rendering stay in the layer. Legacy cache saturation is
|
||||||
decoded in the source. Source and geometry modules remain independent of Cesium,
|
decoded in the source. Source and geometry modules remain independent of Cesium,
|
||||||
DOM and platform middleware.
|
DOM and platform middleware.
|
||||||
|
|
||||||
|
### Native form fields
|
||||||
|
|
||||||
|
`src/ui/formField.js` constructs native numeric inputs without listeners or
|
||||||
|
application state. Static selects and number inputs use the same `gev-field`
|
||||||
|
surface, with explicit compact and inline densities. Feature owners retain
|
||||||
|
option identity/order, bounds, parsing, draft values, input/change timing and
|
||||||
|
teardown. CCTV effective-pose conversion and tuner/device operations remain
|
||||||
|
feature responsibilities. A Vue replacement can reuse this contract and CSS;
|
||||||
|
it replaces DOM construction, rather than adding a second state owner.
|
||||||
|
|
||||||
|
The shared stylesheet owns field surfaces and disabled opacity; feature and theme
|
||||||
|
palettes use `--gev-field-background`, `--gev-field-border-color`,
|
||||||
|
`--gev-field-color` and `--gev-field-disabled-opacity`. Radio retains its own
|
||||||
|
compact density and arrow space. CCTV owns calibration editor width in
|
||||||
|
`cctv.css`; the shared inline density does not impose a feature-specific width.
|
||||||
|
Cyber supplies a shared-field palette explicitly and keeps its legacy native
|
||||||
|
field rules separate, so Radio's palette remains an intentional exception.
|
||||||
|
|
||||||
|
### Read-only status badges
|
||||||
|
|
||||||
|
`src/ui/statusBadge.js` constructs/updates ordinary badges without listeners,
|
||||||
|
status inference or live-region policy. It consumes translated text and controlled
|
||||||
|
visibility; appearance variants share CSS rather than mapping machine states to
|
||||||
|
universal severity. CCTV owns frame/source and calibration rules and their data
|
||||||
|
attributes, while Weather owns coverage labels. The keyed rail-card owner keeps
|
||||||
|
node identity, header focus, disclosure and teardown. The shared adapter never
|
||||||
|
creates another state store or announces all card updates. A Vue view can replace
|
||||||
|
this adapter and retain these presentation inputs and styles.
|
||||||
+424
-40
@@ -1,5 +1,39 @@
|
|||||||
# God's Eye View Current State
|
# God's Eye View Current State
|
||||||
|
|
||||||
|
Aircraft detection candidates reuse normalized display text until callsign or
|
||||||
|
registration changes, including in-place enrichment. Each candidate resolves its
|
||||||
|
fleet model once per collection. Positions, model ownership and tracked-aircraft
|
||||||
|
anchors remain live on every collection; contact admission is unchanged.
|
||||||
|
|
||||||
|
Civil aircraft share one delayed render timestamp per fleet update. Extrapolated
|
||||||
|
fixes reuse their local coordinate frame until their coordinates change, including
|
||||||
|
in-place ground-height corrections. Update cadence, contact counts, interpolation,
|
||||||
|
turn rates and tracked-aircraft motion are unchanged. Cached frames are weakly
|
||||||
|
keyed by each position fix and do not retain retired history.
|
||||||
|
|
||||||
|
Touch-first devices use three times the default Cesium zoom sensitivity, so
|
||||||
|
pinching traverses globe and street scales with fewer gestures. Cesium retains
|
||||||
|
its distance-aware zoom, collision handling and minimum/maximum distance limits.
|
||||||
|
Fine-pointer desktop mouse and trackpad sensitivity is unchanged.
|
||||||
|
|
||||||
|
Analyst region queries resolve unique Natural Earth names offline without
|
||||||
|
geocoding, so Gold Coast and Ivory Coast name the West African natural regions.
|
||||||
|
Duplicate names require a geocoded containment anchor; when several entries for
|
||||||
|
the same name contain that anchor, the lookup stays unresolved rather than
|
||||||
|
selecting the largest entry. Natural outlines try exact names before aliases and
|
||||||
|
broader suffix variants. Follow-ups retain boundary provenance, and cancelled
|
||||||
|
lookups preserve the previous analyst cohort and boundary source. Navigation
|
||||||
|
search keeps a successful geocode, with no search or view-based region recovery.
|
||||||
|
|
||||||
|
Manager-owned periodic network updates pause while the document is hidden. Returning to a visible tab runs one overdue update per enabled layer without overlapping an active refresh and restarts that layer's polling schedule from the return, so the next poll follows one interval later whether or not the catch-up update succeeds; explicit refresh requests remain available. Transit also defers camera-driven feed admission while hidden and rechecks coverage on return. Visibility listeners are released with their polling intervals.
|
||||||
|
|
||||||
|
Terrain height requests accept at most 64 points, matching the browser service's
|
||||||
|
existing batch size and one bounded upstream chunk. Larger requests return HTTP
|
||||||
|
413 before disk-cache loading or upstream work; malformed coordinates remain 400.
|
||||||
|
The browser continues to split larger workloads into sequential 64-point requests.
|
||||||
|
|
||||||
|
Traffic holds continuous rendering only while enabled with animated dots or a pulsing jam heat-line. Empty and high-altitude views release the hold. A failed load leaves no hold only when no animated content is retained: if an earlier load succeeded, ingestion keeps its animated points and the hold stays. Population admission reacquires the hold and resets animation time. Clear, disable, and destroy also release it. Retained fading dots keep animation alive until they are removed. When the last point leaves mid height correction, the correction is settled at once, before the hold is released, so it cannot keep the layer reporting loading.
|
||||||
|
|
||||||
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
|
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
|
||||||
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
||||||
canonical; `greatCircleMeters` is its exact metre conversion. Missing,
|
canonical; `greatCircleMeters` is its exact metre conversion. Missing,
|
||||||
@@ -21,6 +55,16 @@ time. This clears obsolete contacts and settles startup without manufacturing
|
|||||||
position timestamps. Stale, incomplete and malformed empty results retain the
|
position timestamps. Stale, incomplete and malformed empty results retain the
|
||||||
existing recovery behavior.
|
existing recovery behavior.
|
||||||
|
|
||||||
|
Every `/api` response on the dev and preview servers carries
|
||||||
|
`Content-Security-Policy: default-src 'none'; sandbox` and
|
||||||
|
`X-Content-Type-Options: nosniff`. `apiResponseHeadersPlugin` sets them ahead of
|
||||||
|
the Host check and matches `/api` the way the router mounts it (any letter case, a
|
||||||
|
`/` or `.` boundary, absolute-form targets), so errors, partial responses, Host
|
||||||
|
refusals and unknown routes are covered too; a route's own policy is sent as an
|
||||||
|
additional policy, never in place of it. The app document's policy is unchanged.
|
||||||
|
The GBFS proxy relays only bodies that parse as JSON, re-serialized as
|
||||||
|
`application/json; charset=utf-8`; anything else answers 502.
|
||||||
|
|
||||||
Catalog source contracts and their consuming layers are declared in
|
Catalog source contracts and their consuming layers are declared in
|
||||||
`src/app/sourceComposition.js`. Omitted or null acquisition sources preserve layer
|
`src/app/sourceComposition.js`. Omitted or null acquisition sources preserve layer
|
||||||
registration and serialization, report unavailable, and are refused by the existing
|
registration and serialization, report unavailable, and are refused by the existing
|
||||||
@@ -142,6 +186,32 @@ Regression coverage lives in `src/tools/area.test.mjs`,
|
|||||||
`src/tools/queries/environment.test.mjs` and
|
`src/tools/queries/environment.test.mjs` and
|
||||||
`src/layers/perimeters/cards.test.mjs`.
|
`src/layers/perimeters/cards.test.mjs`.
|
||||||
|
|
||||||
|
## Scope controls and HUD text readability
|
||||||
|
|
||||||
|
Display uses a native, visibly checked Scope checkbox. Turning it off disables
|
||||||
|
its feather slider while retaining the value. Scope enabled state and feather
|
||||||
|
are independent of Tactical, Operator, Minimal and Cyber layout changes. Share
|
||||||
|
links (`sc`/`scf`) and scene visual state restore both choices, including feather
|
||||||
|
above 100%. The supported feather range is 0–200% of the keyhole radius: 0% is a
|
||||||
|
hard edge and 200% extends the gradient to the center while keeping its midpoint
|
||||||
|
on the same keyhole edge. First run opens with Scope ON and feather 100%; links
|
||||||
|
predating `scf` retain their historical 35% parse fallback.
|
||||||
|
|
||||||
|
When Scope is OFF, Tactical, Operator and Minimal add small black backplates
|
||||||
|
at 70% opacity behind telemetry, vertical metadata, multiline AIS and scene
|
||||||
|
chrome. They have rounded corners, no halo and no overlap between adjacent lines.
|
||||||
|
Text keeps its original color, glow and opacity in both Scope states. The engine
|
||||||
|
publishes the OFF state as `data-scope-mask="off"` on the document root, so direct
|
||||||
|
setters, share links and scene restoration drive the same treatment. Scope ON
|
||||||
|
uses the original HUD layout, Operator frames and scene chrome, with no added
|
||||||
|
backplates. The OFF-only Minimal coordinate offset clears map attribution.
|
||||||
|
Cyber retains its existing skin.
|
||||||
|
|
||||||
|
`node scripts/qa-scope-hud.mjs` exercises native checkbox click and Space,
|
||||||
|
feather range and keyboard boundaries, all four layout transitions, share/scene
|
||||||
|
restore, original Scope ON styling, OFF-only backing coverage and non-overlapping
|
||||||
|
geometry, multiline AIS, summary clipping and narrow viewport containment. Optional `QA_SCREENSHOT_DIR` saves mode screenshots.
|
||||||
|
|
||||||
## Cyber HUD — September 30, 2026
|
## Cyber HUD — September 30, 2026
|
||||||
|
|
||||||
Display > HUD > Layout includes Cyber, also available through the HUD voice
|
Display > HUD > Layout includes Cyber, also available through the HUD voice
|
||||||
@@ -251,6 +321,7 @@ catalog construction owns each instance.
|
|||||||
|
|
||||||
Wind animates one forecast without advancing forecast time. Separate Weather
|
Wind animates one forecast without advancing forecast time. Separate Weather
|
||||||
observation layers provide radar and satellite history; none claims measured cloud volume.
|
observation layers provide radar and satellite history; none claims measured cloud volume.
|
||||||
|
Weather imagery releases its camera move listener on disable and registers it once on re-enable. Disabling also clears active imagery work.
|
||||||
Weather imagery drapes onto the globe basemap. On photorealistic 3D Tiles each
|
Weather imagery drapes onto the globe basemap. On photorealistic 3D Tiles each
|
||||||
observed product is instead a raised, translucent shell: one rectangle over the
|
observed product is instead a raised, translucent shell: one rectangle over the
|
||||||
product bounds at a fixed height (global infrared 5.5 km, regional infrared
|
product bounds at a fixed height (global infrared 5.5 km, regional infrared
|
||||||
@@ -376,6 +447,9 @@ Cyclones combine fixed NHC/CPHC status and summary GIS endpoints through a bound
|
|||||||
same-origin provider. The status endpoint lacks browser CORS; each installation
|
same-origin provider. The status endpoint lacks browser CORS; each installation
|
||||||
fetches keyless public sources at runtime, with no centralized ingestion service.
|
fetches keyless public sources at runtime, with no centralized ingestion service.
|
||||||
The five-minute snapshot covers Atlantic and eastern/central North Pacific only.
|
The five-minute snapshot covers Atlantic and eastern/central North Pacific only.
|
||||||
|
Classification labels follow NHC status codes: `PC` is a potential tropical
|
||||||
|
cyclone, `PTC` a post-tropical cyclone, and `STS`/`STD` a subtropical
|
||||||
|
storm/depression. Unknown codes remain visible unchanged.
|
||||||
Current positions, advisory issue time and position time stay distinct. NHC may
|
Current positions, advisory issue time and position time stay distinct. NHC may
|
||||||
release an advisory before its nominal time (routinely up to about 30 minutes,
|
release an advisory before its nominal time (routinely up to about 30 minutes,
|
||||||
rarely about an hour), so advisory issue and position times up to and including
|
rarely about an hour), so advisory issue and position times up to and including
|
||||||
@@ -388,6 +462,8 @@ than relabeling older geometry. The cone represents forecast center uncertainty,
|
|||||||
not storm size or the full hazard area. Successful empty and unavailable states
|
not storm size or the full hazard area. Successful empty and unavailable states
|
||||||
are distinct. One owned native Cesium data source preserves polygon holes and
|
are distinct. One owned native Cesium data source preserves polygon holes and
|
||||||
geographic seams and releases on disable; forecast animation is not implied.
|
geographic seams and releases on disable; forecast animation is not implied.
|
||||||
|
NHC movement speed is converted from miles per hour to rounded knots by the
|
||||||
|
proxy; advisory wind intensity is already in knots and is passed through.
|
||||||
Cyclone markers, labels, tracks and cones are hidden beyond the horizon on every map source.
|
Cyclone markers, labels, tracks and cones are hidden beyond the horizon on every map source.
|
||||||
Consult the linked official advisory for safety decisions.
|
Consult the linked official advisory for safety decisions.
|
||||||
|
|
||||||
@@ -424,6 +500,14 @@ than a reading. Heading keeps its existing separate check for its own sentinel.
|
|||||||
Genuine readings are unchanged, including a stopped vessel's zero and the
|
Genuine readings are unchanged, including a stopped vessel's zero and the
|
||||||
highest speed and course the fields can encode.
|
highest speed and course the fields can encode.
|
||||||
|
|
||||||
|
For an already tracked vessel, AISStream static messages update name, type,
|
||||||
|
destination and IMO without changing its position, motion, fix timestamps,
|
||||||
|
position freshness or trail. Non-empty static values replace earlier values;
|
||||||
|
omitted, empty or unavailable fields retain known metadata across Class B's
|
||||||
|
separate reports and in the cache used by the next position update. Position
|
||||||
|
reports still prefer their own metadata name. Invalid Class B parts are ignored;
|
||||||
|
absent fields prefer the latest live value over the static cache.
|
||||||
|
|
||||||
Native `<select>` controls declare a dark colour scheme and explicit option
|
Native `<select>` controls declare a dark colour scheme and explicit option
|
||||||
colours, so the browser-painted option list matches the panel it drops out of.
|
colours, so the browser-painted option list matches the panel it drops out of.
|
||||||
The scheme is declared on the controls rather than on the document root, so
|
The scheme is declared on the controls rather than on the document root, so
|
||||||
@@ -496,6 +580,9 @@ to layer toggles, fly_to_location, frame_overhead, select_nearest_aircraft,
|
|||||||
get_entity_context, annotate_map and analyst_query. Analyst speech and the card
|
get_entity_context, annotate_map and analyst_query. Analyst speech and the card
|
||||||
share one deterministic count/scope headline; lower bounds use "At least" and
|
share one deterministic count/scope headline; lower bounds use "At least" and
|
||||||
partial answers name the unanswered layers. `get_current_view_state` lists enabled layers only.
|
partial answers name the unanswered layers. `get_current_view_state` lists enabled layers only.
|
||||||
|
FIRMS and ALPR entity labels remain English in shared context and tool data;
|
||||||
|
voice-card titles and pointer presentation translate those known labels at display
|
||||||
|
time, while arbitrary names and callsigns remain unchanged.
|
||||||
The model-free `gods-eye-view/agent/world-packet` export exposes
|
The model-free `gods-eye-view/agent/world-packet` export exposes
|
||||||
`getWorldPacket({ viewer, styleManager, dataManager, sceneDirector })` to
|
`getWorldPacket({ viewer, styleManager, dataManager, sceneDirector })` to
|
||||||
non-voice consumers. It reads the same enabled-only state and feed
|
non-voice consumers. It reads the same enabled-only state and feed
|
||||||
@@ -624,6 +711,14 @@ The standalone app offers OpenAI and Gemini Live through an explicit provider ch
|
|||||||
|
|
||||||
Voice action argument schemas have one portable owner under `src/voice/actionSchemas.js`. The Realtime provider builds 30 tools using separate description-only metadata. Description customization cannot replace argument types, enum values or required fields.
|
Voice action argument schemas have one portable owner under `src/voice/actionSchemas.js`. The Realtime provider builds 30 tools using separate description-only metadata. Description customization cannot replace argument types, enum values or required fields.
|
||||||
|
|
||||||
|
`createGevActionRunner` routes `control_cctv` enable/disable through the shared
|
||||||
|
layer visibility lifecycle. Results distinguish successful changes from
|
||||||
|
manager-reported failures or cancellation and report enabled state and lifecycle
|
||||||
|
uncertainty. A caller-supplied abort signal reaches the manager; a late abort
|
||||||
|
preserves an already committed change. Missing-layer errors and other CCTV
|
||||||
|
actions retain their existing behavior. The exported `controlCctv` helper refuses direct
|
||||||
|
enable/disable requests and directs callers to `set_layer_visibility`.
|
||||||
|
|
||||||
`src/voice/layerManifest.js` declares each catalog layer's voice capabilities: spoken aliases, `toggle`, in-view `context`, `track`, and analyst `query` fields with type and unit, or `noQuery` with the reason. The `set_layer_visibility`, `show_data_layers_menu`, `get_entity_context` and `analyst_query` layer enums, the runner's alias table and the analyst field hints are generated from it. `src/voice/layerManifest.test.mjs` fails when a catalog layer is neither listed nor named in `VOICE_OFF_LAYERS` (today only the two scene-driven Bhote Koshi layers). Global Context is listed with `toggle: false`; `set_context_mode` reaches it. `set_panel_open` also opens `weather-panel` and `recent-imagery-panel`.
|
`src/voice/layerManifest.js` declares each catalog layer's voice capabilities: spoken aliases, `toggle`, in-view `context`, `track`, and analyst `query` fields with type and unit, or `noQuery` with the reason. The `set_layer_visibility`, `show_data_layers_menu`, `get_entity_context` and `analyst_query` layer enums, the runner's alias table and the analyst field hints are generated from it. `src/voice/layerManifest.test.mjs` fails when a catalog layer is neither listed nor named in `VOICE_OFF_LAYERS` (today only the two scene-driven Bhote Koshi layers). Global Context is listed with `toggle: false`; `set_context_mode` reaches it. `set_panel_open` also opens `weather-panel` and `recent-imagery-panel`.
|
||||||
|
|
||||||
Portable source exports provide Radio Browser station normalization, CCTV feed types and regional records independently of HTTP middleware. Radio normalization accepts an explicit URL policy; the standalone directory retains its existing HTTPS rules. Tile coordinate validation accepts explicit zoom bounds with unchanged traffic defaults. Cache, request and rendering owners remain unchanged.
|
Portable source exports provide Radio Browser station normalization, CCTV feed types and regional records independently of HTTP middleware. Radio normalization accepts an explicit URL policy; the standalone directory retains its existing HTTPS rules. Tile coordinate validation accepts explicit zoom bounds with unchanged traffic defaults. Cache, request and rendering owners remain unchanged.
|
||||||
@@ -707,6 +802,12 @@ transports and cancellation through response parsing. Stop and application
|
|||||||
teardown abort pending connections; reconnect requests a new client secret.
|
teardown abort pending connections; reconnect requests a new client secret.
|
||||||
Microphone, radio handoff, tool schemas and default model behavior are preserved.
|
Microphone, radio handoff, tool schemas and default model behavior are preserved.
|
||||||
|
|
||||||
|
Coordinate search accepts whitespace-separated decimal degrees with prefix or
|
||||||
|
suffix hemisphere letters, including `40.7128 N 74.0060 W`. Components require
|
||||||
|
an actual separator and an unambiguous hemisphere assignment; malformed numeric
|
||||||
|
fragments and scientific notation fall through to geocoders instead of becoming
|
||||||
|
exact coordinate results.
|
||||||
|
|
||||||
Geospatial lookups are composed through `src/search`: forward/reverse geocoding,
|
Geospatial lookups are composed through `src/search`: forward/reverse geocoding,
|
||||||
text/nearby search and routing use configurable providers/endpoints. Annotation,
|
text/nearby search and routing use configurable providers/endpoints. Annotation,
|
||||||
HUD and voice consumers share the configured service. Existing Google/Photon
|
HUD and voice consumers share the configured service. Existing Google/Photon
|
||||||
@@ -795,6 +896,15 @@ standalone entry supplies application-owned scene, ground and activation service
|
|||||||
Each layer owns its state and visibility listener; destruction cancels source
|
Each layer owns its state and visibility listener; destruction cancels source
|
||||||
reads and pending initialization. Malformed health responses retain prior health.
|
reads and pending initialization. Malformed health responses retain prior health.
|
||||||
Existing catalog fallback, camera poses, frame pacing and coverage controls remain.
|
Existing catalog fallback, camera poses, frame pacing and coverage controls remain.
|
||||||
|
CCTV frames and the Street View fallback accept only JPEG, PNG, GIF, WebP and
|
||||||
|
AVIF, sent under the canonical type; any other type falls through to the next
|
||||||
|
fallback. Live media must match its feed kind (`video/mp4`, `video/webm`, or
|
||||||
|
`multipart/x-mixed-replace` with a boundary) before health reports it connected,
|
||||||
|
and follows redirects only on the camera's origin. Catalog image URLs, and
|
||||||
|
redirects on those hosts, are pinned by parsed URL to the agency's image
|
||||||
|
directory. Every `/api/cctv` route refuses cross-site browser requests but
|
||||||
|
accepts forwarding headers, so cameras work behind a reverse proxy; a forwarded
|
||||||
|
request skips the key-bearing Street View fallback and gets the placeholder.
|
||||||
|
|
||||||
Traffic owns its rendered-surface observer only while enabled, removes it on disable/destroy, and caches surface identity without allocating on unchanged frames.
|
Traffic owns its rendered-surface observer only while enabled, removes it on disable/destroy, and caches surface identity without allocating on unchanged frames.
|
||||||
|
|
||||||
@@ -817,7 +927,11 @@ Viewport limits, saturation retry, placement, query caps and controls are retain
|
|||||||
|
|
||||||
Satellite and mission layers expose separate factories through `./layers/satellites`
|
Satellite and mission layers expose separate factories through `./layers/satellites`
|
||||||
and `./layers/launches`. Each owns its catalog, render state, tracking, interaction
|
and `./layers/launches`. Each owns its catalog, render state, tracking, interaction
|
||||||
and teardown. Source adapters retain the existing CelesTrak and Launch Library
|
and teardown. Satellite core and dense catalogs retain upstream stale-cache
|
||||||
|
provenance only for admitted records; duplicate or invalid entries cannot taint
|
||||||
|
a fresh winning record. Dense removal clears only its own provenance, and a
|
||||||
|
fresh core rebuild or lifecycle reset replaces the corresponding ownership.
|
||||||
|
Source adapters retain the existing CelesTrak and Launch Library
|
||||||
endpoints; standalone entries provide scene services and the satellite dependency.
|
endpoints; standalone entries provide scene services and the satellite dependency.
|
||||||
The mission factory separates paths, replay, camera, panel, overlays and placement.
|
The mission factory separates paths, replay, camera, panel, overlays and placement.
|
||||||
Launch Library pad coordinates retain unknown values: missing fields use the
|
Launch Library pad coordinates retain unknown values: missing fields use the
|
||||||
@@ -1092,6 +1206,26 @@ The images are periodically updated road-condition photographs, not continuous
|
|||||||
video; camera poses use the existing low-confidence heading prior.
|
video; camera poses use the existing low-confidence heading prior.
|
||||||
`CCTV_LITHUANIA_MAX_SOURCES` sets its cap (default 400, bounded to 1–1000).
|
`CCTV_LITHUANIA_MAX_SOURCES` sets its cap (default 400, bounded to 1–1000).
|
||||||
|
|
||||||
|
## Taiwan CCTV source pack
|
||||||
|
|
||||||
|
The CCTV catalog includes Taiwan national freeway cameras from the Freeway
|
||||||
|
Bureau, MOTC. One keyless request reads the daily MOTC 2.0 `CCTV.xml` table
|
||||||
|
(~1,800 cameras); the request refuses redirects, has a 15-second deadline and a
|
||||||
|
6 MiB response cap, and uses the shared 15-minute cache and fair catalog
|
||||||
|
allocation. A failed download serves the last good table (up to a day old),
|
||||||
|
or an empty pack, and is not retried for an hour: the catalog host does not
|
||||||
|
answer from some networks outside Taiwan, and without the backoff every refresh
|
||||||
|
there would wait out the connect timeout. The table
|
||||||
|
publishes MJPEG stream URLs; each is mapped to the same camera server's
|
||||||
|
single-JPEG path for the same camera number, and only those exact HTTPS pairs
|
||||||
|
on the four `freeway.gov.tw` camera servers are registered with the frame
|
||||||
|
proxy. Camera keys come from the server and camera number, because published
|
||||||
|
CCTV IDs contain Chinese place names and a few repeat across cameras. Poses use
|
||||||
|
the low-confidence heading prior. `CCTV_TAIWAN_ENABLED=0` disables the pack;
|
||||||
|
`CCTV_TAIWAN_MAX_SOURCES` sets its cap (default 600, bounded to 1–2000), and
|
||||||
|
`CCTV_TAIWAN_URL` overrides the table URL.
|
||||||
|
Gate: `src/data/cctvTaiwan.test.mjs`.
|
||||||
|
|
||||||
## DriveBC CCTV source pack
|
## DriveBC CCTV source pack
|
||||||
|
|
||||||
The CCTV catalog adds DriveBC highway cameras for British Columbia alongside the
|
The CCTV catalog adds DriveBC highway cameras for British Columbia alongside the
|
||||||
@@ -1138,6 +1272,24 @@ snapshots, lifecycle actions and row descriptors. Remount and teardown remove
|
|||||||
listeners and row subscriptions; obsolete completions do not repaint old rows.
|
listeners and row subscriptions; obsolete completions do not repaint old rows.
|
||||||
The clear control presents busy state while its existing action owns the transaction.
|
The clear control presents busy state while its existing action owns the transaction.
|
||||||
|
|
||||||
|
## Sky background
|
||||||
|
|
||||||
|
`src/sky/starrySky.js` fills Cesium's `Scene#skyBox` slot with 9,096 Yale
|
||||||
|
Bright Star Catalogue stars and a soft ESO/S. Brunier Milky Way. Both layers
|
||||||
|
rotate from ICRF/J2000 into the Earth-fixed frame at the scene clock, with
|
||||||
|
a TEME fallback while Cesium's IAU 2006 tables load. Bundled assets and
|
||||||
|
their provenance are documented in `src/data/local_data/sky/README.md`.
|
||||||
|
|
||||||
|
Anime fades out the Milky Way with its style intensity to avoid cel-shaded
|
||||||
|
bands; the catalogue stars remain. A separate black 1×1 cube map preserves
|
||||||
|
model environment lighting independently of the visible sky. The adapter
|
||||||
|
is validated against Cesium 1.138.0 and must be revalidated on upgrades.
|
||||||
|
|
||||||
|
`scripts/qa-anime-sky.mjs` checks asset readiness, visible Normal sky and
|
||||||
|
Anime band coverage against a Vite dev server.
|
||||||
|
`scripts/qa-sky-model-lighting.mjs` compares a lit `airplane.glb` against
|
||||||
|
black and white environment maps in isolated scenes.
|
||||||
|
|
||||||
## Model atmosphere on Apple Metal
|
## Model atmosphere on Apple Metal
|
||||||
|
|
||||||
Cesium's per-vertex model atmosphere is kept out of the pipeline on devices
|
Cesium's per-vertex model atmosphere is kept out of the pipeline on devices
|
||||||
@@ -1183,6 +1335,84 @@ The application supplies actions and retains effect settings, restore claims and
|
|||||||
rendering. Native keyboard editing, model modes and current defaults are preserved.
|
rendering. Native keyboard editing, model modes and current defaults are preserved.
|
||||||
Destroying the UI removes these listeners before asynchronous teardown.
|
Destroying the UI removes these listeners before asynchronous teardown.
|
||||||
|
|
||||||
|
Ordinary sliders share the native range component in `ui/slider.js` and the
|
||||||
|
`gev-quantitative-slider` surface in `ui/styles/slider.css`. Display, shader
|
||||||
|
parameters, volume, replay speed, weather settings/history, imagery opacity and
|
||||||
|
Street View's date filter use the same rail, thumb, value styling, keyboard focus and disabled state.
|
||||||
|
Generated controls use `createSlider`; static templates apply the same class.
|
||||||
|
Feature owners retain bounds, steps, formatted values, input/change timing and
|
||||||
|
listener cleanup. The radio tuning dial, imagery comparison divider and event
|
||||||
|
comparison/progression controls keep their specialized presentation and behavior.
|
||||||
|
|
||||||
|
## Shared control CSS ownership
|
||||||
|
|
||||||
|
Shared styles own ordinary control surfaces and interaction states; feature CSS
|
||||||
|
owns surrounding layout and context-specific sizing. CCTV's calibration editor
|
||||||
|
width lives in `ui/styles/cctv.css`, while the shared inline field variant owns
|
||||||
|
only density and spinner treatment. Radio supplies its field palette and disabled
|
||||||
|
opacity through the `--gev-field-*` variables, retaining compact typography and
|
||||||
|
arrow space locally. Cyber supplies shared-field palette variables explicitly;
|
||||||
|
its legacy native-field palette rules exclude `.gev-field`. These ownership
|
||||||
|
boundaries preserve the current Tactical/Cyber appearance and native behavior.
|
||||||
|
|
||||||
|
## Ordinary status badges
|
||||||
|
|
||||||
|
CCTV source/frame labels, calibration provenance and Weather coverage labels use
|
||||||
|
`ui/statusBadge.js` and the `gev-status-badge` CSS surface. The listener-free
|
||||||
|
adapter accepts already-translated `label` and `hidden` values; pill, box and plain
|
||||||
|
appearances describe shape/density only. Static CCTV markup uses the same classes
|
||||||
|
as generated rail-card badges. Repeated identical updates do not rewrite text or
|
||||||
|
replace nodes. Empty coverage remains hidden.
|
||||||
|
|
||||||
|
Feature owners retain the status rules, machine-state attributes, translations
|
||||||
|
and announcement policy. CCTV frame acquisition (`idle/loading/error/ready`)
|
||||||
|
remains separate from provider status: retained pixels do not imply a healthy
|
||||||
|
feed. Calibration still distinguishes manually saved, catalog-curated and raw
|
||||||
|
prior poses; unsaved edits override the label until save/reset. Coverage labels
|
||||||
|
remain geographic scope, not health claims. Existing colors and the source
|
||||||
|
badge's polite live region are preserved; coverage and calibration do not gain
|
||||||
|
live regions. Interactive layer toggles, progress indicators and world overlays
|
||||||
|
retain their specialized ownership. Vue can reuse the view contract and CSS while
|
||||||
|
replacing DOM construction. Browser acceptance:
|
||||||
|
`QA_BASE_URL=http://127.0.0.1:4189 node scripts/qa-status-badges.mjs`.
|
||||||
|
|
||||||
|
## Ordinary action buttons
|
||||||
|
|
||||||
|
Scene panel actions, shot LOAD/DEL actions, the EDIT DETAILS / SHARE SCENE
|
||||||
|
bar and one-shot CCTV actions use `ui/actionButton.js` and the shared
|
||||||
|
`gev-action-button` surface in `ui/styles/action-button.css`. Static templates
|
||||||
|
apply the same classes as generated buttons. The native builder accepts translated
|
||||||
|
text, default/danger variants, default/compact sizes and disabled/accessibility
|
||||||
|
properties; it installs no listeners and owns no application state. Feature
|
||||||
|
controllers retain callbacks, enable/disable rules, confirmation prompts,
|
||||||
|
persistence and teardown. The smaller shot actions retain their existing sizing
|
||||||
|
and hover treatment. Feature CSS still owns panel layout and translated wrapping.
|
||||||
|
|
||||||
|
CCTV enable, coverage, auto-hop, projection and adjustment toggles, editable
|
||||||
|
calibration-value chips and the scene authoring dialogs retain their specialized
|
||||||
|
controls. Existing `scene-btn` hooks remain compatible with Street Level and
|
||||||
|
legacy toggles. The shared CSS, explicit input/action boundary and behavior tests
|
||||||
|
can carry to a Vue view while Vue replaces native DOM construction.
|
||||||
|
Browser acceptance: `QA_BASE_URL=http://127.0.0.1:4179 node scripts/qa-action-buttons.mjs`;
|
||||||
|
`qa-scene-controls.mjs` also covers real Scene authoring/playback with native Space capture.
|
||||||
|
|
||||||
|
## Segmented selectors
|
||||||
|
|
||||||
|
Segmented selectors use the controlled DOM adapter in `ui/segmentedControl.js`
|
||||||
|
and the shared `gev-segmented` / `gev-segment` surface. Model coverage, label
|
||||||
|
allocation, drawing shape, Street View fit/panorama filters and imagery mode keep
|
||||||
|
their feature-owned values, actions and persistence. Painting a restored value
|
||||||
|
does not emit a change or move focus. Existing keyboard policies remain explicit:
|
||||||
|
Street View uses arrows/Home/End with one tab stop, imagery mode uses arrows from
|
||||||
|
its controlled selection, and Display keeps its existing native button behavior.
|
||||||
|
Pure value navigation is separate from DOM rendering for reuse by a future view.
|
||||||
|
Segments keep their word minimum width: compact inline groups such as FIT/FILL
|
||||||
|
and ALL/360°/FLAT must not break short labels mid-word. Collapsed Weather follows
|
||||||
|
the same exclusive-rail hiding rule as the other readouts.
|
||||||
|
The imagery mode row reserves 26px for the 24px buttons and group borders so
|
||||||
|
the complete button and inset keyboard-focus outline remain visible.
|
||||||
|
Browser acceptance: `QA_BASE_URL=http://127.0.0.1:4173 node scripts/qa-segmented-controls.mjs`.
|
||||||
|
|
||||||
## Application shortcuts and shader parameter controls
|
## Application shortcuts and shader parameter controls
|
||||||
|
|
||||||
`ui/input` supplies the bubbling application shortcut listener and generated
|
`ui/input` supplies the bubbling application shortcut listener and generated
|
||||||
@@ -1217,6 +1447,47 @@ The right rail skips panels hidden by other CSS (computed `display: none`), but
|
|||||||
keeps the collapsed panels its own exclusive presentation hides, so automatic
|
keeps the collapsed panels its own exclusive presentation hides, so automatic
|
||||||
collapse can still be undone.
|
collapse can still be undone.
|
||||||
|
|
||||||
|
## Phone layout
|
||||||
|
|
||||||
|
At 720px and narrower, `src/ui/mobileShell.js` keeps the globe full screen and
|
||||||
|
shows a bottom tab bar: Layers (Data Layers, with Scenes collapsed below),
|
||||||
|
Cameras (CCTV and Street Level), Context (Global Context, plus Weather and
|
||||||
|
Recent Imagery when shown), Search (Location) and More (Visual Presets and Map
|
||||||
|
Source above Display). A tab opens one bottom sheet; the same tab, the backdrop
|
||||||
|
or Escape closes it, and focus moves into the sheet and back to its tab. A
|
||||||
|
downward swipe of at least 64px on non-control sheet content also closes it,
|
||||||
|
when every scroll container under the finger starts at the top. Ordinary
|
||||||
|
vertical scrolling, horizontal gestures, controls and multitouch stay native;
|
||||||
|
a cancelled gesture or a sheet/viewport change cannot dismiss another sheet. Panels
|
||||||
|
are not moved in the DOM: `styles/mobile-shell.css` presents the rails and dock
|
||||||
|
as the sheet, and the dock's Location and Presets trays render inline. The voice
|
||||||
|
control floats above the tab bar while no sheet is open.
|
||||||
|
|
||||||
|
The phone logo is 58px wide (about 39px high), aligned with the top controls.
|
||||||
|
The shared contact picker preserves exact picks. For a coarse primary pointer
|
||||||
|
that misses onto the map surface, it searches at most eight hits in a 14px square
|
||||||
|
and accepts the first registered layer hit, stopping at non-map objects. Civil
|
||||||
|
and military aircraft and vessels share this rule; fine pointers stay exact.
|
||||||
|
|
||||||
|
Manually opening Context with a tracked civilian or military aircraft enters
|
||||||
|
the existing Contacts mode when Context is off. Contacts adopts that aircraft
|
||||||
|
without restarting its tracking; an active or entering Space Missions mode is
|
||||||
|
left alone. Programmatic sheet changes and closing the tab do not activate a mode.
|
||||||
|
|
||||||
|
Sheets borrow panels. Opening one expands its panels without persisting them or
|
||||||
|
claiming a user choice, and share links keep reporting each borrowed panel's
|
||||||
|
state from before the sheet; closing puts back what opening changed, while a
|
||||||
|
panel the user changed inside the sheet stays as they left it. Escape closes the
|
||||||
|
sheet before a panel's own Escape handling, and the dock's hover auto-close does
|
||||||
|
not apply inside a sheet.
|
||||||
|
Cockpit, recording and clean view close the sheet and hide the tab bar; scene
|
||||||
|
playback keeps both so its STOP control in the Scenes panel stays reachable. The
|
||||||
|
right rail neither hides collapsed siblings nor applies Cyber's one-panel
|
||||||
|
accordion at these widths, since the sheet chooses what shows. The Cesium and Google attribution stays visible above the
|
||||||
|
voice control, and above an open sheet. The page draws under the notch and
|
||||||
|
system bars (`viewport-fit=cover`) and the shell's chrome uses safe-area insets.
|
||||||
|
Wider viewports are unchanged, and crossing to them closes any open sheet.
|
||||||
|
|
||||||
## Surface keyboard lifecycle
|
## Surface keyboard lifecycle
|
||||||
|
|
||||||
Surface Tab cycling excludes controls with a negative tabindex, native disabled
|
Surface Tab cycling excludes controls with a negative tabindex, native disabled
|
||||||
@@ -1456,6 +1727,14 @@ The TomTom `/api/tomtom/*` routes run the shared same-site admission gate before
|
|||||||
status, cache or upstream work, refusing cross-site browser requests while
|
status, cache or upstream work, refusing cross-site browser requests while
|
||||||
preserving same-origin app requests and non-browser local callers. The browser
|
preserving same-origin app requests and non-browser local callers. The browser
|
||||||
layers and their rendering remain in their existing modules.
|
layers and their rendering remain in their existing modules.
|
||||||
|
Past the daily tile budget, or while TomTom fails, the proxy answers with its
|
||||||
|
last copy of a tile (`x-tomtom-cache: STALE-BUDGET` / `STALE-ERROR`) however old
|
||||||
|
it is, and every tile carries `x-tomtom-fetched-at`, the proxy fetch time
|
||||||
|
(or the persisted cache file time after restart). The flow source keeps that mark with each decoded tile, so
|
||||||
|
`get_traffic_flow` returns `stale` and `oldest_tile_at` and says when traffic
|
||||||
|
may be out of date instead of presenting cached tiles as live. Pending freshness
|
||||||
|
metadata is released when body reads fail, are cancelled, or exceed their size
|
||||||
|
limit, as well as when a tile decodes or the source resets.
|
||||||
GBFS null or empty availability counts retain an unknown value through station
|
GBFS null or empty availability counts retain an unknown value through station
|
||||||
parsing and rendering; they use the neutral station color rather than reporting
|
parsing and rendering; they use the neutral station color rather than reporting
|
||||||
an empty dock. Explicit zero and caller-supplied numeric fallbacks remain valid.
|
an empty dock. Explicit zero and caller-supplied numeric fallbacks remain valid.
|
||||||
@@ -1703,6 +1982,10 @@ Updated: September 23, 2026
|
|||||||
|
|
||||||
## Aircraft and vessel server modules
|
## Aircraft and vessel server modules
|
||||||
|
|
||||||
|
OpenSky credential-file failures emit a fixed warning once per process; neither
|
||||||
|
JSON parser excerpts nor filesystem error messages are logged. The environment
|
||||||
|
example uses an absolute path compatible with the macOS launchers.
|
||||||
|
|
||||||
Standalone aircraft routes now live in `server/providers/aircraft/`: OpenSky
|
Standalone aircraft routes now live in `server/providers/aircraft/`: OpenSky
|
||||||
state vectors and regional fallback, adsb.lol military positions, ADSBDB
|
state vectors and regional fallback, adsb.lol military positions, ADSBDB
|
||||||
enrichment and track backfill. Vessel routes and websocket/watchdog setup live
|
enrichment and track backfill. Vessel routes and websocket/watchdog setup live
|
||||||
@@ -1727,6 +2010,9 @@ accessible name remains "Don't show this again". Control behavior is unchanged.
|
|||||||
## FIRMS source status
|
## FIRMS source status
|
||||||
|
|
||||||
The FIRMS CSV parser rejects impossible acquisition calendar dates before the proxy filters records to the trailing 24 hours, while retaining valid UTC acquisition times.
|
The FIRMS CSV parser rejects impossible acquisition calendar dates before the proxy filters records to the trailing 24 hours, while retaining valid UTC acquisition times.
|
||||||
|
The `get_active_fires` tool reports acquisition times as UTC ISO timestamps,
|
||||||
|
including zero-padded hours and minutes; missing or invalid dates and times
|
||||||
|
remain `null`, including impossible calendar dates from a custom source.
|
||||||
|
|
||||||
The FIRMS proxy records source success after appending its rows. If aggregation
|
The FIRMS proxy records source success after appending its rows. If aggregation
|
||||||
throws, that source reports failure without a contradictory success entry.
|
throws, that source reports failure without a contradictory success entry.
|
||||||
@@ -1830,10 +2116,18 @@ fetching, trailing-24-hour filtering and partial-success caching are unchanged.
|
|||||||
> `localStorage['gev:first-run-mission:v1'] === 'suppressed'`, written **only**
|
> `localStorage['gev:first-run-mission:v1'] === 'suppressed'`, written **only**
|
||||||
> by the "Don't show this again" checkbox → the per-session
|
> by the "Don't show this again" checkbox → the per-session
|
||||||
> `sessionStorage['gev:first-run-mission-session:v1'] === 'dismissed'`, written
|
> `sessionStorage['gev:first-run-mission-session:v1'] === 'dismissed'`, written
|
||||||
> by **every** close path (mission, Explore, ESC). So it returns each fresh
|
> by **every** close path (mission, Explore, ESC, the footer dismiss button). So
|
||||||
> browser session until the visitor ticks the box; clearing storage un-ticks it,
|
> it returns each fresh browser session until the visitor ticks the box; clearing
|
||||||
> which is accepted. Both stores fail open — an unreadable store still shows the
|
> storage un-ticks it, which is accepted. Both stores fail open — an unreadable
|
||||||
> launcher rather than silently swallowing first launch.
|
> store still shows the launcher rather than silently swallowing first launch.
|
||||||
|
>
|
||||||
|
> **The footer dismiss button is the keyboardless ESC.** Phones and tablets have
|
||||||
|
> no ESC key, so the footer hint is a `data-first-run-dismiss` button that calls
|
||||||
|
> the same `dismiss()` as ESC. It stays visible at every width (phones used to
|
||||||
|
> hide it, leaving a mission as the only way out) and reads DISMISS on a
|
||||||
|
> `(hover: none) and (pointer: coarse)` screen, "ESC to dismiss" otherwise. Its
|
||||||
|
> 44px touch target is a `::before` outside the layout, so the footer row keeps
|
||||||
|
> its one-line height.
|
||||||
>
|
>
|
||||||
> **What a mission may persist (do not "simplify" this).** Layer enablement is
|
> **What a mission may persist (do not "simplify" this).** Layer enablement is
|
||||||
> durable in this app (`gev:layer-state:v2`, written by
|
> durable in this app (`gev:layer-state:v2`, written by
|
||||||
@@ -1894,6 +2188,16 @@ fetching, trailing-24-hour filtering and partial-success caching are unchanged.
|
|||||||
> always exits non-zero, `1` meaning the control is healthy and `2` meaning it
|
> always exits non-zero, `1` meaning the control is healthy and `2` meaning it
|
||||||
> is not. Plus the unit pins above.
|
> is not. Plus the unit pins above.
|
||||||
|
|
||||||
|
> **2026-10-24 — a turned-off layer's memory is released:** Cesium 1.138
|
||||||
|
> keeps the draw commands past each frame's count in the opaque and other
|
||||||
|
> per-pass arrays (`View.frustumCommandsList`) and in the offscreen pick view,
|
||||||
|
> so a turned-off layer's entities stayed reachable (Data Centers kept 116
|
||||||
|
> MiB). `src/sceneCommandRelease.js`, installed in `src/app/tools.js`,
|
||||||
|
> requests one frame through `governorRequestRender` after every layer
|
||||||
|
> disable and then trims those arrays. It reads private Cesium fields;
|
||||||
|
> `src/sceneCommandRelease.test.mjs` pins them and fails if Cesium renames
|
||||||
|
> them or changes which passes it truncates.
|
||||||
|
|
||||||
> **2026-08-08 — performance waves 1+2:** the app idles via an explicit render
|
> **2026-08-08 — performance waves 1+2:** the app idles via an explicit render
|
||||||
> governor (`src/renderGovernor.js` — hold/release from every per-frame
|
> governor (`src/renderGovernor.js` — hold/release from every per-frame
|
||||||
> animator; discrete mutators call `governorRequestRender`). Any NEW
|
> animator; discrete mutators call `governorRequestRender`). Any NEW
|
||||||
@@ -2732,11 +3036,16 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with
|
|||||||
> first later successful fetch. Smaller chunks reduce timeout risk; complete
|
> first later successful fetch. Smaller chunks reduce timeout risk; complete
|
||||||
> camera batches still wait for their sequential requests, and unresolved
|
> camera batches still wait for their sequential requests, and unresolved
|
||||||
> placement continues to use the existing prior until real heights arrive.
|
> placement continues to use the existing prior until real heights arrive.
|
||||||
> - **Overpass cache admission:** `/api/overpass` parses and sanitizes requests,
|
> - **Overpass cache admission:** `/api/overpass` refuses cross-site and forwarded
|
||||||
> then checks fresh memory, identical in-flight work, and fresh disk entries
|
> POSTs through the shared local-request gate, then limits requests to 300/min
|
||||||
> before invoking its local 90/min limiter. Cache and single-flight responses
|
> per socket peer and 900/min globally before reading or validating bodies.
|
||||||
> therefore do not spend quota; upstream-bound misses retain the existing
|
> Tag-filter brackets are scanned once; nested or unmatched brackets return 400.
|
||||||
> limiter, configured-upstream, stale, and sanitization behavior. An unconfigured transport serves dated stale data or a non-retryable capability error.
|
> It then checks fresh memory, identical in-flight work, and fresh disk entries
|
||||||
|
> before invoking its separate local 90/min upstream limiter. Cache and
|
||||||
|
> single-flight responses therefore do not spend upstream quota; upstream-bound
|
||||||
|
> misses retain the existing limiter, configured-upstream, stale, and sanitization
|
||||||
|
> behavior. An unconfigured transport serves dated stale data or a non-retryable
|
||||||
|
> capability error.
|
||||||
> - **CCTV world-click focus:** clicking an in-world CCTV icon or ambient card
|
> - **CCTV world-click focus:** clicking an in-world CCTV icon or ambient card
|
||||||
> activates it and routes the camera flight through the panel FOCUS policy.
|
> activates it and routes the camera flight through the panel FOCUS policy.
|
||||||
> Aircraft/satellite tracking releases outside cockpit; cockpit retains the
|
> Aircraft/satellite tracking releases outside cockpit; cockpit retains the
|
||||||
@@ -2985,6 +3294,13 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with
|
|||||||
> position getter reads only `_trackedDisplayCached`, the same authoritative
|
> position getter reads only `_trackedDisplayCached`, the same authoritative
|
||||||
> Cartesian already consumed by the tracked visual and camera, and never
|
> Cartesian already consumed by the tracked visual and camera, and never
|
||||||
> advances dead reckoning from the host frame.
|
> advances dead reckoning from the host frame.
|
||||||
|
> - **Scheduled route plausibility:** Aircraft route labels and selected-route
|
||||||
|
> context reject the distant backwards, forwards and antipodal extensions of
|
||||||
|
> an airport pair's great circle. The existing 130 km endpoint admission and
|
||||||
|
> 200 km cross-track tolerance remain; a projected fix outside the finite
|
||||||
|
> origin-to-destination minor arc also has a generous 200 km extension at
|
||||||
|
> either end. Missing coordinates and coincident or antipodal endpoint pairs
|
||||||
|
> retain their loose, unjudgeable behavior; vertical-trend checks are unchanged.
|
||||||
> - **Aircraft label convention (both flight layers, 2026-08-18):** every civil
|
> - **Aircraft label convention (both flight layers, 2026-08-18):** every civil
|
||||||
> and military label surface resolves **callsign → registration → icao24**
|
> and military label surface resolves **callsign → registration → icao24**
|
||||||
> (`_contactLabel()` in `flights.js`; the same chain inline in
|
> (`_contactLabel()` in `flights.js`; the same chain inline in
|
||||||
@@ -3370,7 +3686,7 @@ test:track` 43 tracking invariants · headless QA harnesses under
|
|||||||
|
|
||||||
### Interface localization (October 2026)
|
### Interface localization (October 2026)
|
||||||
|
|
||||||
Shipped locales: **en** (source and fallback), **es** and **ru**. The code lives in
|
Shipped locales: **en** (source and fallback), **es**, **ru** and **zh-CN**. The code lives in
|
||||||
`src/i18n/`. How to mark UI text: [`docs/I18N.md`](./I18N.md); translator workflow:
|
`src/i18n/`. How to mark UI text: [`docs/I18N.md`](./I18N.md); translator workflow:
|
||||||
[`docs/TRANSLATORS.md`](./TRANSLATORS.md).
|
[`docs/TRANSLATORS.md`](./TRANSLATORS.md).
|
||||||
|
|
||||||
@@ -3381,7 +3697,8 @@ Shipped locales: **en** (source and fallback), **es** and **ru**. The code lives
|
|||||||
value). English has no catalog; every other locale is
|
value). English has no catalog; every other locale is
|
||||||
`src/i18n/locales/<code>.json`, and a missing entry falls back to English.
|
`src/i18n/locales/<code>.json`, and a missing entry falls back to English.
|
||||||
- **Locale choice.** `?lang=` → stored choice (`gev:locale:v1`) →
|
- **Locale choice.** `?lang=` → stored choice (`gev:locale:v1`) →
|
||||||
`navigator.languages` → English. `?lang=` is not stored, and share links drop
|
`navigator.languages` → English. Chinese tags map to `zh-CN`; tags containing
|
||||||
|
`Hant`, `TW`, `HK` or `MO` are skipped. `?lang=` is not stored, and share links drop
|
||||||
it, so a recipient keeps their own language. The globe picker in the
|
it, so a recipient keeps their own language. The globe picker in the
|
||||||
bottom-right corner (`src/ui/localePicker.js`) stores the choice, drops
|
bottom-right corner (`src/ui/localePicker.js`) stores the choice, drops
|
||||||
`?lang=` (keeps it only when storage is blocked) and reloads. It asks first
|
`?lang=` (keeps it only when storage is blocked) and reloads. It asks first
|
||||||
@@ -3420,7 +3737,7 @@ Historical planning documents may not match runtime behavior.
|
|||||||
## Runtime Stack
|
## Runtime Stack
|
||||||
|
|
||||||
- Vite + CesiumJS app with Google Photorealistic 3D Tiles
|
- Vite + CesiumJS app with Google Photorealistic 3D Tiles
|
||||||
- Scene/HUD/style systems in `src/ui.js` and `src/hud.js`
|
- Scene/HUD/style systems in `src/ui/` and `src/hud.js`
|
||||||
- Layer management in `src/data/manager.js`
|
- Layer management in `src/data/manager.js`
|
||||||
- Map stack switching in `src/mapStackController.js`
|
- Map stack switching in `src/mapStackController.js`
|
||||||
- Voice control in `src/voice/` (OpenAI Realtime over WebRTC)
|
- Voice control in `src/voice/` (OpenAI Realtime over WebRTC)
|
||||||
@@ -3447,13 +3764,13 @@ its criteria cannot be silently ignored.
|
|||||||
| Satellites | CelesTrak | `src/data/satellites.js` | `/api/celestrak` | 120s |
|
| Satellites | CelesTrak | `src/data/satellites.js` | `/api/celestrak` | 120s |
|
||||||
| Space Missions (30d) | Launch Library 2 + CelesTrak | `src/data/rocketLaunches.js` | `/api/launches` + `/api/celestrak/active` | 5 min |
|
| Space Missions (30d) | Launch Library 2 + CelesTrak | `src/data/rocketLaunches.js` | `/api/launches` + `/api/celestrak/active` | 5 min |
|
||||||
| Traffic | Selectable TomTom / OpenStreetMap / Hybrid roads; optional TomTom flow (BYOK) | `src/data/traffic.js` | browser-direct OpenFreeMap tiles; `/api/tomtom` for flow | viewport-driven; capped tile cache |
|
| Traffic | Selectable TomTom / OpenStreetMap / Hybrid roads; optional TomTom flow (BYOK) | `src/data/traffic.js` | browser-direct OpenFreeMap tiles; `/api/tomtom` for flow | viewport-driven; capped tile cache |
|
||||||
| CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Statens vegvesen (NO, stills + live HLS) + King County (WA) Open Data + WSDOT (WA) Open Data + NYC DOT TMC (opt-in) + City of Toronto RESCU + Catalonia (Servei Català de Trànsit) + FL511 (FL, opt-in) + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) |
|
| CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Statens vegvesen (NO, stills + live HLS, Kartverket ground heights) + King County (WA) Open Data + WSDOT (WA) Open Data + NYC DOT TMC (opt-in) + City of Toronto RESCU + Catalonia (Servei Català de Trànsit) + Hong Kong Transport Department + FL511 (FL, opt-in) + Freeway Bureau, MOTC (Taiwan) + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) |
|
||||||
| Radio | Radio Browser (public-domain station directory) | `src/data/radio.js` | `/api/radio/stations`, `/api/radio/click/:uuid` | 45 min directory refresh |
|
| Radio | Radio Browser (public-domain station directory) | `src/data/radio.js` | `/api/radio/stations`, `/api/radio/click/:uuid` | 45 min directory refresh |
|
||||||
| Transit 🚌 | Operator GTFS-Realtime VehiclePositions (7 keyless regions, `src/data/transitFeeds.js`) | `src/layers/transit/` via `src/app/layers/transit.js` | `/api/transit` | 15s (poll + delayed playback) |
|
| Transit 🚌 | Operator GTFS-Realtime VehiclePositions (7 keyless regions, `src/data/transitFeeds.js`) | `src/layers/transit/` via `src/app/layers/transit.js` | `/api/transit` | 15s (poll + delayed playback) |
|
||||||
| Bikeshare 🚲 | GBFS (Lyft + BCycle + Social Bicycles + PBSC incl. Bike Share Toronto, Bike Itaú (Brazil), Bicing (Barcelona) and seven other Spanish cities + Urban Sharing, Entur, JCDecaux (Lillestrøm, Valencia, Seville) + nextbike (AMBici)) | `src/data/bikeshare.js` | `/api/gbfs` | 60s |
|
| Bikeshare 🚲 | GBFS (Lyft + BCycle + Social Bicycles + PBSC incl. Bike Share Toronto, Bike Itaú (Brazil), Bicing (Barcelona) and seven other Spanish cities + Urban Sharing, Entur, JCDecaux (Lillestrøm, Valencia, Seville) + nextbike (AMBici)) | `src/data/bikeshare.js` | `/api/gbfs` | 60s |
|
||||||
| Directions 🧭 | OSRM on FOSSGIS servers (OpenStreetMap) | `src/data/directions.js` | `/api/route` (`steps=1`) | on placement / mode change |
|
| Directions 🧭 | OSRM on FOSSGIS servers (OpenStreetMap) | `src/data/directions.js` | `/api/route` (`steps=1`) | on placement / mode change |
|
||||||
| Datacenters ▣ | OSM extract (bundled) | `src/data/localLayers.js` | — | static |
|
| Datacenters ▣ | OSM extract (bundled) | `src/data/infrastructure.js` | — | static |
|
||||||
| Dams ▰ | OpenInfraMap/OSM extract (bundled) | `src/data/localLayers.js` | — | static |
|
| Dams ▰ | OpenInfraMap/OSM extract (bundled) | `src/data/infrastructure.js` | — | static |
|
||||||
| Submarine Cables ◠ | TeleGeography public map (bundled) | `src/data/telegeographySubmarineCables.js` | — | static |
|
| Submarine Cables ◠ | TeleGeography public map (bundled) | `src/data/telegeographySubmarineCables.js` | — | static |
|
||||||
| FIRMS Active Fires ▲ | NASA FIRMS live (VIIRS ×3 NRT + MODIS NRT, trailing 24h) | `src/data/firmsHeatmap.js` | `/api/firms` (`FIRMS_MAP_KEY`) | 10 min (proxy TTL 30 min) |
|
| FIRMS Active Fires ▲ | NASA FIRMS live (VIIRS ×3 NRT + MODIS NRT, trailing 24h) | `src/data/firmsHeatmap.js` | `/api/firms` (`FIRMS_MAP_KEY`) | 10 min (proxy TTL 30 min) |
|
||||||
| Street Level 📷 | Mapillary street-level imagery (vector tiles, Graph API, MapillaryJS) | `src/layers/streetLevel/` via `src/app/layers/streetLevel.js` | `/api/mapillary/status`, `/api/mapillary/tiles/coverage/{z}/{x}/{y}` (`MAPILLARY_CLIENT_TOKEN`) | camera-driven (320 ms debounce, ≤9 tiles, server memory cache 24 h) |
|
| Street Level 📷 | Mapillary street-level imagery (vector tiles, Graph API, MapillaryJS) | `src/layers/streetLevel/` via `src/app/layers/streetLevel.js` | `/api/mapillary/status`, `/api/mapillary/tiles/coverage/{z}/{x}/{y}` (`MAPILLARY_CLIENT_TOKEN`) | camera-driven (320 ms debounce, ≤9 tiles, server memory cache 24 h) |
|
||||||
@@ -3505,6 +3822,10 @@ disabled. With it, coverage draws as z11–14 sequence lines below 60 km; the pr
|
|||||||
tiles (12 MB → ~80 KB) and shares one upstream fetch between concurrent
|
tiles (12 MB → ~80 KB) and shares one upstream fetch between concurrent
|
||||||
requests for a tile.
|
requests for a tile.
|
||||||
|
|
||||||
|
Line coverage accepts a provider’s numeric `zoomFor` result or its legacy
|
||||||
|
`{zoom, kind}` shape. Overview levels and invalid zooms show the zoom-in hint
|
||||||
|
instead of silently requesting no tiles.
|
||||||
|
|
||||||
Coverage is z11–14 sequence lines below 60 km; above that nothing is drawn and
|
Coverage is z11–14 sequence lines below 60 km; above that nothing is drawn and
|
||||||
the panel says to zoom in. Tiles come from the visible ground within range of
|
the panel says to zoom in. Tiles come from the visible ground within range of
|
||||||
the camera, nearest first, at most 9 (the halves of a view across
|
the camera, nearest first, at most 9 (the halves of a view across
|
||||||
@@ -3637,8 +3958,8 @@ lookup asks the photo API for the projection (`SPHERE`/`PLANE`; it ignores
|
|||||||
`fieldOfView`) and upload date and checks each answer the same way (when
|
`fieldOfView`) and upload date and checks each answer the same way (when
|
||||||
photos are near but none passes, the panel says so), and the cones pass on
|
photos are near but none passes, the panel says so), and the cones pass on
|
||||||
each photo's projection and upload date. The provider declares
|
each photo's projection and upload date. The provider declares
|
||||||
`sinceBasis: 'upload'`, so while it is lit the panel shows "KartaView: by
|
`sinceBasis: 'upload'`, so while it is lit the panel shows "(by
|
||||||
upload date" under the SINCE slider (`sinceNote` in the UI state). Tiles come uncompressed and
|
upload date)" beside the SINCE value, as ANY DATE (by upload date) (`sinceNote` in the UI state). Tiles come uncompressed and
|
||||||
uncacheable, so the last 24 decoded tiles are kept for 10 minutes while
|
uncacheable, so the last 24 decoded tiles are kept for 10 minutes while
|
||||||
KartaView is lit (`createTileCache`, dropped when it is switched off); one
|
KartaView is lit (`createTileCache`, dropped when it is switched off); one
|
||||||
that does not answer in 20 s is asked once more, and one that is not a line
|
that does not answer in 20 s is asked once more, and one that is not a line
|
||||||
@@ -3799,6 +4120,10 @@ routing it shows `Routing…`, and a router miss reads
|
|||||||
The layer holds continuous render only while a route is drawn (the dashes
|
The layer holds continuous render only while a route is drawn (the dashes
|
||||||
animate). Disable clears all state.
|
animate). Disable clears all state.
|
||||||
|
|
||||||
|
At the metre/kilometre boundary, route distance labels use the rounded
|
||||||
|
ten-metre value to select the unit, switching to kilometres at 995 metres;
|
||||||
|
a 996-metre leg displays as `1.0 km` rather than `1000 m`.
|
||||||
|
|
||||||
**Keys needed: none.** Directions works on a keyless boot, and the layer is off
|
**Keys needed: none.** Directions works on a keyless boot, and the layer is off
|
||||||
until the operator turns it on.
|
until the operator turns it on.
|
||||||
|
|
||||||
@@ -3851,6 +4176,9 @@ and as the last line of the list, because the arrival step is among the ones
|
|||||||
dropped. A route of exactly 200 is complete, and a roundabout exit after the
|
dropped. A route of exactly 200 is complete, and a roundabout exit after the
|
||||||
200th keeps folding into it.
|
200th keeps folding into it.
|
||||||
|
|
||||||
|
Route duration labels select their unit after rounding seconds, switching to
|
||||||
|
minutes at 59.5 seconds; a 59.6-second leg displays as `1 min`, rather than `60 s`.
|
||||||
|
|
||||||
A step's card opens from its dot on the globe or from its line in the list, and
|
A step's card opens from its dot on the globe or from its line in the list, and
|
||||||
the list covers every step including departure and arrival, which have no dot.
|
the list covers every step including departure and arrival, which have no dot.
|
||||||
What a card needs is a resolved ground anchor: a step whose ground cell has not
|
What a card needs is a resolved ground anchor: a step whose ground cell has not
|
||||||
@@ -3963,7 +4291,9 @@ to 0.45 at two minutes, 0.20 at ten minutes and 0.08 at fifteen minutes.
|
|||||||
|
|
||||||
**Cards and DETECT.** The card presents route, mode/operator, displayed motion,
|
**Cards and DETECT.** The card presents route, mode/operator, displayed motion,
|
||||||
display delay and newest-report age, reported speed/stop, vehicle ID, and
|
display delay and newest-report age, reported speed/stop, vehicle ID, and
|
||||||
available trail duration. Its anchor follows the rendered marker; text is
|
available trail duration. Report age is rounded to whole seconds and stays in seconds while that rounded
|
||||||
|
count is below 90; otherwise it shows completed minutes calculated from the
|
||||||
|
rounded second count. Its anchor follows the rendered marker; text is
|
||||||
checked at 4 Hz and published when it changes. DETECT labels retain route,
|
checked at 4 Hz and published when it changes. DETECT labels retain route,
|
||||||
mode and motion, with the selected label suppressed beside its fuller card.
|
mode and motion, with the selected label suppressed beside its fuller card.
|
||||||
Candidate membership changes on revisions, coalesced at 4 Hz; paints read
|
Candidate membership changes on revisions, coalesced at 4 Hz; paints read
|
||||||
@@ -3991,6 +4321,8 @@ animation. Polls, surface completions and trail fades request individual renders
|
|||||||
Disable clears vehicles, detection references, requests, timers and holds;
|
Disable clears vehicles, detection references, requests, timers and holds;
|
||||||
destruction also releases collections and registrations.
|
destruction also releases collections and registrations.
|
||||||
|
|
||||||
|
The `scripts/qa-layer-visibility.mjs` gate uses the development-only registration seam and real browser tab visibility to prove periodic work pauses while hidden, explicit refresh still works, resume performs one overdue refresh, and disabled layers stay stopped.
|
||||||
|
|
||||||
**Acceptance harness.** Run against an already running development server:
|
**Acceptance harness.** Run against an already running development server:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -4077,6 +4409,9 @@ silent for five minutes are removed. Credits appear when the feed is used.
|
|||||||
coordinator, but it is not a user-visible Data Layers entry. Its visible entry
|
coordinator, but it is not a user-visible Data Layers entry. Its visible entry
|
||||||
point is the right-side `CONTEXT` chooser's `CONTACTS` mode.
|
point is the right-side `CONTEXT` chooser's `CONTACTS` mode.
|
||||||
|
|
||||||
|
Contacts distance labels select metres or kilometres after rounding to the
|
||||||
|
nearest metre, so a 999.5-metre contact displays as `1.0 km`, not `1000 m`.
|
||||||
|
|
||||||
At global scale, ambient Radio cluster badges are hard-opacity shared-host
|
At global scale, ambient Radio cluster badges are hard-opacity shared-host
|
||||||
entries: count/category updates and identity replacement do not run keyhole or
|
entries: count/category updates and identity replacement do not run keyhole or
|
||||||
enter/exit ramps. Shared collision, viewport rejection, allocation, and horizon
|
enter/exit ramps. Shared collision, viewport rejection, allocation, and horizon
|
||||||
@@ -4407,7 +4742,7 @@ silently demoting every later lookup for the session.
|
|||||||
multi-sensor datasheets publish a capability range, not the lens's current zoom, so those
|
multi-sensor datasheets publish a capability range, not the lens's current zoom, so those
|
||||||
cameras keep their estimate and show `SPEC FOV min–max° (CAPABILITY)` beside it. Public
|
cameras keep their estimate and show `SPEC FOV min–max° (CAPABILITY)` beside it. Public
|
||||||
camera state carries `fovSource`, `specFovMinDeg`, `specFovMaxDeg` and `spec` (#452).
|
camera state carries `fovSource`, `specFovMinDeg`, `specFovMaxDeg` and `spec` (#452).
|
||||||
- **FIRMS**: no ground clamping (zero 3D-tiles height sampling), ≤18 screen-decluttered ambient labels, click-to-inspect detail card, 2.5k/3k sprite budgets viewport-clipped by FRP.
|
- **FIRMS**: no ground clamping (zero 3D-tiles height sampling), ≤18 screen-decluttered ambient labels, click-to-inspect detail card, 2.5k/3k sprite budgets viewport-clipped by FRP. Fire-power labels share display rounding before localization: one decimal below the rounded 10 MW boundary, then whole megawatts (for example, 9.96 MW reads 10 MW).
|
||||||
- **CCTV v2 foundation:** a pitched
|
- **CCTV v2 foundation:** a pitched
|
||||||
frustum wireframe (4 corner rays + far-cap rectangle) with a monitor plane at the frustum's
|
frustum wireframe (4 corner rays + far-cap rectangle) with a monitor plane at the frustum's
|
||||||
far cap, retargeting the existing video/canvas texture pipeline. Stills are drawn at their source aspect, centered on the placeholder dark (`#01070c`), instead of being stretched to the 16:9 canvas. Manual calibration only —
|
far cap, retargeting the existing video/canvas texture pipeline. Stills are drawn at their source aspect, centered on the placeholder dark (`#01070c`), instead of being stretched to the 16:9 canvas. Manual calibration only —
|
||||||
@@ -4432,8 +4767,10 @@ silently demoting every later lookup for the session.
|
|||||||
default 36 → 250, hard bound 300), filtered to `camera_status === TURNED_ON` (~815 live of
|
default 36 → 250, hard bound 300), filtered to `camera_status === TURNED_ON` (~815 live of
|
||||||
1,003 rows). City packs (2026-07-04): Caltrans (districts 4/7/11/3 — SF, LA, San Diego,
|
1,003 rows). City packs (2026-07-04): Caltrans (districts 4/7/11/3 — SF, LA, San Diego,
|
||||||
Sacramento; cap 300) and TfL London JamCams (cap 250) join Austin (cap 250) as keyless default
|
Sacramento; cap 300) and TfL London JamCams (cap 250) join Austin (cap 250) as keyless default
|
||||||
sources. Ontario 511 (2026-09-12) adds keyless highway cameras including Kitchener-area routes
|
sources. Ontario 511 (2026-09-12) adds highway cameras including Kitchener-area routes
|
||||||
(cap 1,000, all enabled rows from the current ~944-camera catalog). All sources are RAW PRIOR
|
(cap 1,000, all enabled rows from the current ~944-camera catalog). The 511on.ca API now
|
||||||
|
requires a developer key, so the pack needs `ONTARIO_511_API_KEY` and is skipped without one
|
||||||
|
(#1228). All sources are RAW PRIOR
|
||||||
poses, and the layer is stills-first.
|
poses, and the layer is stills-first.
|
||||||
sources. Fintraffic Finland road weather cameras (2026-09-13; cap 300, `CCTV_FINTRAFFIC_MAX_SOURCES`,
|
sources. Fintraffic Finland road weather cameras (2026-09-13; cap 300, `CCTV_FINTRAFFIC_MAX_SOURCES`,
|
||||||
kill switch `CCTV_FINTRAFFIC_ENABLED=0`) are the fourth pack: one keyless GeoJSON station list
|
kill switch `CCTV_FINTRAFFIC_ENABLED=0`) are the fourth pack: one keyless GeoJSON station list
|
||||||
@@ -4441,7 +4778,18 @@ silently demoting every later lookup for the session.
|
|||||||
GATHERING stations carry 2,256 in-collection presets, prioritized to 300 against seven anchors
|
GATHERING stations carry 2,256 in-collection presets, prioritized to 300 against seven anchors
|
||||||
on the main road spine. `CCTV_MAX_SOURCES` is a 4,000 catalog-wide ceiling shared round-robin
|
on the main road spine. `CCTV_MAX_SOURCES` is a 4,000 catalog-wide ceiling shared round-robin
|
||||||
across packs, so a lower global cap thins every region instead of starving the last pack.
|
across packs, so a lower global cap thins every region instead of starving the last pack.
|
||||||
~1,100 cameras total, all RAW PRIOR poses, stills-first.
|
~1,100 cameras total, all RAW PRIOR poses, stills-first. The catalog lives for 15 minutes; once
|
||||||
|
it expires, `/api/cctv` keeps answering from it while one background refresh reloads every
|
||||||
|
pack, so only the very first load waits for the slowest pack (a live first load took about
|
||||||
|
22 s, and is bounded by Tarktee's 30 s). A background refresh that fails or comes back
|
||||||
|
empty keeps the old catalog and is retried after another 15 minutes, not on the next
|
||||||
|
request. Estonia's Tarktee DATEX feeds, which answer in 2 to 16.6 s, get 30 s
|
||||||
|
(`TARKTEE_FETCH_TIMEOUT_MS`) where other packs get 15 s.
|
||||||
|
Both Tarktee XML catalogs refuse redirects and have a 4 MiB per-document
|
||||||
|
limit, enforced against declared length and streamed bytes. A rejected fetch,
|
||||||
|
unsuccessful response, or body-read failure releases both available response
|
||||||
|
bodies; read failures also cancel an active sibling reader. Cameras sharing
|
||||||
|
an image number are deduplicated before the Tarktee pack cap is applied.
|
||||||
- **CCTV v3 UX — viewshed + calibration gizmo** (built 2026-07-05 and field
|
- **CCTV v3 UX — viewshed + calibration gizmo** (built 2026-07-05 and field
|
||||||
validated 2026-07-21): the COVERAGE toggle is a
|
validated 2026-07-21): the COVERAGE toggle is a
|
||||||
tri-state cycle `OFF → ON → VIEWSHED`; viewshed mode renders each visible camera's frustum
|
tri-state cycle `OFF → ON → VIEWSHED`; viewshed mode renders each visible camera's frustum
|
||||||
@@ -4497,7 +4845,7 @@ silently demoting every later lookup for the session.
|
|||||||
A selected mission renders its orbit as four repeating tactical sectors, each containing one prominent cyan dot followed by one hundred thin translucent dashes. The bright dots act as orbit anchors while the subdued dash field remains depth-tested against the globe and is shown only for the selected mission.
|
A selected mission renders its orbit as four repeating tactical sectors, each containing one prominent cyan dot followed by one hundred thin translucent dashes. The bright dots act as orbit anchors while the subdued dash field remains depth-tested against the globe and is shown only for the selected mission.
|
||||||
Close selected-pad views add one static 500 m-radius cyan launch-zone ring with a low-opacity translucent fill over the sampled photoreal launch-site surface. The single scene primitive is created only for the visible selected site and is otherwise dormant. It appears during Focus, sufficiently close manual zoom, and the replay countdown, but is suppressed above 120 km camera altitude, beyond 180 km direct camera-to-pad range, for unselected missions, and whenever Space Missions is inactive. Focus establishes a launch-site-centered camera transform once; subsequent manual heading and pitch changes remain centered on that site without an automated per-frame correction. Surface mission markers and labels use an additional conservative globe-limb margin before the exact ellipsoid occluder boundary, preventing near-horizon visibility from alternating between frames.
|
Close selected-pad views add one static 500 m-radius cyan launch-zone ring with a low-opacity translucent fill over the sampled photoreal launch-site surface. The single scene primitive is created only for the visible selected site and is otherwise dormant. It appears during Focus, sufficiently close manual zoom, and the replay countdown, but is suppressed above 120 km camera altitude, beyond 180 km direct camera-to-pad range, for unselected missions, and whenever Space Missions is inactive. Focus establishes a launch-site-centered camera transform once; subsequent manual heading and pitch changes remain centered on that site without an automated per-frame correction. Surface mission markers and labels use an additional conservative globe-limb margin before the exact ellipsoid occluder boundary, preventing near-horizon visibility from alternating between frames.
|
||||||
- **AIS vessels**: chevron symbology (naval cyan base, type tints), world-space headings, MMSI-keyed reconciliation (selection survives refreshes; pinned 3 refreshes with STALE marker when absent), detection-overlay integration (`type: 'SEA'`), contextStore registration for voice Q&A. Empty-space clicks, id-less photorealistic-tile picks, and Escape dismiss the vessel card/HUD/context and clear its trail; picks owned by another layer (including `gev-trail:*`) and raw vessel-record picks without a live MMSI key are no-ops for vessel selection. Click and key handlers detach while the layer is disabled and reinstall on enable. Selecting another vessel replaces the selection and trail, and reconciliation clears a trail if its owning vessel is evicted.
|
- **AIS vessels**: chevron symbology (naval cyan base, type tints), world-space headings, MMSI-keyed reconciliation (selection survives refreshes; pinned 3 refreshes with STALE marker when absent), detection-overlay integration (`type: 'SEA'`), contextStore registration for voice Q&A. Empty-space clicks, id-less photorealistic-tile picks, and Escape dismiss the vessel card/HUD/context and clear its trail; picks owned by another layer (including `gev-trail:*`) and raw vessel-record picks without a live MMSI key are no-ops for vessel selection. Click and key handlers detach while the layer is disabled and reinstall on enable. Selecting another vessel replaces the selection and trail, and reconciliation clears a trail if its owning vessel is evicted.
|
||||||
- **Track trails**: server accumulates per-MMSI ring buffers (`/api/vessels/track?mmsi=`, Float32+Uint32, 64 samples, 30s/25m thinning); aircraft backfill proxies `/api/flights/track` (OAuth, own credit bucket) and `/api/military/track` (tar1090 readsb, ~24h history, ODbL — credit adsb.lol).
|
- **Track trails**: server accumulates per-MMSI ring buffers (`/api/vessels/track?mmsi=`, Float32+Uint32, 64 samples, 30s/25m thinning); aircraft backfill proxies `/api/flights/track` (OAuth, own credit bucket). Military aircraft keep only their locally accumulated trails: `/api/military/track` validates the hex, then answers `503 history_unavailable` without an upstream request.
|
||||||
- Shared `src/data/pickRegistry.js` stops the two flight layers' click handlers from fighting over the camera.
|
- Shared `src/data/pickRegistry.js` stops the two flight layers' click handlers from fighting over the camera.
|
||||||
|
|
||||||
### Optional Overpass configuration
|
### Optional Overpass configuration
|
||||||
@@ -4532,6 +4880,12 @@ Retry-After; absent delays back off from 30 seconds to five minutes. Empty
|
|||||||
remain. Malformed capability-probe responses remain unknown and are retried after the existing 30-second backoff; valid configured decisions remain memoized.
|
remain. Malformed capability-probe responses remain unknown and are retried after the existing 30-second backoff; valid configured decisions remain memoized.
|
||||||
Consumer capability misses never schedule automatic retries.
|
Consumer capability misses never schedule automatic retries.
|
||||||
|
|
||||||
|
Keyless browser validation also probes the optional Google tiles-token route.
|
||||||
|
The token client cancels rejected response bodies before returning unavailable,
|
||||||
|
so they do not remain pending in browser network observation. The Overpass gate
|
||||||
|
accepts only that same-origin route's expected 404 console diagnostic; other
|
||||||
|
HTTP and JavaScript errors still fail acceptance.
|
||||||
|
|
||||||
Operator-configured Overpass area and focus-footprint queries use body-level
|
Operator-configured Overpass area and focus-footprint queries use body-level
|
||||||
`out geom` (with `center` for focus), preserving relation member outlines.
|
`out geom` (with `center` for focus), preserving relation member outlines.
|
||||||
Member geometry is printed only inside a box around the point (6 km for
|
Member geometry is printed only inside a box around the point (6 km for
|
||||||
@@ -4749,7 +5103,7 @@ and unreachable upstream (502/504) separately from road geometry.
|
|||||||
> branch: **2,281 on Node 25.6.1** (allocation suites skipped) = 2,255 on
|
> branch: **2,281 on Node 25.6.1** (allocation suites skipped) = 2,255 on
|
||||||
> `main` + 26 route pins; the same tree on Node 24 reports 2,295.
|
> `main` + 26 route pins; the same tree on Node 24 reports 2,295.
|
||||||
|
|
||||||
- **Camera verbs** (`src/cameraVerbs.js`) — one motion slot, driven per clock tick. `move_camera` orbits/pans/tilts/rotates; `fly_route` is a cinematic dolly along an existing route annotation.
|
- **Camera verbs** (`src/cameraVerbs.js`) — one motion slot, driven per clock tick. `move_camera` orbits/pans/tilts/rotates; `fly_route` is a cinematic dolly along an existing route annotation. `rotate_globe` rotates the world-space camera rigidly around Earth's center using its screen-up axis, preserving geocentric range and framing. Left/right describe visible globe motion, while `rotate` turns the camera in place. Omitted globe direction/mode default to right/continuous; explicit values prevail. Other camera motions keep their once default. Admission requires 3D, no Cockpit/tracking owner and the whole globe inside the existing keyhole circle, using the shared keyhole/projection geometry and vertical camera FOV with a rotation-local bounding-sphere cone for off-axis containment. Canvas-edge clipping alone does not block a rigid rotation or its unchanged Globe View preset. There is no altitude cutoff. An already smaller, fully framed globe rotates directly; repeated nudges, direction and speed changes recheck current framing without demanding a reset merely because ellipsoid altitude varied. A close-up or overlap refuses without moving the camera. When the unchanged Globe View preset fits, voice says “Globe rotation isn’t available from this view. You can say ‘Reset to Globe View and rotate the globe.’” without asking a permission question or automatically resetting. Explicit Reset/Show Globe View commands remain direct; an explicit reset-and-rotate request waits for reset success before requesting rotation. If OpenAI dispatches both tools together, rotation waits for earlier same-response resets and is skipped if any fails or is cancelled. OpenAI rejects later tool calls in the same response after a globe refusal, including calls arriving while globe admission is pending, and preserves response-scoped `tool_choice: none` for the refusal follow-up; Gemini rejects further tool execution, including queued sibling calls, until fresh typed input or admitted speech. Model interruption, advisory captions and microphone toggles do not release that guard. There is no pending Yes/No offer. Outside the refused response, explicit-command interpretation and avoiding repeated resets remain model-guided; a distinct model-issued reset call is not an app-enforced proof of user intent. A failed standalone reset can still be retried by the model, and a reset result does not guarantee the camera has fully settled. Before suggesting a reset, a cloned camera checks the unchanged Globe View destination against the same keyhole/depth rules without moving the live camera. If that preset cannot fit, `globe-reset-unavailable` asks for further zoom and the refusal instructs further zoom instead of a reset; this also covers ultrawide views. Unsupported view geometry or non-WGS84 projection assumptions fail closed without the closer-zoom instruction. Supported camera inputs/navigation intents cancel rotation before their action. Finite radius greater than the ellipsoid's maximum radius proves whole-arc clearance at constant centre-distance; no low-altitude swept solver is needed. The unchanged Globe View reset preset remains 18,000,000 m. Terrain and buildings remain outside the guarantee.
|
||||||
- **The route dolly is shaped, not linear** (2026-08-20). A trapezoid speed profile (smoothstep up, cruise, smoothstep down, distance taken as the closed-form integral) eases both ends without changing the pace — duration is still `totalM / ROUTE_M_S[speed]`, with one exception: a 0.5 s minimum keeps a degenerate route from being an instant teleport, so routes under 10 m (slow) / 20 m (normal) / 45 m (fast) fly SLOWER than the speed word, never faster. Turns bank up to **10°** (a 90° street corner settles near 7.5°), measured as a triangular pulse over a 4 s window centred on the camera so the roll leads in and unwinds after. Altitude breathes ±20 m around the 260 m mean and lifts up to 26 m into turns. Pitch is LOCKED at −32°; heading comes from a gaze that leads the path by 6.5 s of travel. `prefers-reduced-motion` zeroes the roll and the altitude shaping and keeps the easing.
|
- **The route dolly is shaped, not linear** (2026-08-20). A trapezoid speed profile (smoothstep up, cruise, smoothstep down, distance taken as the closed-form integral) eases both ends without changing the pace — duration is still `totalM / ROUTE_M_S[speed]`, with one exception: a 0.5 s minimum keeps a degenerate route from being an instant teleport, so routes under 10 m (slow) / 20 m (normal) / 45 m (fast) fly SLOWER than the speed word, never faster. Turns bank up to **10°** (a 90° street corner settles near 7.5°), measured as a triangular pulse over a 4 s window centred on the camera so the roll leads in and unwinds after. Altitude breathes ±20 m around the 260 m mean and lifts up to 26 m into turns. Pitch is LOCKED at −32°; heading comes from a gaze that leads the path by 6.5 s of travel. `prefers-reduced-motion` zeroes the roll and the altitude shaping and keeps the easing.
|
||||||
- **Invariants — do not "fix" these.** (1) **Every** release levels the roll: completion unwinds it through the ease-out envelope, and `interruptCameraMotion` zeroes it synchronously (heading/pitch/position preserved). Cesium keeps the last up vector it was handed, so skipping this leaves the user holding a tilted horizon. (2) Heading is interpolated as an ANGLE about the local up, never as a Cartesian lerp — a lerp cannot cross an antipodal pair, so an out-and-back route looked backwards for the entire return leg; `signedTurnRad` branches the exact-180° case deterministically because the cross product's sign there is a floating-point coin toss. (3) A COLD floor cell is missing data, not flat ground: route vertices carry height 0, so trusting them flew a mountain corridor at 260 m above the ELLIPSOID. The corridor warm is fire-and-forget, so the dolly ARMS — camera untouched, no teleport onto the route — for up to 1.2 s waiting for real floor data, falls back to a rendered-mesh probe (`scene.sampleHeight`) when the DEM stays cold — latched to ONE firing per flight, for exactly the cells the cache could not answer, so a route costs at most 8 `sampleHeight` calls however long it arms — and otherwise holds the launch altitude for the whole route. A corridor is only RESOLVED when every cell is accounted for: one warm cell says nothing about the ground under the other seven, and treating it as an answer let the dolly descend to 460 m over a 1,600 m rendered surface. The probe reads the rendered surface at the current LOD (rooftops and primitives included) — a better estimate than nothing, not a guaranteed upper bound. Never descend blind. The floor is SMOOTHED in both directions for the ride while the hard clearance clamp reads the RAW sample, so a cell boundary cannot pop the eye but a cliff is still cleared on the frame it is seen. A pre-departure floor is adopted whole; one arriving mid-flight is eased onto, and the eye's descent rate is capped.
|
- **Invariants — do not "fix" these.** (1) **Every** release levels the roll: completion unwinds it through the ease-out envelope, and `interruptCameraMotion` zeroes it synchronously (heading/pitch/position preserved). Cesium keeps the last up vector it was handed, so skipping this leaves the user holding a tilted horizon. (2) Heading is interpolated as an ANGLE about the local up, never as a Cartesian lerp — a lerp cannot cross an antipodal pair, so an out-and-back route looked backwards for the entire return leg; `signedTurnRad` branches the exact-180° case deterministically because the cross product's sign there is a floating-point coin toss. (3) A COLD floor cell is missing data, not flat ground: route vertices carry height 0, so trusting them flew a mountain corridor at 260 m above the ELLIPSOID. The corridor warm is fire-and-forget, so the dolly ARMS — camera untouched, no teleport onto the route — for up to 1.2 s waiting for real floor data, falls back to a rendered-mesh probe (`scene.sampleHeight`) when the DEM stays cold — latched to ONE firing per flight, for exactly the cells the cache could not answer, so a route costs at most 8 `sampleHeight` calls however long it arms — and otherwise holds the launch altitude for the whole route. A corridor is only RESOLVED when every cell is accounted for: one warm cell says nothing about the ground under the other seven, and treating it as an answer let the dolly descend to 460 m over a 1,600 m rendered surface. The probe reads the rendered surface at the current LOD (rooftops and primitives included) — a better estimate than nothing, not a guaranteed upper bound. Never descend blind. The floor is SMOOTHED in both directions for the ride while the hard clearance clamp reads the RAW sample, so a cell boundary cannot pop the eye but a cliff is still cleared on the frame it is seen. A pre-departure floor is adopted whole; one arriving mid-flight is eased onto, and the eye's descent rate is capped.
|
||||||
- Pins: `src/routeCinematics.test.mjs` (26 tests), with `scripts/qa-flyroute-mutations.mjs` reverting each fix individually to prove they are load-bearing (19 named defects); rendered proof `scripts/qa-flyroute-cinema.mjs` (drives the real voice runner, measures the real Cesium camera every frame, writes a labelled contact sheet).
|
- Pins: `src/routeCinematics.test.mjs` (26 tests), with `scripts/qa-flyroute-mutations.mjs` reverting each fix individually to prove they are load-bearing (19 named defects); rendered proof `scripts/qa-flyroute-cinema.mjs` (drives the real voice runner, measures the real Cesium camera every frame, writes a labelled contact sheet).
|
||||||
@@ -4758,7 +5112,7 @@ and unreachable upstream (502/504) separately from road geometry.
|
|||||||
- **Every selectable layer writes that one slot** (2026-08-21). The tracking layers — flights, military, satellites — publish selection on their own awareness lane (`gev:awareness-subject-selected`, consumed by the readout card and the Contacts panel) and for the life of the voice tools never wrote the shared slot, so `scope:'selected'` silently answered `in_view` with a contact plainly selected on screen. They now call `selectTrackedSubjectContext` / `refreshTrackedSubjectContext` / `clearTrackedSubjectContext` (`src/data/contextStore.js`) on select / poll / deselect. **Invariants: (a)** that write path must NOT dispatch `gev:entity-selected` — tracking layers already own an event lane and a second one makes two surfaces fight over one subject; **(b)** exactly one record per tracking layer, because a frozen snapshot of a moved contact must never reach the visible-entity scan; **(c)** precedence is recency, not layer — one slot, last selection wins, so clicking an overlay entity supersedes a tracked plane as the voice subject while the plane stays tracked. Satellites refresh on the 1 s propagation beat, not per frame.
|
- **Every selectable layer writes that one slot** (2026-08-21). The tracking layers — flights, military, satellites — publish selection on their own awareness lane (`gev:awareness-subject-selected`, consumed by the readout card and the Contacts panel) and for the life of the voice tools never wrote the shared slot, so `scope:'selected'` silently answered `in_view` with a contact plainly selected on screen. They now call `selectTrackedSubjectContext` / `refreshTrackedSubjectContext` / `clearTrackedSubjectContext` (`src/data/contextStore.js`) on select / poll / deselect. **Invariants: (a)** that write path must NOT dispatch `gev:entity-selected` — tracking layers already own an event lane and a second one makes two surfaces fight over one subject; **(b)** exactly one record per tracking layer, because a frozen snapshot of a moved contact must never reach the visible-entity scan; **(c)** precedence is recency, not layer — one slot, last selection wins, so clicking an overlay entity supersedes a tracked plane as the voice subject while the plane stays tracked. Satellites refresh on the 1 s propagation beat, not per frame.
|
||||||
- **Context-mode vocabulary is symmetric** (2026-08-21). `set_context_mode` accepts `contacts`; the internal id is `flights`. Every model-readable field (`mode`, `entering`, `priorMode`, nested `context`/`contextRollback`, and the transition diagnostic text) is reported in the accepted vocabulary, with the internal id preserved as `<field>Internal`; an absent secondary mode stays `null` rather than claiming to be `off`. Mapping lives in `src/contextModePolicy.js` so UI text and voice payloads cannot drift. Reporting the internal id made the model read `mode:'flights'` as "Contacts is off" and refuse to answer from the `contactsWindow` counts in the same payload.
|
- **Context-mode vocabulary is symmetric** (2026-08-21). `set_context_mode` accepts `contacts`; the internal id is `flights`. Every model-readable field (`mode`, `entering`, `priorMode`, nested `context`/`contextRollback`, and the transition diagnostic text) is reported in the accepted vocabulary, with the internal id preserved as `<field>Internal`; an absent secondary mode stays `null` rather than claiming to be `off`. Mapping lives in `src/contextModePolicy.js` so UI text and voice payloads cannot drift. Reporting the internal id made the model read `mode:'flights'` as "Contacts is off" and refuse to answer from the `contactsWindow` counts in the same payload.
|
||||||
- **Analyst → track handoff carries a key, not just a label** (2026-08-21). `analyst_query` items include `icao24`/`mmsi` alongside the display `id`, and contact lookup uses the shared tiered ranking in `src/data/contactMatch.js`: hex exact → callsign exact → registration exact → callsign prefix → registration prefix → callsign substring → registration substring. The tiers keep an exact callsign ahead of a colliding registration regardless of feed order, and registrations compare separator-insensitively (`G-ABCD`/`GABCD`, `05-8152`/`058152`, `N123AB`/`N-123AB`).
|
- **Analyst → track handoff carries a key, not just a label** (2026-08-21). `analyst_query` items include `icao24`/`mmsi` alongside the display `id`, and contact lookup uses the shared tiered ranking in `src/data/contactMatch.js`: hex exact → callsign exact → registration exact → callsign prefix → registration prefix → callsign substring → registration substring. The tiers keep an exact callsign ahead of a colliding registration regardless of feed order, and registrations compare separator-insensitively (`G-ABCD`/`GABCD`, `05-8152`/`058152`, `N123AB`/`N-123AB`).
|
||||||
- **A typed command supersedes the turn it interrupts** (2026-08-21). `sendTextCommand` defers its `response.create` behind an active response instead of colliding with it, marks that response superseded so a late function call from it is refused rather than dispatched, and drops the old turn's queued follow-up. A refused call is still ANSWERED — a terminal `{ok:false, superseded:true}` `function_call_output` — because an unanswered `function_call` strands a pending call and deadlocks the model; the refusal creates no response of its own. A burst of typed commands coalesces into one response while keeping both conversation items.
|
- **A typed command supersedes the turn it interrupts** (2026-08-21; hardened 2026-10-17). `sendTextCommand` defers its `response.create` behind an active response instead of colliding with it, marks that response superseded so a late function call from it is refused rather than dispatched, and drops the old turn's queued follow-up. A refused call is still ANSWERED — a terminal `{ok:false, superseded:true}` `function_call_output` — because an unanswered `function_call` strands a pending call and deadlocks the model; the refusal creates no response of its own. A burst of typed commands coalesces into one response while keeping both conversation items. If the local channel rejects the user item, that command requests no response and adds no deferred answer; earlier accepted deferred input remains queued. Interruption and pointer preparation still happen before the send attempt.
|
||||||
- **Subject reconciliation across satellite catalog rebuilds** (2026-08-21). A dense↔core toggle or TLE refresh clears and repopulates the catalog, so the published subject is re-resolved against the new satrec; a subject that did NOT survive releases the slot. The per-frame refresh cannot do this itself — `_getTrackedFramePosition` returns early once the satellite has no catalog entry — so the reconcile runs at rebuild completion. An empty catalog is a rebuild in flight, not a disappearance.
|
- **Subject reconciliation across satellite catalog rebuilds** (2026-08-21). A dense↔core toggle or TLE refresh clears and repopulates the catalog, so the published subject is re-resolved against the new satrec; a subject that did NOT survive releases the slot. The per-frame refresh cannot do this itself — `_getTrackedFramePosition` returns early once the satellite has no catalog entry — so the reconcile runs at rebuild completion. An empty catalog is a rebuild in flight, not a disappearance.
|
||||||
- **One aircraft-proximity snapshot** (2026-08-22; hardened 2026-09-30). `collectAircraftProximityWindow` (`src/data/militaryAwareness.js`) computes the Contacts panel cohort once. The voice analyst reads the exact retained snapshot and evaluation timestamp instead of re-scanning live layers, so the panel readout and spoken count for one centre cannot cross snapshots. **Invariant: do not re-derive a proximity count anywhere else.** Each layer retains 20,000 rows and samples one row beyond the cap; a saturated cohort reports `complete:false`, `truncated:true`, and is spoken as a lower bound. They diverged before because the panel read live billboard positions while the analyst used last-fix coordinates over a 2,000-record slice — 111 on screen, 15 spoken. Explicit regions, arbitrary points, and a radius different from the panel radius deliberately keep the general record engine. Entity-centred results carry `window: {engine:'contacts-window', centeredOn, radiusKm, flights, military, aircraft, complete, evaluatedAt}`.
|
- **One aircraft-proximity snapshot** (2026-08-22; hardened 2026-09-30). `collectAircraftProximityWindow` (`src/data/militaryAwareness.js`) computes the Contacts panel cohort once. The voice analyst reads the exact retained snapshot and evaluation timestamp instead of re-scanning live layers, so the panel readout and spoken count for one centre cannot cross snapshots. **Invariant: do not re-derive a proximity count anywhere else.** Each layer retains 20,000 rows and samples one row beyond the cap; a saturated cohort reports `complete:false`, `truncated:true`, and is spoken as a lower bound. They diverged before because the panel read live billboard positions while the analyst used last-fix coordinates over a 2,000-record slice — 111 on screen, 15 spoken. Explicit regions, arbitrary points, and a radius different from the panel radius deliberately keep the general record engine. Entity-centred results carry `window: {engine:'contacts-window', centeredOn, radiusKm, flights, military, aircraft, complete, evaluatedAt}`.
|
||||||
- **Centre precedence for nearby asks**: explicit place in the question > Contacts subject (a selected non-contact entity never silently becomes the centre) > an entity the user names > the current view, said aloud. Contacts active with no subject uses the view rather than reading an empty panel.
|
- **Centre precedence for nearby asks**: explicit place in the question > Contacts subject (a selected non-contact entity never silently becomes the centre) > an entity the user names > the current view, said aloud. Contacts active with no subject uses the view rather than reading an empty panel.
|
||||||
@@ -4792,6 +5146,7 @@ and unreachable upstream (502/504) separately from road geometry.
|
|||||||
- HUD `SUMMARY` readout requests a five-word intelligence-style summary from `/api/openai/hud-summary` (model `OPENAI_HUD_SUMMARY_MODEL`, default `gpt-5-nano`, minimal reasoning).
|
- HUD `SUMMARY` readout requests a five-word intelligence-style summary from `/api/openai/hud-summary` (model `OPENAI_HUD_SUMMARY_MODEL`, default `gpt-5-nano`, minimal reasoning).
|
||||||
- Input is the live basemap label context (place/street/nearby-place labels + enabled layers) — the model is instructed not to infer from coordinates.
|
- Input is the live basemap label context (place/street/nearby-place labels + enabled layers) — the model is instructed not to infer from coordinates.
|
||||||
- Output is sanitized to exactly five words; falls back to the deterministic telemetry summary on error/timeout (5s abort); typewriter animation on update.
|
- Output is sanitized to exactly five words; falls back to the deterministic telemetry summary on error/timeout (5s abort); typewriter animation on update.
|
||||||
|
- A summary asked for before the camera starts moving never paints over the next view: `moveStart` marks the summary dirty and bumps its revision, as `moveEnd` does, aborts the request in flight and stops a reply still typing; neither a reply nor a fallback line (on an error, the 5 s timeout, the abort, or a failed context build) paints once the revision has moved. A dropped request forgets its signature and a timeout counts as a failure, so the next 15 s tick asks again even over the same place. An interrupted typewriter reply also forgets its signature; fully displayed answers remain reusable.
|
||||||
|
|
||||||
### Place-search providers
|
### Place-search providers
|
||||||
|
|
||||||
@@ -4924,6 +5279,7 @@ are omitted rather than framing the wrong part of the globe.
|
|||||||
- **A vertex is a lon/lat, and the mark is DRAPED.** Clicks are picked through `pickWorldFromScreen` — one of two scene helpers the annotation resolver factory (`src/annotations/resolver.js`) exposes alongside `sampleGroundHeight`, so the app has ONE depth-aware pick cascade rather than a second written beside it — which returns the height the pick landed on as well as the coordinate, so the live preview hangs on the roof or hillside under the pointer instead of sinking to sea level. The world renderer draws areas and routes with `clampToGround`, so the height is dropped at finish in one place (`finishSpec`) rather than being carried downstream and discarded silently: what you get is the outline you drew, lying on the surface beneath it.
|
- **A vertex is a lon/lat, and the mark is DRAPED.** Clicks are picked through `pickWorldFromScreen` — one of two scene helpers the annotation resolver factory (`src/annotations/resolver.js`) exposes alongside `sampleGroundHeight`, so the app has ONE depth-aware pick cascade rather than a second written beside it — which returns the height the pick landed on as well as the coordinate, so the live preview hangs on the roof or hillside under the pointer instead of sinking to sea level. The world renderer draws areas and routes with `clampToGround`, so the height is dropped at finish in one place (`finishSpec`) rather than being carried downstream and discarded silently: what you get is the outline you drew, lying on the surface beneath it.
|
||||||
- **Draped marks classify onto `ClassificationType.BOTH`** — terrain AND 3D tiles — not onto 3D tiles alone. With only `CESIUM_3D_TILE`, every area fill, area outline, route and arrow rendered to nothing on a keyless boot, where Cesium's own globe carries Esri/OSM imagery and there are no tiles to classify against; the labels appeared and the geometry did not. That hit spoken annotations exactly as hard as hand-drawn ones. `scripts/qa-draw-tool.mjs` counts the mark's own colour in the rendered frame before and after a shape is finished, on a keyed AND a keyless server, so "the board says there is a mark" can no longer pass for "the mark is on screen".
|
- **Draped marks classify onto `ClassificationType.BOTH`** — terrain AND 3D tiles — not onto 3D tiles alone. With only `CESIUM_3D_TILE`, every area fill, area outline, route and arrow rendered to nothing on a keyless boot, where Cesium's own globe carries Esri/OSM imagery and there are no tiles to classify against; the labels appeared and the geometry did not. That hit spoken annotations exactly as hard as hand-drawn ones. `scripts/qa-draw-tool.mjs` counts the mark's own colour in the rendered frame before and after a shape is finished, on a keyed AND a keyless server, so "the board says there is a mark" can no longer pass for "the mark is on screen".
|
||||||
- **A finished area is a CLOSED ring.** The fill polygon closes itself but the outline beside it is a polyline, so an open ring drew every edge except the one back to the first vertex — a triangle with two sides. `finishSpec` repeats the first position at the end. A line is never closed: joining its ends would invent a segment nobody drew.
|
- **A finished area is a CLOSED ring.** The fill polygon closes itself but the outline beside it is a polyline, so an open ring drew every edge except the one back to the first vertex — a triangle with two sides. `finishSpec` repeats the first position at the end. A line is never closed: joining its ends would invent a segment nobody drew.
|
||||||
|
- Drawn-line distance labels select metres or kilometres after rounding to the nearest metre, so a 999.5-metre line displays as `1.0 km` rather than `1000 m`.
|
||||||
- **Measurements and centres are computed on unwrapped longitudes** (`unwrapLongitudes` / `wrapLongitude` in `drawMode.js`, used by `ringAreaM2`, `ringCentroid` and the engine's manual-area anchor). A ~50,000 m² rectangle straddling the antimeridian reads its 0.002° width as 359.998° under raw arithmetic: it measured thousands of square kilometres and anchored on the Greenwich meridian, half a world from the shape. Path length was already safe — haversine is periodic in the longitude delta — and a test pins that so a "fix" cannot break it.
|
- **Measurements and centres are computed on unwrapped longitudes** (`unwrapLongitudes` / `wrapLongitude` in `drawMode.js`, used by `ringAreaM2`, `ringCentroid` and the engine's manual-area anchor). A ~50,000 m² rectangle straddling the antimeridian reads its 0.002° width as 359.998° under raw arithmetic: it measured thousands of square kilometres and anchored on the Greenwich meridian, half a world from the shape. Path length was already safe — haversine is periodic in the longitude delta — and a test pins that so a "fix" cannot break it.
|
||||||
- **Bounds.** One shape holds at most `MAX_VERTICES` = 512 points; two clicks within 0.5 m are one vertex (that is what makes a double-click finish without leaving a stray point); a vertex that is not a finite on-globe coordinate is refused. A shape that encloses nothing is refused rather than placed: three collinear points are not an area and a line under a metre long is not a line, and the hint says which.
|
- **Bounds.** One shape holds at most `MAX_VERTICES` = 512 points; two clicks within 0.5 m are one vertex (that is what makes a double-click finish without leaving a stray point); a vertex that is not a finite on-globe coordinate is refused. A shape that encloses nothing is refused rather than placed: three collinear points are not an area and a line under a metre long is not a line, and the hint says which.
|
||||||
- Pointer ownership: while a session is open the tool holds the single pointer claim (`claimPointer('draw')`, `src/data/inputOwnership.js`) and **every** ambient scene selection handler — the shared tracking click gesture behind flights, military and CCTV, plus vessels, satellites, launches, fires, installations, bikeshare, radio, submarine cables, the bundled GeoJSON layers and the CCTV calibration gizmo — returns before picking. A vertex over an aircraft is a vertex, not a selection.
|
- Pointer ownership: while a session is open the tool holds the single pointer claim (`claimPointer('draw')`, `src/data/inputOwnership.js`) and **every** ambient scene selection handler — the shared tracking click gesture behind flights, military and CCTV, plus vessels, satellites, launches, fires, installations, bikeshare, radio, submarine cables, the bundled GeoJSON layers and the CCTV calibration gizmo — returns before picking. A vertex over an aircraft is a vertex, not a selection.
|
||||||
@@ -4969,8 +5325,9 @@ pins and share links already supply complete boxes.
|
|||||||
- **The TRACKED contact's 2D↔3D handoff is DEFAULT behaviour (2026-08-19), driven by camera distance alone.** It does NOT consult the DISPLAY-rail `3D` toggle, which continues to own the FLEET (the un-instanced draw-call budget stays the operator's decision). Policy lives in `src/data/trackedModelRegime.js` and is shared by both layers: enter below `TRACKED_MODEL_ENTER_ALT_M` = 150,000 m and hand back to the billboard only above `TRACKED_MODEL_EXIT_ALT_M` = 172,500 m. **The swap distance was set by playtesting on 2026-08-20:** a first pass at 1,000,000 m switched too early; 2D reads correctly at ~600 km and the handoff belongs at ~150 km. **Consequence, recorded on purpose:** the tracked contact now enters 3D NEARER than the FLEET does (`MODEL_ALT_CEIL_M` = 800,000 m, unchanged), so with the DISPLAY-rail `3D` toggle on, 150–800 km draws surrounding contacts as models while the selected one is still a glyph. Nothing double-draws (the fleet pass skips the tracked icao) and aligning the two is a fleet-side decision, deliberately out of scope. **The two thresholds are asymmetric on purpose:** a single threshold makes a tracked orbit sitting ON the boundary strobe billboard↔model as the camera's altitude wobbles across it. Do not collapse them. The latch is scoped per selection, so a new target re-evaluates against the ENTER ceiling rather than inheriting the previous target's exit band. Exactly ONE model is involved; it loads on demand when the regime opens, is HIDDEN (not released) on regime exit so re-entry has no load gap, and is released by the existing teardown on deselect/re-track/destroy. Cockpit and TR-3B suppression are unchanged. **Two invariants around it:** (a) the hysteresis latch AND the load-failure latch are per-selection state cleared by `_resetTrackedSelectionState()` in the tracking lifecycle (deselect / re-track / cross-layer / init / destroy) — the predicate's icao-change guard is defence only, since it needs a drawn frame while nothing is selected and the render governor's idle mode does not promise one; (b) on-demand loading is bounded at 3 attempts per selection with a 1.5 s backoff and one console warning naming the asset — the driver runs every `scene.preUpdate`, so an unbounded catch means a missing GLB spins load→reject at frame rate. The billboard stays the visual throughout a failed load.
|
- **The TRACKED contact's 2D↔3D handoff is DEFAULT behaviour (2026-08-19), driven by camera distance alone.** It does NOT consult the DISPLAY-rail `3D` toggle, which continues to own the FLEET (the un-instanced draw-call budget stays the operator's decision). Policy lives in `src/data/trackedModelRegime.js` and is shared by both layers: enter below `TRACKED_MODEL_ENTER_ALT_M` = 150,000 m and hand back to the billboard only above `TRACKED_MODEL_EXIT_ALT_M` = 172,500 m. **The swap distance was set by playtesting on 2026-08-20:** a first pass at 1,000,000 m switched too early; 2D reads correctly at ~600 km and the handoff belongs at ~150 km. **Consequence, recorded on purpose:** the tracked contact now enters 3D NEARER than the FLEET does (`MODEL_ALT_CEIL_M` = 800,000 m, unchanged), so with the DISPLAY-rail `3D` toggle on, 150–800 km draws surrounding contacts as models while the selected one is still a glyph. Nothing double-draws (the fleet pass skips the tracked icao) and aligning the two is a fleet-side decision, deliberately out of scope. **The two thresholds are asymmetric on purpose:** a single threshold makes a tracked orbit sitting ON the boundary strobe billboard↔model as the camera's altitude wobbles across it. Do not collapse them. The latch is scoped per selection, so a new target re-evaluates against the ENTER ceiling rather than inheriting the previous target's exit band. Exactly ONE model is involved; it loads on demand when the regime opens, is HIDDEN (not released) on regime exit so re-entry has no load gap, and is released by the existing teardown on deselect/re-track/destroy. Cockpit and TR-3B suppression are unchanged. **Two invariants around it:** (a) the hysteresis latch AND the load-failure latch are per-selection state cleared by `_resetTrackedSelectionState()` in the tracking lifecycle (deselect / re-track / cross-layer / init / destroy) — the predicate's icao-change guard is defence only, since it needs a drawn frame while nothing is selected and the render governor's idle mode does not promise one; (b) on-demand loading is bounded at 3 attempts per selection with a 1.5 s backoff and one console warning naming the asset — the driver runs every `scene.preUpdate`, so an unbounded catch means a missing GLB spins load→reject at frame rate. The billboard stays the visual throughout a failed load.
|
||||||
- **Grounded 3D handoff is terrain-validity gated (2026-08-23).** A ready civilian or military glTF does not own the visual until `groundSnap` (`src/data/groundSnap.js`) can answer with a MEASURED photoreal-surface height. On success the layer writes `height + the model's measured belly offset` before revealing the model. Cache movement is measured on the WGS84 surface, not across altitude, so a stationary contact keeps its snap through poll-time vertical-datum changes. Model existence or GPU readiness alone never suppresses the billboard floor, and ordinary zoom/style/deselect transitions preserve a valid snap cache. **Two states, and the difference is the whole design.** COLD — nothing has ever resolved for this icao (tiles still streaming on first sight, sample failure, backoff after a first miss): there is no evidence of where the ground is, `heightFor` returns null, the model stays hidden and the depth-test-free 2D billboard remains opaque and floored. WARM — a snap resolved and then a >`MOVE_INVALIDATE_M` (50 m) taxi move stopped it answering directly: the measurement is DEMOTED to a bounded last-known rather than deleted, and it keeps answering while the resample is outstanding, so a taxiing aircraft does not pop 3D→2D→3D across a 2–30 s retry backoff. The bound is `HELD_SNAP_MAX_DRIFT_M` = 250 m from the spot the value was measured at, past which the hold is dropped rather than stretched and the contact is COLD again. It is spatial with no timer beside it (ground under a contact that has not moved does not change; what invalidates the value is the contact MOVING) and deliberately a quarter of the billboard chain's `HELD_FLOOR_MAX_DRIFT_KM` — that hold only ever RAISES a sprite, while a held snap IS the model's placement, so its error shows in both directions. A fresh sample releases the hold, and so does a ground flip (which already calls `forget`). **A loading model is HIDDEN, never zero-scaled:** admission sets `show = false` (Cesium's default is `true`, and an unplaced primitive would claim the visual at the identity matrix), and ownership is `ready && show` — Cesium 1.138's `Model.update` has no `show` guard, so hiding a primitive costs its load nothing.
|
- **Grounded 3D handoff is terrain-validity gated (2026-08-23).** A ready civilian or military glTF does not own the visual until `groundSnap` (`src/data/groundSnap.js`) can answer with a MEASURED photoreal-surface height. On success the layer writes `height + the model's measured belly offset` before revealing the model. Cache movement is measured on the WGS84 surface, not across altitude, so a stationary contact keeps its snap through poll-time vertical-datum changes. Model existence or GPU readiness alone never suppresses the billboard floor, and ordinary zoom/style/deselect transitions preserve a valid snap cache. **Two states, and the difference is the whole design.** COLD — nothing has ever resolved for this icao (tiles still streaming on first sight, sample failure, backoff after a first miss): there is no evidence of where the ground is, `heightFor` returns null, the model stays hidden and the depth-test-free 2D billboard remains opaque and floored. WARM — a snap resolved and then a >`MOVE_INVALIDATE_M` (50 m) taxi move stopped it answering directly: the measurement is DEMOTED to a bounded last-known rather than deleted, and it keeps answering while the resample is outstanding, so a taxiing aircraft does not pop 3D→2D→3D across a 2–30 s retry backoff. The bound is `HELD_SNAP_MAX_DRIFT_M` = 250 m from the spot the value was measured at, past which the hold is dropped rather than stretched and the contact is COLD again. It is spatial with no timer beside it (ground under a contact that has not moved does not change; what invalidates the value is the contact MOVING) and deliberately a quarter of the billboard chain's `HELD_FLOOR_MAX_DRIFT_KM` — that hold only ever RAISES a sprite, while a held snap IS the model's placement, so its error shows in both directions. A fresh sample releases the hold, and so does a ground flip (which already calls `forget`). **A loading model is HIDDEN, never zero-scaled:** admission sets `show = false` (Cesium's default is `true`, and an unplaced primitive would claim the visual at the identity matrix), and ownership is `ready && show` — Cesium 1.138's `Model.update` has no `show` guard, so hiding a primitive costs its load nothing.
|
||||||
- Commercial and military aircraft use the same high-level FLEET model regime: 2D billboards when zoomed out, optional glTF models when closer, controlled by the DISPLAY rail `3D` toggle and `Proximity` / `All` modes. Since 2026-08-16 (Hangar fleet) models are PER-CLASS: real CC-BY GLBs for light/bizjet/turboprop/widebody/helicopter/uav (`CLASS_MODEL_REAL` in `src/data/aircraftClass.js` — meters-baked, scale 1, per-model belly/radius; provenance in `public/models/README.md`), the shared `airplane.glb` for the remaining civilian classes, and the military layer maps weight classes (real GLBs / 747 heavies / `jet.glb` fastjets, per-model heading offsets, flat amber or emergency red). Textured civilians carry a HEAVY tint, not a light one: `MODEL_COLOR_BLEND_AMOUNT` is `0.94` in BOTH layers under Cesium's `ColorBlendMode.MIX`, so the class colour supplies 94% of the surface and the asset's own texture ~6%. The visual direction is clean light silhouettes with only a weak diffuse contribution from the approved textures, so liveries deliberately do NOT read. IR boost raises the blend to a full `1.0`. Under NVG/FLIR (map preset or Cockpit vision) models render unlit flat-white at full alpha and scene fog is disabled (fog otherwise blacks out distant models with the globe hidden); state restores on exit.
|
- Commercial and military aircraft use the same high-level FLEET model regime: 2D billboards when zoomed out, optional glTF models when closer, controlled by the DISPLAY rail `3D` toggle and `Proximity` / `All` modes. Since 2026-08-16 (Hangar fleet) models are PER-CLASS: real CC-BY GLBs for light/bizjet/turboprop/widebody/helicopter/uav (`CLASS_MODEL_REAL` in `src/data/aircraftClass.js` — meters-baked, scale 1, per-model belly/radius; provenance in `public/models/README.md`), the shared `airplane.glb` for the remaining civilian classes, and the military layer maps weight classes (real GLBs / 747 heavies / `jet.glb` fastjets, per-model heading offsets, flat amber or emergency red). Textured civilians carry a HEAVY tint, not a light one: `MODEL_COLOR_BLEND_AMOUNT` is `0.94` in BOTH layers under Cesium's `ColorBlendMode.MIX`, so the class colour supplies 94% of the surface and the asset's own texture ~6%. The visual direction is clean light silhouettes with only a weak diffuse contribution from the approved textures, so liveries deliberately do NOT read. IR boost raises the blend to a full `1.0`. Under NVG/FLIR (map preset or Cockpit vision) models render unlit flat-white at full alpha and scene fog is disabled (fog otherwise blacks out distant models with the globe hidden); state restores on exit.
|
||||||
|
- **Aircraft models skip Cesium's dynamic environment map (2026-10-24).** Flights, Military Flights and Local ADS-B pass `environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS` (`{ enabled: false }`, `src/data/aircraftModelLighting.js`) to every drawn model. Otherwise each `Model` renders the sky around it and reads it back from the GPU (`readPixels`) when it is first placed, and again whenever it moves more than 1 km, the scene's atmosphere settings change, or (under sunlight dynamic lighting) an hour of scene time passes (`DynamicEnvironmentMapManager`, Cesium 1.138). With the class colour supplying 94% of the surface, the models are lit from Cesium's default spherical harmonics instead, with no specular environment map, so the sky's model-lighting cube map no longer reaches aircraft. `src/data/aircraftModelLighting.test.mjs` pins the option at every drawn model call; the two preload calls only warm the glTF cache and are never drawn.
|
||||||
- The DISPLAY-rail `3D` toggle is the user-facing activation path for both aircraft layers. Their small approved GLBs build their render resources without Cesium's frame-spread job queue, preventing continuous Photorealistic 3D Tiles streaming from starving model readiness; model caps, tracking, camera, and fallback billboards are unchanged.
|
- The DISPLAY-rail `3D` toggle is the user-facing activation path for both aircraft layers. Their small approved GLBs build their render resources without Cesium's frame-spread job queue, preventing continuous Photorealistic 3D Tiles streaming from starving model readiness; model caps, tracking, camera, and fallback billboards are unchanged.
|
||||||
- **The `3D` toggle DEFAULTS ON in `proximity` on a first run.** Proximity is itself the budget — models appear only below `MODEL_ALT_CEIL_M` and only for the nearest `MODEL_MAX` in view — so the default costs nothing at globe scale, and `all` remains a deliberate opt-in. A fresh boot runs NO layer-state restoration (`LayerStateCoordinator.start()` returns early with neither a share payload nor stored state), so four independent initializers decide what a first-run operator sees and must agree: `booleanOption('models3d', 'e', true)` in `src/data/layerState.js`, `_models3dEnabled = true` in BOTH flight layers, `this._models3dEnabled = true` in `src/ui.js`, and the `active` / `visible` classes on `#models3d-toggle` / `#models3d-mode-row` in `index.html`. All four are pinned together in `src/data/layerState.test.mjs`. Explicit state still wins: because the codec omits default-valued options, `models3d: false` is now what travels in a link (`lo=…f.e.0`) and restores OFF at both aircraft layers. **Consequence for returning users:** a stored `gev:layer-state:v2` blob is a FULL options snapshot, so a session that wrote one before this change carries `models3d:false` and keeps 3D off until the operator flips it (or clears the key) — the durable snapshot is treated as the recipient's own state, by design.
|
- **The `3D` toggle DEFAULTS ON in `proximity` on a first run.** Proximity is itself the budget — models appear only below `MODEL_ALT_CEIL_M` and only for the nearest `MODEL_MAX` in view — so the default costs nothing at globe scale, and `all` remains a deliberate opt-in. A fresh boot runs NO layer-state restoration (`LayerStateCoordinator.start()` returns early with neither a share payload nor stored state), so four independent initializers decide what a first-run operator sees and must agree: `booleanOption('models3d', 'e', true)` in `src/data/layerState.js`, `_models3dEnabled = true` in BOTH flight layers, `this._models3dEnabled = true` in `src/ui/aircraftDisplay.js`, and the `active` / `visible` classes on `#models3d-toggle` / `#models3d-mode-row` in `index.html`. All four are pinned together in `src/data/layerState.test.mjs`. Explicit state still wins: because the codec omits default-valued options, `models3d: false` is now what travels in a link (`lo=…f.e.0`) and restores OFF at both aircraft layers. **Consequence for returning users:** a stored `gev:layer-state:v2` blob is a FULL options snapshot, so a session that wrote one before this change carries `models3d:false` and keeps 3D off until the operator flips it (or clears the key) — the durable snapshot is treated as the recipient's own state, by design.
|
||||||
- **Consequence of the flip on the recorded tracked/fleet inversion:** the 150–800 km band where surrounding contacts draw as models while the SELECTED one is still a glyph is now what an operator sees WITHOUT arming anything. The inversion itself is unchanged and still deliberate (see `src/data/trackedModelRegime.js`); only its reachability changed.
|
- **Consequence of the flip on the recorded tracked/fleet inversion:** the 150–800 km band where surrounding contacts draw as models while the SELECTED one is still a glyph is now what an operator sees WITHOUT arming anything. The inversion itself is unchanged and still deliberate (see `src/data/trackedModelRegime.js`); only its reachability changed.
|
||||||
- Civilian and military 2D aircraft use the established distance scale: `3×` near
|
- Civilian and military 2D aircraft use the established distance scale: `3×` near
|
||||||
the camera and a `0.5×` floor from 8,000 km outward. A standard 20 px ambient
|
the camera and a `0.5×` floor from 8,000 km outward. A standard 20 px ambient
|
||||||
@@ -4980,7 +5337,7 @@ pins and share links already supply complete boxes.
|
|||||||
- Fleet model eligibility is distance-based with on-screen priority and hard caps (`MODEL_MAX`, `MODEL_MAX_ALL`) to avoid draw-call explosions. Each model owns its own `modelMatrix`; shared scratch matrices are forbidden because they caused stacking/flicker.
|
- Fleet model eligibility is distance-based with on-screen priority and hard caps (`MODEL_MAX`, `MODEL_MAX_ALL`) to avoid draw-call explosions. Each model owns its own `modelMatrix`; shared scratch matrices are forbidden because they caused stacking/flicker.
|
||||||
- Tracked aircraft use standalone model primitives driven from the already-settled dead-reckoned display position, while the tracked Cesium entity remains billboard-backed so `viewer.trackedEntity` always has a ready bounding sphere.
|
- Tracked aircraft use standalone model primitives driven from the already-settled dead-reckoned display position, while the tracked Cesium entity remains billboard-backed so `viewer.trackedEntity` always has a ready bounding sphere.
|
||||||
- Flights and military layers mirror the same tracking invariants: no warm-up freeze/jump, altitude-scaled framing, trail head glued to the displayed plane, no pull-out when switching targets, and no cross-layer orphan when switching between commercial and military tracks.
|
- Flights and military layers mirror the same tracking invariants: no warm-up freeze/jump, altitude-scaled framing, trail head glued to the displayed plane, no pull-out when switching targets, and no cross-layer orphan when switching between commercial and military tracks.
|
||||||
- Regression surface: `npm run test:track` drives the real app headless with synthetic aircraft feeds and asserts the tracking invariants without depending on live OpenSky/adsb data. `src/data/trackedModelRegime.test.mjs` pins the tracked contact's threshold math, the enter/exit asymmetry, and the default-on / cockpit / TR-3B / deselect wiring in both layers.
|
- Regression surface: `npm run test:track` drives the real app headless with synthetic aircraft feeds and asserts the tracking invariants without depending on live OpenSky/adsb data. Arrival-rotation checks observe the first reveal within a bounded wall-clock window before the one-second idle fallback, rather than assuming the 80 ms fleet cadence fits inside four rendered frames; an incorrect first orientation fails immediately. `src/data/trackedModelRegime.test.mjs` pins the tracked contact's threshold math, the enter/exit asymmetry, and the default-on / cockpit / TR-3B / deselect wiring in both layers.
|
||||||
|
|
||||||
### TR-3B conversion Easter egg (August 2026)
|
### TR-3B conversion Easter egg (August 2026)
|
||||||
|
|
||||||
@@ -5055,7 +5412,7 @@ pins and share links already supply complete boxes.
|
|||||||
- `/api/overpass` is bounded by body/response caps, per-client/global rate limits, concurrency limits, operator-configured upstreams, in-flight dedupe, cache bounds, and static validation that every selector is spatially bounded.
|
- `/api/overpass` is bounded by body/response caps, per-client/global rate limits, concurrency limits, operator-configured upstreams, in-flight dedupe, cache bounds, and static validation that every selector is spatially bounded.
|
||||||
- `/api/military-installations` uses an independent limiter with the same 90-per-client/300-global one-minute bounds, so viewport installation refreshes never consume `/api/overpass` annotation capacity.
|
- `/api/military-installations` uses an independent limiter with the same 90-per-client/300-global one-minute bounds, so viewport installation refreshes never consume `/api/overpass` annotation capacity.
|
||||||
- `/api/route` proxies bounded OSRM route requests for annotation routes, with profile allowlisting, distance caps, response caps, caching, and sanitized "no route found" errors.
|
- `/api/route` proxies bounded OSRM route requests for annotation routes, with profile allowlisting, distance caps, response caps, caching, and sanitized "no route found" errors.
|
||||||
- Track endpoints: `/api/vessels/track?mmsi=` (server-accumulated ring buffers; sub-route handled before the rows snapshot), `/api/flights/track?icao24=` (OAuth, 60s cache, sanitized errors, independent OpenSky credit bucket), `/api/military/track?hex=` (60s cache, 5MB cap, ODbL attribution required in UI).
|
- Track endpoints: `/api/vessels/track?mmsi=` (server-accumulated ring buffers; sub-route handled before the rows snapshot), `/api/flights/track?icao24=` (OAuth, 60s cache, sanitized errors, independent OpenSky credit bucket), `/api/military/track?hex=` (400 for an invalid hex, otherwise `503 history_unavailable` with `Retry-After: 1800` and `Cache-Control: no-store`, no upstream request; military tracking uses its locally accumulated trail).
|
||||||
- Realtime debug logs redact API keys, bearer tokens, client secrets, and image data URLs before writing to disk; request bodies are size-capped.
|
- Realtime debug logs redact API keys, bearer tokens, client secrets, and image data URLs before writing to disk; request bodies are size-capped.
|
||||||
- `/api/realtime/debug-log` enforces an always-on 120-per-client/400-global one-minute limiter (not the opt-in `GEV_RATELIMIT_OPENAI_PER_MIN` bucket the cost-bearing OpenAI routes share), appends asynchronously through a serialized queue, and rotates `realtime-conversations.jsonl` at 32 MB keeping one prior generation, so the sink is bounded at twice that regardless of session length. A malformed record answers 400 and a failed write 500, both with a fixed message.
|
- `/api/realtime/debug-log` enforces an always-on 120-per-client/400-global one-minute limiter (not the opt-in `GEV_RATELIMIT_OPENAI_PER_MIN` bucket the cost-bearing OpenAI routes share), appends asynchronously through a serialized queue, and rotates `realtime-conversations.jsonl` at 32 MB keeping one prior generation, so the sink is bounded at twice that regardless of session length. A malformed record answers 400 and a failed write 500, both with a fixed message.
|
||||||
|
|
||||||
@@ -5142,7 +5499,8 @@ pins and share links already supply complete boxes.
|
|||||||
- **Cockpit left-panel clearance:** the Cockpit Contact card and peripheral HUD participate in the adaptive left accordion's live obstacle measurements, including live viewport-height changes. Expanding Layers or Scenes keeps the active panel in the available upper-left corridor with internal scrolling; it does not cover the Contact card, lower Cockpit controls, or Cesium credit line. Outside Cockpit the hidden card does not alter the normal corridor.
|
- **Cockpit left-panel clearance:** the Cockpit Contact card and peripheral HUD participate in the adaptive left accordion's live obstacle measurements, including live viewport-height changes. Expanding Layers or Scenes keeps the active panel in the available upper-left corridor with internal scrolling; it does not cover the Contact card, lower Cockpit controls, or Cesium credit line. Outside Cockpit the hidden card does not alter the normal corridor.
|
||||||
- **Cockpit Context scope:** the 250 km radius applies to the air/sea proximity cohorts. Installation counts come only from the currently loaded viewport and are labeled `CURRENT VIEWPORT ONLY` in the cockpit as well as the normal Context panel; neither surface presents them as a complete 250 km installation survey.
|
- **Cockpit Context scope:** the 250 km radius applies to the air/sea proximity cohorts. Installation counts come only from the currently loaded viewport and are labeled `CURRENT VIEWPORT ONLY` in the cockpit as well as the normal Context panel; neither surface presents them as a complete 250 km installation survey.
|
||||||
- **Cockpit camera anchor:** first-person mode does not write feed-boundary corrections directly into the camera. A cockpit-only inertial anchor advances from the selected aircraft's displayed course and speed, then converges toward the authoritative delayed track with correction capped below forward motion. The displayed kinematics are derived from the same consecutive fix segment as the rendered position, with raw feed speed/course used only as fallback; a transient zero/missing feed speed therefore cannot freeze a visibly moving aircraft after layer enable or a map/cockpit handoff. Rendered altitude continues to come from that interpolated track position. Late ADS-B fixes and short render stalls can remove drift without accelerating or reversing the view. Camera placement runs before scene update/culling at a bounded 20 Hz so a moving cockpit does not force Photoreal 3D Tiles to retraverse on every display frame; textual instruments update at 10 Hz and context/layout work at 4 Hz. Every far Cockpit contact pip shares one stable Cesium texture-atlas entry and skips unused screen-projected course calculations, while only in-range 2D aircraft silhouettes pay the screen-projected rotation cost; ambient glTF collections are hidden/retained rather than synchronously destroyed at cockpit entry, and context rails lay out only on explicit content/state changes and viewport resize. The deliberate 15/30-second layer interpolation delays and per-Cesium-frame position caches remain unchanged.
|
- **Cockpit camera anchor:** first-person mode does not write feed-boundary corrections directly into the camera. A cockpit-only inertial anchor advances from the selected aircraft's displayed course and speed, then converges toward the authoritative delayed track with correction capped below forward motion. The displayed kinematics are derived from the same consecutive fix segment as the rendered position, with raw feed speed/course used only as fallback; a transient zero/missing feed speed therefore cannot freeze a visibly moving aircraft after layer enable or a map/cockpit handoff. Rendered altitude continues to come from that interpolated track position. Late ADS-B fixes and short render stalls can remove drift without accelerating or reversing the view. Camera placement runs before scene update/culling at a bounded 20 Hz so a moving cockpit does not force Photoreal 3D Tiles to retraverse on every display frame; textual instruments update at 10 Hz and context/layout work at 4 Hz. Every far Cockpit contact pip shares one stable Cesium texture-atlas entry and skips unused screen-projected course calculations, while only in-range 2D aircraft silhouettes pay the screen-projected rotation cost; ambient glTF collections are hidden/retained rather than synchronously destroyed at cockpit entry, and context rails lay out only on explicit content/state changes and viewport resize. The deliberate 15/30-second layer interpolation delays and per-Cesium-frame position caches remain unchanged.
|
||||||
- **Cockpit route, vision, and view controls:** visible on-screen `COCKPIT`, `RESET`, and `EXIT COCKPIT` controls replace reliance on the `C` shortcut. RESET uses the same canonical globe route as the map and voice actions, exits Cockpit, and releases its camera ownership rather than exposing the hidden map-style top action. When the tracked commercial flight has a plausible ADSBDB route, the top of the right briefing rail shows a compact `FROM → TO` airport strip and the visor shows a centered estimated-destination chevron with its relative bearing; absent or implausible route data hides the strip and cue rather than guessing. The cockpit-local vision control is an interactive `PREV / CURRENT / NEXT` carousel over `NORMAL`, `CRT`, `NVG`, `FLIR`, `ANIME`, `NOIR`, and `SNOW`; its previous/next actions wrap, and activating the current value advances to the next style. Cockpit enters on the active map preset, so a Cyber FLIR map opens Cockpit on FLIR while Normal remains a distinct option instead of producing a second FLIR entry. Cockpit choices temporarily activate the existing Cesium post-process stages, while exiting Cockpit through either EXIT or RESET restores the pre-entry map style. The regional-news page uses a free Google News RSS locality query first, with the existing GDELT query retained only as a fail-soft fallback; linked headlines remain reporting, not verified incidents or risk intelligence.
|
- **Cockpit route, vision, and view controls:** visible on-screen `COCKPIT`, `RESET`, and `EXIT COCKPIT` controls replace reliance on the `C` shortcut. RESET uses the same canonical globe route as the map and voice actions, exits Cockpit, and releases its camera ownership rather than exposing the hidden map-style top action. When the tracked commercial flight has a plausible ADSBDB route, the top of the right briefing rail shows a compact `FROM → TO` airport strip and the visor shows a centered estimated-destination chevron with its relative bearing; its destination label rounds to whole degrees in 000–359, wrapping rounded north to 000° while the chevron continues to use the relative bearing independently of label rounding; absent or implausible route data hides the strip and cue rather than guessing. The cockpit-local vision control is an interactive `PREV / CURRENT / NEXT` carousel over `NORMAL`, `CRT`, `NVG`, `FLIR`, `ANIME`, `NOIR`, and `SNOW`; its previous/next actions wrap, and activating the current value advances to the next style. Cockpit enters on the active map preset, so a Cyber FLIR map opens Cockpit on FLIR while Normal remains a distinct option instead of producing a second FLIR entry. Cockpit choices temporarily activate the existing Cesium post-process stages, while exiting Cockpit through either EXIT or RESET restores the pre-entry map style. The regional-news page uses a free Google News RSS locality query first, with the existing GDELT query retained only as a fail-soft fallback; linked headlines remain reporting, not verified incidents or risk intelligence.
|
||||||
|
- Cockpit wind-direction labels wrap after rounding to whole degrees, so a direction of 359.5° displays as `N · 0°` rather than `N · 360°`.
|
||||||
- **Cockpit missing observations:** missing local weather observations remain unknown rather than being converted to zero or clear skies, in the briefing and in the cloud effects; a missing temperature drops the whole local weather record, including its wind and condition.
|
- **Cockpit missing observations:** missing local weather observations remain unknown rather than being converted to zero or clear skies, in the briefing and in the cloud effects; a missing temperature drops the whole local weather record, including its wind and condition.
|
||||||
- **Cockpit weather status:** the earlier multi-canvas atmospheric compositor remains fail-closed and is not attached to the live viewer. Cockpit clouds are a separate transparent WebGL pass with a capped 520×320 framebuffer, 24 ray steps, three FBM octaves, and a 12 FPS ceiling. It defaults off and starts only when local storage explicitly contains the persisted `WX ON` opt-in (`'1'`). When opted in, observations refresh after five minutes or 25 km of aircraft movement, fail transparent when unavailable or clear, and stop on exit or disable. `WX OFF` governs atmospheric rendering only: the briefing still fetches source-backed Natural Earth region, headline, and Open-Meteo local-information data, aborting and replacing any in-flight request when the selected aircraft changes. No weather effect runs in map mode and no synthetic fallback is shown.
|
- **Cockpit weather status:** the earlier multi-canvas atmospheric compositor remains fail-closed and is not attached to the live viewer. Cockpit clouds are a separate transparent WebGL pass with a capped 520×320 framebuffer, 24 ray steps, three FBM octaves, and a 12 FPS ceiling. It defaults off and starts only when local storage explicitly contains the persisted `WX ON` opt-in (`'1'`). When opted in, observations refresh after five minutes or 25 km of aircraft movement, fail transparent when unavailable or clear, and stop on exit or disable. `WX OFF` governs atmospheric rendering only: the briefing still fetches source-backed Natural Earth region, headline, and Open-Meteo local-information data, aborting and replacing any in-flight request when the selected aircraft changes. No weather effect runs in map mode and no synthetic fallback is shown.
|
||||||
- **Cockpit trail visibility:** entering cockpit hides the selected aircraft's trail body and head so they cannot cross the first-person view; exit restores them. This cockpit-only presentation change does not alter the normal map-mode invariant that aircraft trails render through terrain using their depth-fail material.
|
- **Cockpit trail visibility:** entering cockpit hides the selected aircraft's trail body and head so they cannot cross the first-person view; exit restores them. This cockpit-only presentation change does not alter the normal map-mode invariant that aircraft trails render through terrain using their depth-fail material.
|
||||||
@@ -5162,10 +5520,11 @@ the `_detectionUserOverridden` / explicit-intent flags that would suppress a
|
|||||||
separate landed behaviour. Pinned in `src/reasonableDefaults.test.mjs` (feather,
|
separate landed behaviour. Pinned in `src/reasonableDefaults.test.mjs` (feather,
|
||||||
detection, OUTSIDE opacity) and `src/data/layerState.test.mjs` (3D).
|
detection, OUTSIDE opacity) and `src/data/layerState.test.mjs` (3D).
|
||||||
|
|
||||||
**The 2026-08-24 defaults** (superseding the interim 08-23 values of 8%/3%):
|
**Current defaults** (scope feather revised to 100% on 2026-10-25;
|
||||||
|
detection and outside opacity retain the 2026-08-24 defaults):
|
||||||
the first-run look is Detection DENSE
|
the first-run look is Detection DENSE
|
||||||
`75%`, ELASTIC allocation, Fade `7%`, OUTSIDE opacity `1%`, scope feather `11%`,
|
`75%`, ELASTIC allocation, Fade `7%`, OUTSIDE opacity `1%`, Scope ON with feather
|
||||||
and 3D fleet mode PROXIMITY, with `AIRCRAFT_BRACKET_FLOOR_ANCHOR` at `0.01` so
|
`100%`, and 3D fleet mode PROXIMITY, with `AIRCRAFT_BRACKET_FLOOR_ANCHOR` at `0.01` so
|
||||||
brackets keep their approved brightness exactly at the new OUTSIDE default. Two
|
brackets keep their approved brightness exactly at the new OUTSIDE default. Two
|
||||||
terms that must never be conflated: scope FEATHER softens the black scope-mask
|
terms that must never be conflated: scope FEATHER softens the black scope-mask
|
||||||
edge; detection FADE is the label/card fading band around the keyhole. The
|
edge; detection FADE is the label/card fading band around the keyhole. The
|
||||||
@@ -5179,7 +5538,7 @@ demand with square-root demand scaling and semantic layer weights
|
|||||||
(`labelArbiter.js:181`). First-run default: ELASTIC.
|
(`labelArbiter.js:181`). First-run default: ELASTIC.
|
||||||
|
|
||||||
**A default has THREE surfaces, and a PARSE fallback that is not one of them.**
|
**A default has THREE surfaces, and a PARSE fallback that is not one of them.**
|
||||||
The value literals (engine constant, markup value, markup readout, `ui.js`
|
The value literals (engine constant, markup value, markup readout, `src/ui/visualPresets.js`
|
||||||
`GLOBAL_POST_DEFAULTS`, the share generator's starting state) must all move
|
`GLOBAL_POST_DEFAULTS`, the share generator's starting state) must all move
|
||||||
together, because a fresh boot runs no restore and those literals ARE the startup
|
together, because a fresh boot runs no restore and those literals ARE the startup
|
||||||
state. The `scf` / `ko` PARSE fallbacks deliberately do NOT move: they answer
|
state. The `scf` / `ko` PARSE fallbacks deliberately do NOT move: they answer
|
||||||
@@ -5227,7 +5586,7 @@ easier to meet (detection is now on more often), but does not create it.
|
|||||||
- **Detection OUTSIDE opacity: `1%`** (moved `5% → 3% → 1%` on 2026-08-24).
|
- **Detection OUTSIDE opacity: `1%`** (moved `5% → 3% → 1%` on 2026-08-24).
|
||||||
`KEYHOLE_OUTSIDE_OPACITY_DEFAULT` in `src/celestialRing.js`, mirrored by
|
`KEYHOLE_OUTSIDE_OPACITY_DEFAULT` in `src/celestialRing.js`, mirrored by
|
||||||
`#detection-opacity-slider`'s markup value AND readout,
|
`#detection-opacity-slider`'s markup value AND readout,
|
||||||
`GLOBAL_POST_DEFAULTS.detectionOutsideOpacityPct` in `ui.js`, and
|
`GLOBAL_POST_DEFAULTS.detectionOutsideOpacityPct` in `src/ui/visualPresets.js`, and
|
||||||
`_detectionOutsideOpacityPct` in `sharelink.js`. The slider's `step` is now
|
`_detectionOutsideOpacityPct` in `sharelink.js`. The slider's `step` is now
|
||||||
`1`, so 1–4 % are reachable at all (at the previous step of 5 the entire
|
`1`, so 1–4 % are reachable at all (at the previous step of 5 the entire
|
||||||
sub-default range was one stop wide). `AIRCRAFT_BRACKET_FLOOR_ANCHOR` in
|
sub-default range was one stop wide). `AIRCRAFT_BRACKET_FLOOR_ANCHOR` in
|
||||||
@@ -5235,11 +5594,12 @@ easier to meet (detection is now on more often), but does not create it.
|
|||||||
calibrated so `AIRCRAFT_BRACKET_ALPHA_FLOOR` (0.35) lands exactly at the
|
calibrated so `AIRCRAFT_BRACKET_ALPHA_FLOOR` (0.35) lands exactly at the
|
||||||
default, and the mapping follows bracket brightness rather than slider
|
default, and the mapping follows bracket brightness rather than slider
|
||||||
position. The `ko` PARSE fallback stays at `5`.
|
position. The `ko` PARSE fallback stays at `5`.
|
||||||
- **Scope feather: `11%`** — a soft scope-mask edge (moved `0% → 8% → 11%`; `0%` hard crop for one day,
|
- **Scope: `ON`, feather: `100%`** — a broad, soft scope-mask edge (revised on
|
||||||
`35%` before that). `SCOPE_FEATHER_RATIO_DEFAULT` in `src/scopeMask.js`,
|
2026-10-25 from the prior `11%`; earlier defaults were `0%`, `8%` and `35%`).
|
||||||
|
`SCOPE_FEATHER_RATIO_DEFAULT` in `src/scopeMask.js`,
|
||||||
mirrored by `#scope-feather-slider`'s markup value AND readout and
|
mirrored by `#scope-feather-slider`'s markup value AND readout and
|
||||||
`_scopeFeatherPct` in `sharelink.js`. The slider is untouched and still spans
|
`_scopeFeatherPct` in `sharelink.js`. The slider spans
|
||||||
0–100, and an explicit `0` is still the hard-crop path — pinned, so moving the
|
0–200, and an explicit `0` is still the hard-crop path — pinned, so moving the
|
||||||
default cannot quietly delete it. The `scf` PARSE fallback deliberately stays
|
default cannot quietly delete it. The `scf` PARSE fallback deliberately stays
|
||||||
at `35`: a link predating `scf` was authored when 35 was what its author saw,
|
at `35`: a link predating `scf` was authored when 35 was what its author saw,
|
||||||
and restoring the author's view is what a share link is for.
|
and restoring the author's view is what a share link is for.
|
||||||
@@ -5270,6 +5630,11 @@ easier to meet (detection is now on more often), but does not create it.
|
|||||||
short-way-round longitude span and wraps the padded edges, so a dateline-
|
short-way-round longitude span and wraps the padded edges, so a dateline-
|
||||||
crossing box stays its true width. Raw subtraction inflated a 0.41° metro box
|
crossing box stays its true width. Raw subtraction inflated a 0.41° metro box
|
||||||
to 86.7° and a 60° territory to 132°.
|
to 86.7° and a 60° territory to 132°.
|
||||||
|
- **Point distances take the short way round 180°.** The annotation
|
||||||
|
resolver's `approximateDistanceM` (voice "point out" proximity checks) and
|
||||||
|
`places_nearby` distances wrap the longitude difference. Raw subtraction put
|
||||||
|
Savusavu 38,148 km from a camera over the Lau Islands, so the proximity check
|
||||||
|
refused it, and sorted a place 2 km across 180° last.
|
||||||
- CCTV calibration persists at `godsEyeView.cctv.calibration.v2` (v2 rebuild;
|
- CCTV calibration persists at `godsEyeView.cctv.calibration.v2` (v2 rebuild;
|
||||||
the store was wiped clean, no import from the old `v1` key).
|
the store was wiped clean, no import from the old `v1` key).
|
||||||
- CCTV v3 floor QA pins are: zero samples for heading-only edits; zero transient
|
- CCTV v3 floor QA pins are: zero samples for heading-only edits; zero transient
|
||||||
@@ -5660,6 +6025,8 @@ callsign, class and category, adsbdb operator/type and a plausible route when
|
|||||||
known, altitude, ground speed, track, vertical rate, position age, message
|
known, altitude, ground speed, track, vertical rate, position age, message
|
||||||
count, "Heard by your receiver"); markers are not camera-followed. Positioned
|
count, "Heard by your receiver"); markers are not camera-followed. Positioned
|
||||||
aircraft join detection boxes, labelled only with a decoded callsign.
|
aircraft join detection boxes, labelled only with a decoded callsign.
|
||||||
|
The card's `TRK` label wraps after rounding to whole degrees, so a track of
|
||||||
|
359.5° displays as `TRK 0°` rather than `TRK 360°`.
|
||||||
|
|
||||||
Receivers publish one record shape from `src/sources/adsbRecords.js`
|
Receivers publish one record shape from `src/sources/adsbRecords.js`
|
||||||
(`icao`, `callsign`, `category`, `onGround`, `lat`, `lon`, `altitudeFt`,
|
(`icao`, `callsign`, `category`, `onGround`, `lat`, `lon`, `altitudeFt`,
|
||||||
@@ -5766,7 +6133,7 @@ closed. Performance and live agency reliability require recorded validation.
|
|||||||
|
|
||||||
## Wind forecast models
|
## Wind forecast models
|
||||||
|
|
||||||
Wind supports selectable GFS/IFS models with persisted model choice. Each fetch chooses the available cycle and forecast step nearest the current time; rows show both issue and valid UTC timestamps. Grid identities include model, cycle, forecast step and any requested scalar field. The six model/field cache slots each retain the last two issued grids for manifest/grid rollover, coalesce concurrent requests, retain last-good data on failures and retry no faster than once per minute. Model changes clear the old field immediately, cancel old acquisition and ignore superseded results.
|
Wind supports selectable GFS/IFS models with persisted model choice. Each fetch chooses the available cycle and forecast step nearest the current time; rows show both issue and valid UTC timestamps. Grid identities include model, cycle, forecast step and any requested scalar field. The six model/field cache slots each retain the last two issued grids for manifest/grid rollover, coalesce concurrent requests, retain last-good data on failures and retry no faster than once per minute. Model changes clear the old field immediately, cancel old acquisition and ignore superseded results. Last-good data is marked stale; the wind row shows it, and `get_wind` returns `stale` and adds "(forecast may be stale)" to its summary.
|
||||||
|
|
||||||
### Local Weather review candidate
|
### Local Weather review candidate
|
||||||
|
|
||||||
@@ -5851,3 +6218,20 @@ datacenters, dams and the Nepal locator retain that shared credit while displaye
|
|||||||
|
|
||||||
Military fragments stitch by source identity. Different mapped parcels merge only
|
Military fragments stitch by source identity. Different mapped parcels merge only
|
||||||
when their footprints touch or contain one another, never across a seam tolerance gap.
|
when their footprints touch or contain one another, never across a seam tolerance gap.
|
||||||
|
|
||||||
|
### Shared native dropdowns and numeric fields
|
||||||
|
|
||||||
|
- Scene and CCTV selects share the default `gev-field` surface. HUD layout and
|
||||||
|
draw colour retain compact density. Radio filter, tuner gain and FM frequency
|
||||||
|
use the same base while retaining their feature-specific skins and layout.
|
||||||
|
- The CCTV inline number editor uses `createNumberInput` with its existing
|
||||||
|
precision and inline density. Enter/blur commit, Escape cancellation, frozen
|
||||||
|
camera base, camera-switch guards and disposal remain with CCTV controls.
|
||||||
|
- Options, values, native keyboard/typeahead, disabled attributes and browser
|
||||||
|
validity remain native. Frequency bounds, tuner commit timing and calibration
|
||||||
|
parsing are unchanged. Specialized tuner graphics and calibration chips
|
||||||
|
retain their own controls. Provider settings and scene-authoring dialogs are
|
||||||
|
outside this change.
|
||||||
|
- `scripts/qa-form-fields.mjs` exercises native selections, number validity,
|
||||||
|
calibration commit/cancel and stale-camera/disposal guards against an installed
|
||||||
|
controller with fixture ports, plus default/Cyber and narrow translated layouts.
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# Translating God's Eye View
|
# Translating God's Eye View
|
||||||
|
|
||||||
Shipped languages: **English** (source and fallback), **Spanish** (`es`) and **Russian** (`ru`).
|
Shipped languages: **English** (source and fallback), **Spanish** (`es`), **Russian** (`ru`) and **Simplified Chinese** (`zh-CN`).
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
@@ -63,4 +63,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`: [Spanish](./translations/es.md), [Russian](./translations/ru.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`: [Spanish](./translations/es.md), [Russian](./translations/ru.md), [Simplified Chinese](./translations/zh-CN.md). Read it before translating, and add a term there when you settle on one.
|
||||||
+15
-9
@@ -3,10 +3,11 @@
|
|||||||
God's Eye View uses explicit auth modes for `/api/flights`:
|
God's Eye View uses explicit auth modes for `/api/flights`:
|
||||||
|
|
||||||
- `OPENSKY_AUTH_MODE=oauth` (default, recommended)
|
- `OPENSKY_AUTH_MODE=oauth` (default, recommended)
|
||||||
- `OPENSKY_AUTH_MODE=auto` (OAuth first, then Basic fallback)
|
- `OPENSKY_AUTH_MODE=auto` (OAuth when configured, otherwise anonymous)
|
||||||
- `OPENSKY_AUTH_MODE=basic`
|
|
||||||
- `OPENSKY_AUTH_MODE=anon`
|
- `OPENSKY_AUTH_MODE=anon`
|
||||||
|
|
||||||
|
`OPENSKY_AUTH_MODE=basic` is retired (see below).
|
||||||
|
|
||||||
Reference: OpenSky REST API docs recommend OAuth2 Client Credentials flow.
|
Reference: OpenSky REST API docs recommend OAuth2 Client Credentials flow.
|
||||||
|
|
||||||
## Quick Start (OAuth Client JSON)
|
## Quick Start (OAuth Client JSON)
|
||||||
@@ -41,13 +42,18 @@ Launchers resolve OAuth creds in this order:
|
|||||||
2. `OPENSKY_CREDENTIALS_FILE`
|
2. `OPENSKY_CREDENTIALS_FILE`
|
||||||
3. Keychain (`opensky-network` / `client_id`, `client_secret`)
|
3. Keychain (`opensky-network` / `client_id`, `client_secret`)
|
||||||
|
|
||||||
## Basic Auth (Legacy)
|
The server itself reads 1 and 2, so `npm run dev`, `npm run preview` and
|
||||||
|
Pinokio also work with only `OPENSKY_CREDENTIALS_FILE` set in `.env`, on any
|
||||||
|
platform. It reads the file on each token refresh.
|
||||||
|
|
||||||
Basic auth is still supported for now, but OpenSky is moving away from it.
|
## Basic Auth (Retired)
|
||||||
|
|
||||||
```bash
|
OpenSky stopped accepting username/password (HTTP Basic) sign-in on
|
||||||
OPENSKY_AUTH_MODE=basic ./scripts/dev-fresh.sh
|
2026-03-18; its API now takes only OAuth2 client credentials. With
|
||||||
```
|
`OPENSKY_AUTH_MODE=basic` the server no longer sends `OPENSKY_USERNAME` /
|
||||||
|
`OPENSKY_PASSWORD`: it requests anonymously (reason `basic_retired`) and
|
||||||
|
logs once what to set instead. Create an API client on your OpenSky
|
||||||
|
account page and use its `credentials.json` as above.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
@@ -61,6 +67,6 @@ Useful reasons:
|
|||||||
|
|
||||||
- `oauth_invalid_or_missing`: OAuth client not found/usable
|
- `oauth_invalid_or_missing`: OAuth client not found/usable
|
||||||
- `oauth_invalid_credentials`: OAuth client rejected by OpenSky
|
- `oauth_invalid_credentials`: OAuth client rejected by OpenSky
|
||||||
- `basic_invalid_credentials`: username/password rejected
|
- `missing_oauth_creds`: auto mode found no OAuth client
|
||||||
- `missing_basic_creds`: basic mode missing user/pass
|
- `basic_retired`: basic mode requested; answered anonymously
|
||||||
- `rate_limited`: OpenSky throttling
|
- `rate_limited`: OpenSky throttling
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Simplified Chinese (`zh-CN`)
|
||||||
|
|
||||||
|
Catalog: `src/i18n/locales/zh-CN.json`. General rules: [TRANSLATORS.md](../TRANSLATORS.md).
|
||||||
|
|
||||||
|
Translation sources: abiyeenzo (#914) and EDIT-Aeterna (#979).
|
||||||
|
|
||||||
|
## Style
|
||||||
|
|
||||||
|
- Mainland usage and Simplified characters. Address the user neutrally, without 您/你 where a plain imperative works: 「点击地图」, 「重试」.
|
||||||
|
- Chinese has no case: an UPPERCASE English HUD label becomes the same short Chinese label. Keep labels as short as the English: 「实时」, 「过期」, 「关」.
|
||||||
|
- Full-width punctuation in Chinese text: ,。:;?!()“”. A dash is 「——」 without spaces; keep `·` as the HUD separator and `…` for ellipsis.
|
||||||
|
- One space between Chinese and Latin letters or digits: 「AIS 船舶」, 「24 小时」, 「{count} 个」. No space next to full-width punctuation.
|
||||||
|
- Units: km, m, km/h, m/s, kn, ft, MW, °, % stay as written; time words are 小时, 分钟, 秒 after a number (「5 分钟前」).
|
||||||
|
- Product names, provider names and codes stay as they are: Google, Cesium, CelesTrak, NOAA, FIRMS, AIS, ADS-B, GNSS, CCTV, HUD, CRT, NVG, FLIR, WHOT/BHOT, Ironbow, CONUS.
|
||||||
|
|
||||||
|
## Plurals
|
||||||
|
|
||||||
|
`Intl.PluralRules('zh-CN')` has one category, `other`: a `tn()` entry has only that form. Use a measure word next to the number where Chinese needs one: 「{count} 个密钥」, 「{count} 架飞机」, 「{count} 艘船」.
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
| English | 中文 |
|
||||||
|
| ------------------------------------- | --------------------------- |
|
||||||
|
| layer / data layer | 图层 / 数据图层 |
|
||||||
|
| feed | 数据源 |
|
||||||
|
| source / data source | 来源 / 数据源 |
|
||||||
|
| LIVE (status) / live data | 实时 / 实时数据 |
|
||||||
|
| SIMULATED | 模拟 |
|
||||||
|
| stale / degraded / unavailable | 过期 / 降级 / 不可用 |
|
||||||
|
| retry | 重试 |
|
||||||
|
| key (API key) / POWER UP | 密钥(API 密钥)/ 增强 |
|
||||||
|
| Provider Settings | 服务商设置 |
|
||||||
|
| globe / map / map source / basemap | 地球 / 地图 / 地图源 / 底图 |
|
||||||
|
| aircraft / flight / military flights | 飞机 / 航班 / 军用航班 |
|
||||||
|
| vessel, ship | 船舶 |
|
||||||
|
| satellite | 卫星 |
|
||||||
|
| launch (rocket) / space mission | 发射 / 太空任务 |
|
||||||
|
| contact (tracked object) | 目标 |
|
||||||
|
| installation, site | 设施 |
|
||||||
|
| camera / CCTV camera | 摄像头 / CCTV 摄像头 |
|
||||||
|
| earthquake / active fire | 地震 / 活跃火点 |
|
||||||
|
| scene / shot | 场景 / 镜头 |
|
||||||
|
| briefing / panel / dock | 简报 / 面板 / 停靠栏 |
|
||||||
|
| preset / visual preset | 预设 / 视觉预设 |
|
||||||
|
| Display / HUD layout | 显示 / HUD 布局 |
|
||||||
|
| Tactical / Operator / Minimal / Cyber | 战术 / 操作员 / 精简 / 赛博 |
|
||||||
|
| Scope (circular viewport mask) / keyhole | 视域 |
|
||||||
|
| Clean UI | 纯净界面 |
|
||||||
|
| cockpit | 座舱 |
|
||||||
|
| heading / altitude / ground speed | 航向 / 高度 / 地速 |
|
||||||
|
| map heading / North up | 方位 / 正北朝上 |
|
||||||
|
| Street Level / Street View (Google) | 街景 / 街景(Google) |
|
||||||
|
| Recent Imagery | 近期影像 |
|
||||||
|
| Global Context / Context (panel) | 全局态势 / 态势 |
|
||||||
|
| radio (station) | 电台 |
|
||||||
|
| transit / bikeshare | 公共交通 / 共享单车 |
|
||||||
|
| Previous / Next | 上一个 / 下一个 |
|
||||||
|
| compass points (N, NNE, …) | 北, 北东北, … |
|
||||||
|
| weather field / FIELD (selector) | 气象场 / 要素 |
|
||||||
|
| weather model / forecast lead hours | 模式 / 预报时效 |
|
||||||
|
| valid (forecast time) | 有效时间 |
|
||||||
|
| scene cloud / mosaic | 整景云量 / 镶嵌图 |
|
||||||
|
| pin (Recent Imagery) | 固定 |
|
||||||
|
| THIS (voice pointer) | 所指 |
|
||||||
|
| outline / trace an outline | 轮廓 / 绘制轮廓 |
|
||||||
|
| mark (map annotation) | 标注 |
|
||||||
|
| camera badge | 摄像头标记 |
|
||||||
|
| provider / embedded provider controls | 服务商 / 平台控件 |
|
||||||
|
| share (a view or link) | 分享 |
|
||||||
|
| AI agent | AI 智能体 |
|
||||||
|
| stage (rocket) / downrange | 子级 / 航程 |
|
||||||
|
| capture shot / camera move | 保存镜头 / 运镜 |
|
||||||
|
| story beat / evidence beat | 事件节点 / 证据节点 |
|
||||||
|
| camera pose | 位姿 |
|
||||||
|
| forecast uncertainty cone | 预报不确定范围 |
|
||||||
|
| seed (catalog data) | 预置 |
|
||||||
|
| frame (bring into view) | 将…纳入视野 |
|
||||||
+1
-1
@@ -26,7 +26,7 @@
|
|||||||
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,chevron_left,chevron_right,close,close_fullscreen,dark_mode,draw,east,flare,flight,layers_clear,light_mode,local_fire_department,my_location,navigation,normal,on,open_in_full,public,radar,radio,right_panel_close,right_panel_open,rocket_launch,skip_next,skip_previous,upload_file,view_in_ar,visibility,visibility_off" 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,chevron_left,chevron_right,close,close_fullscreen,dark_mode,draw,east,flare,flight,layers,layers_clear,light_mode,local_fire_department,more_horiz,my_location,navigation,normal,on,open_in_full,public,radar,radio,right_panel_close,right_panel_open,rocket_launch,search,skip_next,skip_previous,upload_file,videocam,view_in_ar,visibility,visibility_off" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- gev:template scene-chrome -->
|
<!-- gev:template scene-chrome -->
|
||||||
|
|||||||
@@ -272,6 +272,8 @@
|
|||||||
"./layers/street-level/mapillary": "./src/layers/streetLevel/providers/mapillary/index.js",
|
"./layers/street-level/mapillary": "./src/layers/streetLevel/providers/mapillary/index.js",
|
||||||
"./layers/street-level/mapillary/source": "./src/layers/streetLevel/providers/mapillary/source.js",
|
"./layers/street-level/mapillary/source": "./src/layers/streetLevel/providers/mapillary/source.js",
|
||||||
"./layers/street-level/google": "./src/layers/streetLevel/providers/google/index.js",
|
"./layers/street-level/google": "./src/layers/streetLevel/providers/google/index.js",
|
||||||
|
"./layers/street-level/kartaview": "./src/layers/streetLevel/providers/kartaview/index.js",
|
||||||
|
"./layers/street-level/panoramax": "./src/layers/streetLevel/providers/panoramax/index.js",
|
||||||
"./tools": "./src/tools/index.js",
|
"./tools": "./src/tools/index.js",
|
||||||
"./tools/mcp": "./src/tools/mcp/index.js",
|
"./tools/mcp": "./src/tools/mcp/index.js",
|
||||||
"./tools/panel": "./src/tools/panel.js",
|
"./tools/panel": "./src/tools/panel.js",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ under the license listed below.
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `airplane.glb` | “boeing 747” by [zairiq-123](https://sketchfab.com/zairiq-123) | [Sketchfab model](https://sketchfab.com/3d-models/boeing-747-9b16672038ba48f98e6d80a159044ed9) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Substantially modified and optimized for God's Eye View, including geometry/material simplification and coordinate/orientation preparation. The former 24× runtime calibration is baked into the mesh; location, rotation, and scale are applied; the bounding-box centre is at the origin; and the model uses glTF +Y-up with its nose toward local −X. Geometry remains uncompressed so the aircraft asset does not compete with photogrammetry for Draco worker capacity. |
|
| `airplane.glb` | “boeing 747” by [zairiq-123](https://sketchfab.com/zairiq-123) | [Sketchfab model](https://sketchfab.com/3d-models/boeing-747-9b16672038ba48f98e6d80a159044ed9) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Substantially modified and optimized for God's Eye View, including geometry/material simplification and coordinate/orientation preparation. The former 24× runtime calibration is baked into the mesh; location, rotation, and scale are applied; the bounding-box centre is at the origin; and the model uses glTF +Y-up with its nose toward local −X. Geometry remains uncompressed so the aircraft asset does not compete with photogrammetry for Draco worker capacity. |
|
||||||
| `jet.glb` | “Private Jet” by [Nick the Name](https://sketchfab.com/Nick_The_Name) | [Sketchfab model](https://sketchfab.com/3d-models/private-jet-cbdd1de6ced9461e950eafaa302cc82b) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Repackaged as a glTF binary for the project's military-flight visualization. Imported hierarchy transforms are baked into the meshes; location, rotation, and scale are applied; the bounding-box centre is at the origin; and the model uses meter scale, glTF +Y-up, and the shared nose −X convention. Existing materials are preserved. |
|
| `jet.glb` | “Private Jet” by [Nick the Name](https://sketchfab.com/Nick_The_Name) | [Sketchfab model](https://sketchfab.com/3d-models/private-jet-cbdd1de6ced9461e950eafaa302cc82b) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Repackaged as a glTF binary for the project's military-flight visualization. Imported hierarchy transforms are baked into the meshes; location, rotation, and scale are applied; the bounding-box centre is at the origin; and the model uses meter scale, glTF +Y-up, and the shared nose −X convention. Existing materials are preserved. |
|
||||||
| `ship.glb` | “Low Poly Cargo Ship” by [Javier_Fernandez](https://sketchfab.com/Javier.Fernandez) | [Sketchfab model](https://sketchfab.com/3d-models/low-poly-cargo-ship-4c22cbaf01c1427f8ab60b3a07b1b32c) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized and repackaged as a glTF binary for the project's vessel visualization. |
|
|
||||||
| `bell206.glb` | “Bell 206 JetRanger” by [terran4627](https://sketchfab.com/terran4627) | [Sketchfab model](https://sketchfab.com/3d-models/bell-206-jetranger-d2f7ba1d671549d4b26aaf834139a1dd) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. |
|
| `bell206.glb` | “Bell 206 JetRanger” by [terran4627](https://sketchfab.com/terran4627) | [Sketchfab model](https://sketchfab.com/3d-models/bell-206-jetranger-d2f7ba1d671549d4b26aaf834139a1dd) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. |
|
||||||
| `c172.glb` | “Cessna 172” by [e737](https://sketchfab.com/e0057537) | [Sketchfab model](https://sketchfab.com/3d-models/cessna-172-64cddaee5aff470682659a8c08525046) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. |
|
| `c172.glb` | “Cessna 172” by [e737](https://sketchfab.com/e0057537) | [Sketchfab model](https://sketchfab.com/3d-models/cessna-172-64cddaee5aff470682659a8c08525046) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. |
|
||||||
| `citation2.glb` | “1990 Cessna Citation, Texture Detailed, Exterior” by [BlenderCommunityHead](https://sketchfab.com/aboodgoudagad) | [Sketchfab model](https://sketchfab.com/3d-models/1990-cessna-citation-texture-detailed-exterior-a78839624fe64900a8352cb23462350a) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. |
|
| `citation2.glb` | “1990 Cessna Citation, Texture Detailed, Exterior” by [BlenderCommunityHead](https://sketchfab.com/aboodgoudagad) | [Sketchfab model](https://sketchfab.com/3d-models/1990-cessna-citation-texture-detailed-exterior-a78839624fe64900a8352cb23462350a) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. |
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,158 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Build src/data/local_data/sky/bright_stars.json from the Yale Bright Star
|
||||||
|
* Catalogue, 5th Revised Edition (Hoffleit & Warren 1991).
|
||||||
|
*
|
||||||
|
* Source: CDS VizieR catalogue V/50, https://cdsarc.cds.unistra.fr/viz-bin/cat/V/50
|
||||||
|
* Fetched: https://cdsarc.cds.unistra.fr/ftp/V/50/catalog.gz
|
||||||
|
* Format: fixed-width ASCII, 197 bytes per record, byte layout in
|
||||||
|
* https://cdsarc.cds.unistra.fr/ftp/V/50/ReadMe
|
||||||
|
* License: public domain (Yale University Observatory / NASA ADC); CDS asks
|
||||||
|
* for an acknowledgement, which the Data attribution popover carries.
|
||||||
|
*
|
||||||
|
* Transform (deterministic):
|
||||||
|
* 1. J2000 RA/Dec (bytes 76-90), V magnitude (103-107) and B−V (110-114).
|
||||||
|
* 2. The 14 records without a J2000 position (novae and other objects later
|
||||||
|
* found not to be stars) are dropped.
|
||||||
|
* 3. RA/Dec rounded to 3 decimals of a degree (3.6″ — a fraction of a pixel
|
||||||
|
* at any field of view the globe uses), magnitudes to 2.
|
||||||
|
* 4. Missing B−V is stored as null; the renderer gives it a neutral colour.
|
||||||
|
* 5. Records keep catalogue (HR) order for a stable diff.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/build-bright-stars.mjs [catalog | catalog.gz]
|
||||||
|
* With no argument it downloads the catalogue from CDS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import zlib from 'node:zlib';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const SOURCE_URL = 'https://cdsarc.cds.unistra.fr/ftp/V/50/catalog.gz';
|
||||||
|
const OUT = path.join(
|
||||||
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
|
'..',
|
||||||
|
'src',
|
||||||
|
'data',
|
||||||
|
'local_data',
|
||||||
|
'sky',
|
||||||
|
'bright_stars.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Slice 1-based inclusive byte columns the way the CDS ReadMe numbers them. */
|
||||||
|
function columns(line, from, to) {
|
||||||
|
return line.slice(from - 1, to).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse a fixed-width float; blank fields are null. */
|
||||||
|
function parseField(text) {
|
||||||
|
if (text === '') return null;
|
||||||
|
const value = Number(text);
|
||||||
|
return Number.isFinite(value) ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse one BSC5 record. Returns null for records without a J2000 position
|
||||||
|
* or V magnitude.
|
||||||
|
*/
|
||||||
|
export function parseBscRecord(line) {
|
||||||
|
const hr = parseField(columns(line, 1, 4));
|
||||||
|
const raH = parseField(columns(line, 76, 77));
|
||||||
|
const raM = parseField(columns(line, 78, 79));
|
||||||
|
const raS = parseField(columns(line, 80, 83));
|
||||||
|
const sign = columns(line, 84, 84);
|
||||||
|
const deD = parseField(columns(line, 85, 86));
|
||||||
|
const deM = parseField(columns(line, 87, 88));
|
||||||
|
const deS = parseField(columns(line, 89, 90));
|
||||||
|
const vmag = parseField(columns(line, 103, 107));
|
||||||
|
const bv = parseField(columns(line, 110, 114));
|
||||||
|
if (
|
||||||
|
hr === null ||
|
||||||
|
raH === null ||
|
||||||
|
raM === null ||
|
||||||
|
raS === null ||
|
||||||
|
deD === null ||
|
||||||
|
deM === null ||
|
||||||
|
deS === null ||
|
||||||
|
vmag === null ||
|
||||||
|
(sign !== '+' && sign !== '-')
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
const raDeg = (raH + raM / 60 + raS / 3600) * 15;
|
||||||
|
const decDeg = (sign === '-' ? -1 : 1) * (deD + deM / 60 + deS / 3600);
|
||||||
|
return { hr, raDeg, decDeg, vmag, bv };
|
||||||
|
}
|
||||||
|
|
||||||
|
const round = (value, decimals) => {
|
||||||
|
const scale = 10 ** decimals;
|
||||||
|
return Math.round(value * scale) / scale;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Build the committed pack from the raw catalogue text. */
|
||||||
|
export function buildStarPack(text, { fetched = null } = {}) {
|
||||||
|
const stars = [];
|
||||||
|
let dropped = 0;
|
||||||
|
for (const line of text.split('\n')) {
|
||||||
|
if (line.trim() === '') continue;
|
||||||
|
const record = parseBscRecord(line);
|
||||||
|
if (!record) {
|
||||||
|
dropped += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
stars.push(
|
||||||
|
round(record.raDeg, 3),
|
||||||
|
round(record.decDeg, 3),
|
||||||
|
round(record.vmag, 2),
|
||||||
|
record.bv === null ? null : round(record.bv, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
meta: {
|
||||||
|
source:
|
||||||
|
'Yale Bright Star Catalogue, 5th Revised Ed. (Hoffleit & Warren 1991), CDS V/50',
|
||||||
|
url: SOURCE_URL,
|
||||||
|
license: 'public domain',
|
||||||
|
fetched,
|
||||||
|
frame: 'ICRF/J2000, epoch 2000.0',
|
||||||
|
columns: ['raDeg', 'decDeg', 'vmag', 'bv'],
|
||||||
|
count: stars.length / 4,
|
||||||
|
dropped,
|
||||||
|
},
|
||||||
|
stars,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readCatalogue(file) {
|
||||||
|
if (!file) {
|
||||||
|
const response = await fetch(SOURCE_URL);
|
||||||
|
if (!response.ok) throw new Error(`${SOURCE_URL}: HTTP ${response.status}`);
|
||||||
|
return zlib
|
||||||
|
.gunzipSync(Buffer.from(await response.arrayBuffer()))
|
||||||
|
.toString('latin1');
|
||||||
|
}
|
||||||
|
const bytes = fs.readFileSync(file);
|
||||||
|
return (file.endsWith('.gz') ? zlib.gunzipSync(bytes) : bytes).toString(
|
||||||
|
'latin1',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const [file] = process.argv.slice(2);
|
||||||
|
const text = await readCatalogue(file);
|
||||||
|
const pack = buildStarPack(text, {
|
||||||
|
fetched: new Date().toISOString().slice(0, 10),
|
||||||
|
});
|
||||||
|
// One star per line keeps the JSON compact and the diff readable.
|
||||||
|
const rows = [];
|
||||||
|
for (let i = 0; i < pack.stars.length; i += 4)
|
||||||
|
rows.push(JSON.stringify(pack.stars.slice(i, i + 4)).slice(1, -1));
|
||||||
|
const body = `{"meta":${JSON.stringify(pack.meta)},\n"stars":[\n${rows.join(',\n')}\n]}\n`;
|
||||||
|
fs.mkdirSync(path.dirname(OUT), { recursive: true });
|
||||||
|
fs.writeFileSync(OUT, body);
|
||||||
|
console.log(
|
||||||
|
`wrote ${pack.meta.count} stars (${pack.meta.dropped} dropped) → ${path.relative(process.cwd(), OUT)} (${body.length} bytes)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] === fileURLToPath(import.meta.url)) await main();
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Build the soft Milky Way cube map in src/data/local_data/sky/ from the ESO
|
||||||
|
* all-sky Milky Way panorama.
|
||||||
|
*
|
||||||
|
* Source page: https://www.eso.org/public/images/eso0932a/
|
||||||
|
* Fetched: https://cdn.eso.org/images/original/eso0932a.tif
|
||||||
|
* (6000×3000 8-bit sRGB TIFF, 27.7 MB; sha256 in README.md)
|
||||||
|
* Credit: ESO/S. Brunier
|
||||||
|
* License: CC BY 4.0 (https://www.eso.org/public/outreach/copyright/).
|
||||||
|
* Modified: stars removed, blurred, reprojected to ICRF cube
|
||||||
|
* faces and tone-mapped, as below.
|
||||||
|
* Frame: plate carrée in galactic coordinates, centred on the Galactic
|
||||||
|
* Centre, galactic longitude increasing to the left.
|
||||||
|
*
|
||||||
|
* Transform (deterministic):
|
||||||
|
* 1. Resample to WORK_WIDTH×WORK_WIDTH/2, sRGB to linear light.
|
||||||
|
* 2. Remove stars with a morphological opening: the renderer draws
|
||||||
|
* catalogue stars itself, sharp and in the right place.
|
||||||
|
* 3. Paint out the planets the 2008–2010 exposures caught (PLANETS).
|
||||||
|
* 4. Blur, round on the sphere: σ = BLUR_DEG, widening to FAINT_BLUR_DEG
|
||||||
|
* in the faint glow so unresolved stars leave no clumps.
|
||||||
|
* 5. Resample into six WebGL cube faces (uploaded without flipY) with the
|
||||||
|
* ICRF axes: +X to the March equinox, +Z to the north celestial pole.
|
||||||
|
* 6. Median sky to black with a soft knee, brightest 0.01 % to ~97 %
|
||||||
|
* white, on luminance to keep the hue; gamma 1/2.2, dither, WebP.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/build-milky-way.mjs <eso0932a.tif>
|
||||||
|
* Needs the sharp dev dependency.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
export const FACE_SIZE = 1024;
|
||||||
|
export const BLUR_DEG = 0.4;
|
||||||
|
// Faint glow is grain of unresolved stars and needs the wide blur; bright
|
||||||
|
// parts keep the fine one and their dust lanes. FAINT_RANGE (fraction of
|
||||||
|
// black-to-white) is where the two blend.
|
||||||
|
export const FAINT_BLUR_DEG = 2;
|
||||||
|
const FAINT_RANGE = [0.005, 0.08];
|
||||||
|
const WORK_WIDTH = 4096;
|
||||||
|
const OPEN_RADIUS = 3;
|
||||||
|
const WEBP_QUALITY = 96;
|
||||||
|
// The median sky is black: above it the glow away from the plane reads as
|
||||||
|
// grey blotches. SOFT_KNEE eases the cut in so it draws no edges.
|
||||||
|
const BLACK_PERCENTILE = 0.5;
|
||||||
|
const SOFT_KNEE = 0.03;
|
||||||
|
const WHITE_PERCENTILE = 0.9999;
|
||||||
|
const OUT_DIR = path.join(
|
||||||
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
|
'..',
|
||||||
|
'src',
|
||||||
|
'data',
|
||||||
|
'local_data',
|
||||||
|
'sky',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bright planets in the panorama, galactic longitude/latitude in degrees and
|
||||||
|
* the radius painted over with the surrounding sky. By position both are
|
||||||
|
* Jupiter, in Sagittarius (2008) and Aquarius (2010); no catalogue star lies
|
||||||
|
* under either.
|
||||||
|
*/
|
||||||
|
export const PLANETS = Object.freeze([
|
||||||
|
{ l: 13.4, b: -16.6, radiusDeg: 2.5 },
|
||||||
|
{ l: 69.0, b: -58.4, radiusDeg: 2.5 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Cube faces in WebGL upload order. */
|
||||||
|
export const CUBE_FACES = Object.freeze([
|
||||||
|
'positiveX',
|
||||||
|
'negativeX',
|
||||||
|
'positiveY',
|
||||||
|
'negativeY',
|
||||||
|
'positiveZ',
|
||||||
|
'negativeZ',
|
||||||
|
]);
|
||||||
|
export const FACE_FILE_SUFFIX = Object.freeze({
|
||||||
|
positiveX: 'px',
|
||||||
|
negativeX: 'nx',
|
||||||
|
positiveY: 'py',
|
||||||
|
negativeY: 'ny',
|
||||||
|
positiveZ: 'pz',
|
||||||
|
negativeZ: 'nz',
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ICRF (J2000 equatorial) → galactic rotation, rows = galactic axes
|
||||||
|
* (Hipparcos catalogue, ESA SP-1200 vol. 1 §1.5.3).
|
||||||
|
*/
|
||||||
|
export const ICRF_TO_GALACTIC = Object.freeze([
|
||||||
|
[-0.0548755604162154, -0.873437090234885, -0.4838350155487132],
|
||||||
|
[0.4941094278755837, -0.4448296299600112, 0.7469822444972189],
|
||||||
|
[-0.867666149019004, -0.1980763734312015, 0.4559837761750669],
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direction (not normalized) that the WebGL cube-map lookup sends to texel
|
||||||
|
* coordinate (s, t) ∈ [0, 1]² of `face`, t = 0 being the first image row.
|
||||||
|
* Inverse of the OpenGL ES 3.0 §3.8.10 major-axis table.
|
||||||
|
*/
|
||||||
|
export function cubeFaceDirection(face, s, t) {
|
||||||
|
const sc = 2 * s - 1;
|
||||||
|
const tc = 2 * t - 1;
|
||||||
|
switch (face) {
|
||||||
|
case 'positiveX':
|
||||||
|
return [1, -tc, -sc];
|
||||||
|
case 'negativeX':
|
||||||
|
return [-1, -tc, sc];
|
||||||
|
case 'positiveY':
|
||||||
|
return [sc, 1, tc];
|
||||||
|
case 'negativeY':
|
||||||
|
return [sc, -1, -tc];
|
||||||
|
case 'positiveZ':
|
||||||
|
return [sc, -tc, 1];
|
||||||
|
case 'negativeZ':
|
||||||
|
return [-sc, -tc, -1];
|
||||||
|
default:
|
||||||
|
throw new RangeError(`Unknown cube face ${face}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rotate an ICRF direction into galactic coordinates. */
|
||||||
|
export function icrfToGalactic([x, y, z]) {
|
||||||
|
return ICRF_TO_GALACTIC.map((row) => row[0] * x + row[1] * y + row[2] * z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a direction to (u, v) ∈ [0, 1)² of a plate carrée centred on
|
||||||
|
* longitude 0 with longitude increasing to the left and v = 0 at +90°.
|
||||||
|
*/
|
||||||
|
export function directionToPlateCarree(x, y, z) {
|
||||||
|
const longitude = Math.atan2(y, x);
|
||||||
|
const latitude = Math.atan2(z, Math.hypot(x, y));
|
||||||
|
let u = 0.5 - longitude / (2 * Math.PI);
|
||||||
|
u -= Math.floor(u);
|
||||||
|
return [u, 0.5 - latitude / Math.PI];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Box widths whose three passes approximate a Gaussian of `sigma` (Kovesi). */
|
||||||
|
function boxRadii(sigma) {
|
||||||
|
const ideal = Math.sqrt((12 * sigma * sigma) / 3 + 1);
|
||||||
|
let lower = Math.floor(ideal);
|
||||||
|
if (lower % 2 === 0) lower -= 1;
|
||||||
|
const upper = lower + 2;
|
||||||
|
const m = Math.round(
|
||||||
|
(12 * sigma * sigma - 3 * lower * lower - 12 * lower - 9) /
|
||||||
|
(-4 * lower - 4),
|
||||||
|
);
|
||||||
|
return [0, 1, 2].map((i) => ((i < m ? lower : upper) - 1) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Index along a line, wrapped or clamped at the ends. */
|
||||||
|
function lineIndex(i, length, wrap) {
|
||||||
|
if (wrap) return ((i % length) + length) % length;
|
||||||
|
return i < 0 ? 0 : i >= length ? length - 1 : i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Running-sum box blur of one strided line. */
|
||||||
|
function boxLine(src, dst, offset, stride, length, radius, wrap) {
|
||||||
|
if (radius < 1) {
|
||||||
|
for (let i = 0; i < length; i++)
|
||||||
|
dst[offset + i * stride] = src[offset + i * stride];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const r = wrap ? Math.min(radius, Math.floor((length - 1) / 2)) : radius;
|
||||||
|
const at = (i) => src[offset + lineIndex(i, length, wrap) * stride];
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = -r; i <= r; i++) sum += at(i);
|
||||||
|
const scale = 1 / (2 * r + 1);
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
dst[offset + i * stride] = sum * scale;
|
||||||
|
sum += at(i + r + 1) - at(i - r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minimum or maximum over a (2r+1) window of one strided line. */
|
||||||
|
function extremumLine(src, dst, offset, stride, length, radius, wrap, pick) {
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
let value = src[offset + lineIndex(i - radius, length, wrap) * stride];
|
||||||
|
for (let k = -radius + 1; k <= radius; k++)
|
||||||
|
value = pick(
|
||||||
|
value,
|
||||||
|
src[offset + lineIndex(i + k, length, wrap) * stride],
|
||||||
|
);
|
||||||
|
dst[offset + i * stride] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Separable minimum or maximum filter over a plate-carrée plane, in place. */
|
||||||
|
function extremumPlane(plane, width, height, radius, pick) {
|
||||||
|
const scratch = new Float32Array(plane.length);
|
||||||
|
for (let y = 0; y < height; y++)
|
||||||
|
extremumLine(plane, scratch, y * width, 1, width, radius, true, pick);
|
||||||
|
for (let x = 0; x < width; x++)
|
||||||
|
extremumLine(scratch, plane, x, width, height, radius, false, pick);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Blur one plate-carrée channel in place, round on the sphere. */
|
||||||
|
function blurPlane(plane, width, height, sigmaPx) {
|
||||||
|
const scratch = new Float32Array(plane.length);
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
const latitude = (0.5 - (y + 0.5) / height) * Math.PI;
|
||||||
|
const sigmaX = Math.min(
|
||||||
|
width / 4,
|
||||||
|
sigmaPx / Math.max(Math.cos(latitude), 1e-3),
|
||||||
|
);
|
||||||
|
for (const radius of boxRadii(sigmaX)) {
|
||||||
|
boxLine(plane, scratch, y * width, 1, width, Math.round(radius), true);
|
||||||
|
plane.set(scratch.subarray(y * width, (y + 1) * width), y * width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const radius of boxRadii(sigmaPx)) {
|
||||||
|
for (let x = 0; x < width; x++)
|
||||||
|
boxLine(plane, scratch, x, width, height, Math.round(radius), false);
|
||||||
|
plane.set(scratch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unit vector for galactic longitude and latitude in degrees. */
|
||||||
|
function galacticVector(l, b) {
|
||||||
|
const lr = (l * Math.PI) / 180;
|
||||||
|
const br = (b * Math.PI) / 180;
|
||||||
|
return [
|
||||||
|
Math.cos(br) * Math.cos(lr),
|
||||||
|
Math.cos(br) * Math.sin(lr),
|
||||||
|
Math.sin(br),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace a disc of the sky with the median of the ring just outside it, per
|
||||||
|
* channel. A median, not a mean: the planet's diffraction spikes reach far
|
||||||
|
* past the disc and would lift a mean well above the sky. The blur that
|
||||||
|
* follows hides the seam.
|
||||||
|
*/
|
||||||
|
function paintOut(planes, width, height, { l, b, radiusDeg }) {
|
||||||
|
const centre = galacticVector(l, b);
|
||||||
|
const inner = Math.cos((radiusDeg * Math.PI) / 180);
|
||||||
|
const outer = Math.cos(((radiusDeg + 1) * Math.PI) / 180);
|
||||||
|
const disc = [];
|
||||||
|
const ring = [];
|
||||||
|
const span = Math.ceil(((radiusDeg + 1) / 180) * height);
|
||||||
|
const row = Math.round((0.5 - b / 180) * height);
|
||||||
|
for (let y = Math.max(0, row - span); y < Math.min(height, row + span); y++) {
|
||||||
|
const latitude = 90 - ((y + 0.5) * 180) / height;
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const longitude = (0.5 - (x + 0.5) / width) * 360;
|
||||||
|
const p = galacticVector(longitude, latitude);
|
||||||
|
const cos = p[0] * centre[0] + p[1] * centre[1] + p[2] * centre[2];
|
||||||
|
if (cos >= inner) disc.push(y * width + x);
|
||||||
|
else if (cos >= outer) ring.push(y * width + x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const plane of planes) {
|
||||||
|
const values = Float32Array.from(ring, (index) => plane[index]).sort();
|
||||||
|
const median = values[values.length >> 1];
|
||||||
|
for (const index of disc) plane[index] = median;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bilinear sample with longitude wrap and latitude clamp. */
|
||||||
|
function samplePlane(plane, width, height, u, v) {
|
||||||
|
const fx = u * width - 0.5;
|
||||||
|
const fy = Math.min(Math.max(v * height - 0.5, 0), height - 1);
|
||||||
|
const x0 = Math.floor(fx);
|
||||||
|
const y0 = Math.floor(fy);
|
||||||
|
const ax = fx - x0;
|
||||||
|
const ay = fy - y0;
|
||||||
|
const xa = ((x0 % width) + width) % width;
|
||||||
|
const xb = (xa + 1) % width;
|
||||||
|
const yb = Math.min(y0 + 1, height - 1);
|
||||||
|
const top = plane[y0 * width + xa] * (1 - ax) + plane[y0 * width + xb] * ax;
|
||||||
|
const bottom =
|
||||||
|
plane[yb * width + xa] * (1 - ax) + plane[yb * width + xb] * ax;
|
||||||
|
return top * (1 - ay) + bottom * ay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deterministic hash noise in [-0.5, 0.5) for dithering. */
|
||||||
|
function dither(index, channel) {
|
||||||
|
let h = (index * 374761393 + channel * 668265263) | 0;
|
||||||
|
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
||||||
|
h ^= h >>> 16;
|
||||||
|
return (h >>> 0) / 4294967296 - 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Black level with a soft knee: 0 at and below the level, then a quadratic
|
||||||
|
* ease that joins the straight line, so faint sky fades out instead of
|
||||||
|
* ending in a contour.
|
||||||
|
*/
|
||||||
|
export function softBlack(x, knee = SOFT_KNEE) {
|
||||||
|
return x <= 0 ? 0 : (x * x) / (x + knee);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function smoothstep(edge0, edge1, x) {
|
||||||
|
const t = Math.min(Math.max((x - edge0) / (edge1 - edge0), 0), 1);
|
||||||
|
return t * t * (3 - 2 * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per pixel, keep `fine` where `weight(luminance of coarse)` is 1 and take
|
||||||
|
* `coarse` where it is 0, blending in between. The weight follows the coarse
|
||||||
|
* copy, so it is smooth itself and draws no seams along dark lanes. Writes
|
||||||
|
* into `fine`.
|
||||||
|
*/
|
||||||
|
function blendByBrightness(fine, coarse, weight) {
|
||||||
|
const [r] = fine;
|
||||||
|
for (let i = 0; i < r.length; i++) {
|
||||||
|
const w = weight((coarse[0][i] + coarse[1][i] + coarse[2][i]) / 3);
|
||||||
|
for (let c = 0; c < 3; c++)
|
||||||
|
fine[c][i] = coarse[c][i] + (fine[c][i] - coarse[c][i]) * w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Value at `fraction` of the sorted luminance. */
|
||||||
|
function luminancePercentile(planes, fraction) {
|
||||||
|
const [r, g, b] = planes;
|
||||||
|
const lum = new Float32Array(r.length);
|
||||||
|
for (let i = 0; i < lum.length; i++) lum[i] = (r[i] + g[i] + b[i]) / 3;
|
||||||
|
lum.sort();
|
||||||
|
return lum[Math.min(lum.length - 1, Math.floor(fraction * lum.length))];
|
||||||
|
}
|
||||||
|
|
||||||
|
const srgbToLinear = (value) =>
|
||||||
|
value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
||||||
|
|
||||||
|
async function decodePanorama(sharp, file) {
|
||||||
|
const width = WORK_WIDTH;
|
||||||
|
const height = WORK_WIDTH / 2;
|
||||||
|
const raw = await sharp(file)
|
||||||
|
.resize(width, height, { fit: 'fill', kernel: 'lanczos3' })
|
||||||
|
.removeAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer();
|
||||||
|
const lut = Float32Array.from({ length: 256 }, (_, i) =>
|
||||||
|
srgbToLinear(i / 255),
|
||||||
|
);
|
||||||
|
const planes = [0, 1, 2].map(() => new Float32Array(width * height));
|
||||||
|
for (let i = 0; i < width * height; i++)
|
||||||
|
for (let c = 0; c < 3; c++) planes[c][i] = lut[raw[i * 3 + c]];
|
||||||
|
return { width, height, planes };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const [file] = process.argv.slice(2);
|
||||||
|
if (!file) throw new Error('usage: build-milky-way.mjs <eso0932a.tif>');
|
||||||
|
const { default: sharp } = await import('sharp');
|
||||||
|
const { width, height, planes } = await decodePanorama(sharp, file);
|
||||||
|
const sigmaPx = (BLUR_DEG / 180) * height;
|
||||||
|
for (const plane of planes) {
|
||||||
|
extremumPlane(plane, width, height, OPEN_RADIUS, Math.min);
|
||||||
|
extremumPlane(plane, width, height, OPEN_RADIUS, Math.max);
|
||||||
|
}
|
||||||
|
// After the opening: it sinks the starry sky to the dark gaps between
|
||||||
|
// stars, and a fill taken before it would stay brighter than the sky.
|
||||||
|
for (const planet of PLANETS) paintOut(planes, width, height, planet);
|
||||||
|
const wide = planes.map((plane) => Float32Array.from(plane));
|
||||||
|
for (const plane of planes) blurPlane(plane, width, height, sigmaPx);
|
||||||
|
for (const plane of wide)
|
||||||
|
blurPlane(plane, width, height, (FAINT_BLUR_DEG / 180) * height);
|
||||||
|
// Levels from the fine blur, so the blend weight and the output share
|
||||||
|
// them; the blend moves the median only in the faint sky it smooths.
|
||||||
|
const black = luminancePercentile(planes, BLACK_PERCENTILE);
|
||||||
|
const white = luminancePercentile(planes, WHITE_PERCENTILE);
|
||||||
|
blendByBrightness(planes, wide, (luminance) =>
|
||||||
|
smoothstep(
|
||||||
|
FAINT_RANGE[0],
|
||||||
|
FAINT_RANGE[1],
|
||||||
|
(luminance - black) / (white - black),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let total = 0;
|
||||||
|
for (const face of CUBE_FACES) {
|
||||||
|
const pixels = Buffer.alloc(FACE_SIZE * FACE_SIZE * 3);
|
||||||
|
for (let j = 0; j < FACE_SIZE; j++) {
|
||||||
|
for (let i = 0; i < FACE_SIZE; i++) {
|
||||||
|
const direction = cubeFaceDirection(
|
||||||
|
face,
|
||||||
|
(i + 0.5) / FACE_SIZE,
|
||||||
|
(j + 0.5) / FACE_SIZE,
|
||||||
|
);
|
||||||
|
const [u, v] = directionToPlateCarree(...icrfToGalactic(direction));
|
||||||
|
const index = j * FACE_SIZE + i;
|
||||||
|
const rgb = [0, 1, 2].map((c) =>
|
||||||
|
samplePlane(planes[c], width, height, u, v),
|
||||||
|
);
|
||||||
|
const luminance = (rgb[0] + rgb[1] + rgb[2]) / 3;
|
||||||
|
const level = softBlack((luminance - black) / (white - black));
|
||||||
|
for (let c = 0; c < 3; c++) {
|
||||||
|
// Scale every channel by the luminance curve to keep the hue.
|
||||||
|
const linear = luminance > 0 ? (rgb[c] / luminance) * level : 0;
|
||||||
|
const encoded = Math.min(linear, 1) ** (1 / 2.2) * 255;
|
||||||
|
pixels[index * 3 + c] = Math.min(
|
||||||
|
255,
|
||||||
|
Math.max(0, Math.round(encoded + dither(index, c))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const out = path.join(OUT_DIR, `milkyway_${FACE_FILE_SUFFIX[face]}.webp`);
|
||||||
|
const info = await sharp(pixels, {
|
||||||
|
raw: { width: FACE_SIZE, height: FACE_SIZE, channels: 3 },
|
||||||
|
})
|
||||||
|
.webp({ quality: WEBP_QUALITY, smartSubsample: true, effort: 6 })
|
||||||
|
.toFile(out);
|
||||||
|
total += info.size;
|
||||||
|
console.log(`${path.relative(process.cwd(), out)}: ${info.size} bytes`);
|
||||||
|
}
|
||||||
|
console.log(`total ${total} bytes`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] === fileURLToPath(import.meta.url)) await main();
|
||||||
@@ -24,11 +24,10 @@ const renderer = (file) =>
|
|||||||
/^src\/layers\/[^/]+\/(?:index|rendering|snapshotRenderer|overlay|presentation|cards|controls)\.js$/.test(
|
/^src\/layers\/[^/]+\/(?:index|rendering|snapshotRenderer|overlay|presentation|cards|controls)\.js$/.test(
|
||||||
file,
|
file,
|
||||||
);
|
);
|
||||||
// Two compatibility composition entries intentionally select standalone defaults.
|
// One compatibility composition entry intentionally selects standalone defaults.
|
||||||
const compatibilityEdge = (from, to) =>
|
const compatibilityEdge = (from, to) =>
|
||||||
(from === 'server/providers/local.js' &&
|
from === 'server/providers/local.js' &&
|
||||||
to === 'server/standalone/key-setup.js') ||
|
to === 'server/standalone/key-setup.js';
|
||||||
(from === 'src/ui.js' && to === 'src/standalone/catalog.js');
|
|
||||||
const portableExport = (key) =>
|
const portableExport = (key) =>
|
||||||
key.startsWith('./sources/') ||
|
key.startsWith('./sources/') ||
|
||||||
/\/source$/.test(key) ||
|
/\/source$/.test(key) ||
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ export function defaultGatewayFromRouteTable(text) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Decide this launch's trusted-peer list. An operator value always wins, so a
|
* Decide this launch's trusted-peer list. An operator value always wins, so a
|
||||||
* compose override stays authoritative; otherwise the gateway alone is
|
* compose override stays authoritative; `none` trusts no additional peers.
|
||||||
|
* An unset or blank value means the gateway alone is
|
||||||
* trusted. No gateway trusts nothing and Provider Settings stays loopback-only,
|
* trusted. No gateway trusts nothing and Provider Settings stays loopback-only,
|
||||||
* exactly as under `npm run dev`.
|
* exactly as under `npm run dev`.
|
||||||
* @param {{env?: NodeJS.ProcessEnv, routeTable?: string}} input
|
* @param {{env?: NodeJS.ProcessEnv, routeTable?: string}} input
|
||||||
@@ -56,6 +57,7 @@ export function defaultGatewayFromRouteTable(text) {
|
|||||||
*/
|
*/
|
||||||
export function resolveTrustedPeers({ env = process.env, routeTable = '' } = {}) {
|
export function resolveTrustedPeers({ env = process.env, routeTable = '' } = {}) {
|
||||||
const explicit = String(env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim();
|
const explicit = String(env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim();
|
||||||
|
if (explicit.toLowerCase() === 'none') return '';
|
||||||
if (explicit) return explicit;
|
if (explicit) return explicit;
|
||||||
return defaultGatewayFromRouteTable(routeTable) ?? '';
|
return defaultGatewayFromRouteTable(routeTable) ?? '';
|
||||||
}
|
}
|
||||||
@@ -109,15 +111,17 @@ async function start() {
|
|||||||
String(process.env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim(),
|
String(process.env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim(),
|
||||||
);
|
);
|
||||||
const trustedPeers = resolveTrustedPeers({ routeTable: readRouteTable() });
|
const trustedPeers = resolveTrustedPeers({ routeTable: readRouteTable() });
|
||||||
|
// Set even an empty list before Vite loads .env: the resolved launcher policy
|
||||||
|
// must remain authoritative when there is no trusted gateway.
|
||||||
|
process.env.GEV_KEY_SETUP_TRUSTED_PEERS = trustedPeers;
|
||||||
if (trustedPeers) {
|
if (trustedPeers) {
|
||||||
// Set before Vite is imported: the dev server snapshots process.env during
|
|
||||||
// configuration, and the admission gate reads it per request.
|
|
||||||
process.env.GEV_KEY_SETUP_TRUSTED_PEERS = trustedPeers;
|
|
||||||
console.log(
|
console.log(
|
||||||
fromOperator
|
fromOperator
|
||||||
? `[Docker] Provider Settings trusted peers: ${trustedPeers} (GEV_KEY_SETUP_TRUSTED_PEERS).`
|
? `[Docker] Provider Settings trusted peers: ${trustedPeers} (GEV_KEY_SETUP_TRUSTED_PEERS).`
|
||||||
: `[Docker] Provider Settings trusts the container gateway (${trustedPeers}).`,
|
: `[Docker] Provider Settings trusts the container gateway (${trustedPeers}).`,
|
||||||
);
|
);
|
||||||
|
} else if (fromOperator) {
|
||||||
|
console.log('[Docker] Provider Settings trusts no additional peers (GEV_KEY_SETUP_TRUSTED_PEERS=none).');
|
||||||
} else {
|
} else {
|
||||||
console.log('[Docker] No default gateway; Provider Settings stays loopback-only.');
|
console.log('[Docker] No default gateway; Provider Settings stays loopback-only.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
".prettierrc.json",
|
".prettierrc.json",
|
||||||
"package.json",
|
"package.json",
|
||||||
"scripts/format-scope.json",
|
"scripts/format-scope.json",
|
||||||
|
"src/ui/styles/form-field.css",
|
||||||
|
"scripts/qa-form-fields.mjs",
|
||||||
"scripts/package-boundaries.json",
|
"scripts/package-boundaries.json",
|
||||||
"scripts/format.mjs",
|
"scripts/format.mjs",
|
||||||
|
"scripts/qa-layer-visibility.mjs",
|
||||||
"scripts/next-layer-state-token.mjs",
|
"scripts/next-layer-state-token.mjs",
|
||||||
"scripts/check-layer-state-tokens.mjs",
|
"scripts/check-layer-state-tokens.mjs",
|
||||||
"scripts/qa-layer-token-twochar.mjs",
|
"scripts/qa-layer-token-twochar.mjs",
|
||||||
@@ -112,11 +115,14 @@
|
|||||||
"server/providers/cctv/catalog.js",
|
"server/providers/cctv/catalog.js",
|
||||||
"server/providers/cctv/lithuania.js",
|
"server/providers/cctv/lithuania.js",
|
||||||
"src/data/cctvLithuania.test.mjs",
|
"src/data/cctvLithuania.test.mjs",
|
||||||
|
"server/providers/cctv/taiwan.js",
|
||||||
|
"src/data/cctvTaiwan.test.mjs",
|
||||||
"server/providers/cctv/constants.js",
|
"server/providers/cctv/constants.js",
|
||||||
"server/providers/cctv/media.js",
|
"server/providers/cctv/media.js",
|
||||||
"server/providers/cctv/normalize.js",
|
"server/providers/cctv/normalize.js",
|
||||||
"src/data/cctvMissingNumbers.test.mjs",
|
"src/data/cctvMissingNumbers.test.mjs",
|
||||||
"server/providers/cctv/sources.js",
|
"server/providers/cctv/sources.js",
|
||||||
|
"src/data/cctvSourcePins.test.mjs",
|
||||||
"server/providers/radio.js",
|
"server/providers/radio.js",
|
||||||
"server/providers/radio/catalog.js",
|
"server/providers/radio/catalog.js",
|
||||||
"server/providers/radio/constants.js",
|
"server/providers/radio/constants.js",
|
||||||
@@ -188,7 +194,11 @@
|
|||||||
"src/ui/visualInput.js",
|
"src/ui/visualInput.js",
|
||||||
"src/ui/visualInput.test.mjs",
|
"src/ui/visualInput.test.mjs",
|
||||||
"scripts/qa-visual-input.mjs",
|
"scripts/qa-visual-input.mjs",
|
||||||
|
"scripts/qa-scope-hud.mjs",
|
||||||
|
"scripts/qa-segmented-controls.mjs",
|
||||||
"src/ui/displayControls.js",
|
"src/ui/displayControls.js",
|
||||||
|
"src/ui/segmentedControl.test.mjs",
|
||||||
|
"src/ui/styles/segmented-control.css",
|
||||||
"src/ui/displayControls.test.mjs",
|
"src/ui/displayControls.test.mjs",
|
||||||
"src/ui/cameraOrientationControls.js",
|
"src/ui/cameraOrientationControls.js",
|
||||||
"src/ui/cameraOrientationControls.test.mjs",
|
"src/ui/cameraOrientationControls.test.mjs",
|
||||||
@@ -276,8 +286,7 @@
|
|||||||
"src/ui/scenePresentation.js",
|
"src/ui/scenePresentation.js",
|
||||||
"src/ui/sceneControls.test.mjs",
|
"src/ui/sceneControls.test.mjs",
|
||||||
"scripts/qa-scene-controls.mjs",
|
"scripts/qa-scene-controls.mjs",
|
||||||
"src/ui.js",
|
"scripts/qa-action-buttons.mjs",
|
||||||
"src/standalone/ui.js",
|
|
||||||
"src/ui/applicationShell.js",
|
"src/ui/applicationShell.js",
|
||||||
"src/ui/panelLayoutController.js",
|
"src/ui/panelLayoutController.js",
|
||||||
"src/ui/panelPositionControls.js",
|
"src/ui/panelPositionControls.js",
|
||||||
@@ -299,6 +308,8 @@
|
|||||||
"src/ui/styles/status.css",
|
"src/ui/styles/status.css",
|
||||||
"src/ui/styles/layers.css",
|
"src/ui/styles/layers.css",
|
||||||
"src/ui/styles/controls.css",
|
"src/ui/styles/controls.css",
|
||||||
|
"src/ui/styles/slider.css",
|
||||||
|
"src/ui/styles/action-button.css",
|
||||||
"src/ui/styles/cctv.css",
|
"src/ui/styles/cctv.css",
|
||||||
"src/ui/styles/foundation.css",
|
"src/ui/styles/foundation.css",
|
||||||
"src/ui/styles/first-run.css",
|
"src/ui/styles/first-run.css",
|
||||||
@@ -443,6 +454,7 @@
|
|||||||
"src/data/satellites.js",
|
"src/data/satellites.js",
|
||||||
"src/data/rocketLaunches.js",
|
"src/data/rocketLaunches.js",
|
||||||
"src/layers/satellites/source.test.mjs",
|
"src/layers/satellites/source.test.mjs",
|
||||||
|
"src/layers/satellites/stale.test.mjs",
|
||||||
"src/layers/launches/source.test.mjs",
|
"src/layers/launches/source.test.mjs",
|
||||||
"src/layers/launches/ingestion.test.mjs",
|
"src/layers/launches/ingestion.test.mjs",
|
||||||
"src/layers/installations/lifecycle.js",
|
"src/layers/installations/lifecycle.js",
|
||||||
@@ -549,6 +561,7 @@
|
|||||||
"src/data/cctvWarendorf.test.mjs",
|
"src/data/cctvWarendorf.test.mjs",
|
||||||
"src/data/cctvCalgary.test.mjs",
|
"src/data/cctvCalgary.test.mjs",
|
||||||
"src/data/cctvCataloniaSource.test.mjs",
|
"src/data/cctvCataloniaSource.test.mjs",
|
||||||
|
"src/data/cctvHk.test.mjs",
|
||||||
"src/data/cctvVegvesen.test.mjs",
|
"src/data/cctvVegvesen.test.mjs",
|
||||||
"src/data/cctvNyc.test.mjs",
|
"src/data/cctvNyc.test.mjs",
|
||||||
"src/data/cctvNswSource.test.mjs",
|
"src/data/cctvNswSource.test.mjs",
|
||||||
@@ -1059,5 +1072,23 @@
|
|||||||
"scripts/assemble-changelog.mjs",
|
"scripts/assemble-changelog.mjs",
|
||||||
"src/tooling/changelogFragments.test.mjs",
|
"src/tooling/changelogFragments.test.mjs",
|
||||||
"src/data/gbfsProxyRateLimit.test.mjs",
|
"src/data/gbfsProxyRateLimit.test.mjs",
|
||||||
"src/terrainHeightsBounds.test.mjs"
|
"src/data/routeSegment.test.mjs",
|
||||||
|
"src/terrainHeightsBounds.test.mjs",
|
||||||
|
"scripts/build-bright-stars.mjs",
|
||||||
|
"scripts/build-milky-way.mjs",
|
||||||
|
"src/sky/stars.test.mjs",
|
||||||
|
"src/sky/starrySky.test.mjs",
|
||||||
|
"src/tooling/skyAssets.test.mjs",
|
||||||
|
"src/tooling/skyAssets.paintOut.test.mjs",
|
||||||
|
"scripts/qa-anime-sky.mjs",
|
||||||
|
"scripts/qa-sky-model-lighting.mjs",
|
||||||
|
"server/providers/overpass/requestAdmission.test.mjs",
|
||||||
|
"src/tooling/trackArrivalObservation.test.mjs",
|
||||||
|
"src/ui/styles/status-badge.css",
|
||||||
|
"src/ui/statusBadge.test.mjs",
|
||||||
|
"src/ui/cctvBadges.test.mjs",
|
||||||
|
"scripts/qa-status-badges.mjs",
|
||||||
|
"src/dockerTrustedPeers.test.mjs",
|
||||||
|
"src/cameraGlobeRotation.test.mjs",
|
||||||
|
"src/voice/globeResetConfirmation.test.mjs"
|
||||||
]
|
]
|
||||||
+128
-30
@@ -22,7 +22,14 @@
|
|||||||
},
|
},
|
||||||
"viewer": {
|
"viewer": {
|
||||||
"exports": ["./application/viewer"],
|
"exports": ["./application/viewer"],
|
||||||
"modules": ["src/app/viewer.js", "src/app/atmosphereCompat.js"],
|
"modules": [
|
||||||
|
"src/app/viewer.js",
|
||||||
|
"src/app/atmosphereCompat.js",
|
||||||
|
"src/sky/starrySky.js",
|
||||||
|
"src/sky/stars.js",
|
||||||
|
"src/data/bundledJson.js",
|
||||||
|
"src/renderGovernor.js"
|
||||||
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
"vite-build": {
|
"vite-build": {
|
||||||
@@ -96,6 +103,9 @@
|
|||||||
"modules": [
|
"modules": [
|
||||||
"server/providers/space.js",
|
"server/providers/space.js",
|
||||||
"server/providers/space/celestrak.js",
|
"server/providers/space/celestrak.js",
|
||||||
|
"server/providers/common/same-site.js",
|
||||||
|
"src/localRequestGate.mjs",
|
||||||
|
"server/providers/space/cooldown.js",
|
||||||
"server/providers/space/launch-library.js",
|
"server/providers/space/launch-library.js",
|
||||||
"server/providers/common/http.js",
|
"server/providers/common/http.js",
|
||||||
"src/data/spaceProviderRequests.js",
|
"src/data/spaceProviderRequests.js",
|
||||||
@@ -187,10 +197,13 @@
|
|||||||
"exports": ["./server/providers/cctv"],
|
"exports": ["./server/providers/cctv"],
|
||||||
"modules": [
|
"modules": [
|
||||||
"server/providers/cctv.js",
|
"server/providers/cctv.js",
|
||||||
|
"server/providers/common/same-site.js",
|
||||||
|
"src/localRequestGate.mjs",
|
||||||
"server/providers/cctv/cap.js",
|
"server/providers/cctv/cap.js",
|
||||||
"server/providers/cctv/groundHeights.js",
|
"server/providers/cctv/groundHeights.js",
|
||||||
"server/providers/cctv/catalog.js",
|
"server/providers/cctv/catalog.js",
|
||||||
"server/providers/cctv/lithuania.js",
|
"server/providers/cctv/lithuania.js",
|
||||||
|
"server/providers/cctv/taiwan.js",
|
||||||
"server/providers/cctv/constants.js",
|
"server/providers/cctv/constants.js",
|
||||||
"server/providers/cctv/media.js",
|
"server/providers/cctv/media.js",
|
||||||
"server/providers/cctv/normalize.js",
|
"server/providers/cctv/normalize.js",
|
||||||
@@ -251,6 +264,7 @@
|
|||||||
"server/providers/common/http.js",
|
"server/providers/common/http.js",
|
||||||
"server/providers/common/rate-limit.js",
|
"server/providers/common/rate-limit.js",
|
||||||
"server/providers/common/request.js",
|
"server/providers/common/request.js",
|
||||||
|
"server/providers/common/same-site.js",
|
||||||
"server/providers/overpass.js",
|
"server/providers/overpass.js",
|
||||||
"server/providers/overpass/cache.js",
|
"server/providers/overpass/cache.js",
|
||||||
"server/providers/overpass/constants.js",
|
"server/providers/overpass/constants.js",
|
||||||
@@ -260,6 +274,7 @@
|
|||||||
"server/providers/places/routes.js",
|
"server/providers/places/routes.js",
|
||||||
"src/data/placeProviderPayloads.js",
|
"src/data/placeProviderPayloads.js",
|
||||||
"src/data/routeSteps.js",
|
"src/data/routeSteps.js",
|
||||||
|
"src/localRequestGate.mjs",
|
||||||
"src/sources/httpBody.js",
|
"src/sources/httpBody.js",
|
||||||
"src/i18n/marker.js",
|
"src/i18n/marker.js",
|
||||||
"src/sources/overpass.js",
|
"src/sources/overpass.js",
|
||||||
@@ -421,16 +436,18 @@
|
|||||||
"src/ui/visualInput.js",
|
"src/ui/visualInput.js",
|
||||||
"src/ui/applicationShortcuts.js",
|
"src/ui/applicationShortcuts.js",
|
||||||
"src/ui/styleParameters.js",
|
"src/ui/styleParameters.js",
|
||||||
|
"src/ui/slider.js",
|
||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"display-controls": {
|
"display-controls": {
|
||||||
"exports": ["./ui/display"],
|
"exports": ["./ui/display"],
|
||||||
"modules": ["src/ui/displayControls.js"],
|
"modules": ["src/ui/displayControls.js", "src/ui/segmentedControl.js"],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
"visual-effects": {
|
"visual-effects": {
|
||||||
@@ -449,7 +466,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -465,7 +483,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
@@ -479,6 +498,7 @@
|
|||||||
"src/ui/weatherPanel.js",
|
"src/ui/weatherPanel.js",
|
||||||
"src/ui/railCards.js",
|
"src/ui/railCards.js",
|
||||||
"src/ui/railCardBlocks.js",
|
"src/ui/railCardBlocks.js",
|
||||||
|
"src/ui/slider.js",
|
||||||
"src/ui/chipGroup.js",
|
"src/ui/chipGroup.js",
|
||||||
"src/ui/rowList.js",
|
"src/ui/rowList.js",
|
||||||
"src/ui/railTimeline.js",
|
"src/ui/railTimeline.js",
|
||||||
@@ -493,9 +513,11 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/unitLabels.js",
|
"src/ui/unitLabels.js",
|
||||||
"src/ui/keyGuidance.js",
|
"src/ui/keyGuidance.js",
|
||||||
"src/ui/statusText.js"
|
"src/ui/statusText.js",
|
||||||
|
"src/ui/statusBadge.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
@@ -511,7 +533,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
@@ -526,7 +549,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
@@ -536,6 +560,7 @@
|
|||||||
"src/ui/cctv.js",
|
"src/ui/cctv.js",
|
||||||
"src/ui/cctvBindings.js",
|
"src/ui/cctvBindings.js",
|
||||||
"src/ui/cctvCalibration.js",
|
"src/ui/cctvCalibration.js",
|
||||||
|
"src/ui/formField.js",
|
||||||
"src/ui/cctvControls.js",
|
"src/ui/cctvControls.js",
|
||||||
"src/ui/cctvFrames.js",
|
"src/ui/cctvFrames.js",
|
||||||
"src/ui/cctvVideo.js",
|
"src/ui/cctvVideo.js",
|
||||||
@@ -544,7 +569,9 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
"src/ui/unitLabels.js"
|
"src/i18n/locales/zh-CN.json",
|
||||||
|
"src/ui/unitLabels.js",
|
||||||
|
"src/ui/statusBadge.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
@@ -568,6 +595,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/layerNames.js"
|
"src/ui/layerNames.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
@@ -601,6 +629,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/unitLabels.js"
|
"src/ui/unitLabels.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
@@ -611,10 +640,12 @@
|
|||||||
"src/ui/scenes.js",
|
"src/ui/scenes.js",
|
||||||
"src/ui/sceneControls.js",
|
"src/ui/sceneControls.js",
|
||||||
"src/ui/scenePresentation.js",
|
"src/ui/scenePresentation.js",
|
||||||
|
"src/ui/actionButton.js",
|
||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/unitLabels.js"
|
"src/ui/unitLabels.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
@@ -623,6 +654,8 @@
|
|||||||
"exports": ["./ui/shell"],
|
"exports": ["./ui/shell"],
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/i18n/marker.js",
|
"src/i18n/marker.js",
|
||||||
|
"src/ui/mobileShell.js",
|
||||||
|
"src/exclusiveSurfaces.js",
|
||||||
"src/ui/placeNames.js",
|
"src/ui/placeNames.js",
|
||||||
"src/hudLayouts.js",
|
"src/hudLayouts.js",
|
||||||
"src/hudLayoutPolicy.js",
|
"src/hudLayoutPolicy.js",
|
||||||
@@ -672,6 +705,7 @@
|
|||||||
"src/ui/cctv.js",
|
"src/ui/cctv.js",
|
||||||
"src/ui/cctvBindings.js",
|
"src/ui/cctvBindings.js",
|
||||||
"src/ui/cctvCalibration.js",
|
"src/ui/cctvCalibration.js",
|
||||||
|
"src/ui/formField.js",
|
||||||
"src/ui/cctvControls.js",
|
"src/ui/cctvControls.js",
|
||||||
"src/ui/cctvFrames.js",
|
"src/ui/cctvFrames.js",
|
||||||
"src/ui/cctvVideo.js",
|
"src/ui/cctvVideo.js",
|
||||||
@@ -700,6 +734,7 @@
|
|||||||
"src/ui/contextSubscriptions.js",
|
"src/ui/contextSubscriptions.js",
|
||||||
"src/ui/contextTransactions.js",
|
"src/ui/contextTransactions.js",
|
||||||
"src/ui/displayControls.js",
|
"src/ui/displayControls.js",
|
||||||
|
"src/ui/segmentedControl.js",
|
||||||
"src/ui/effects.js",
|
"src/ui/effects.js",
|
||||||
"src/ui/frameRateMonitor.js",
|
"src/ui/frameRateMonitor.js",
|
||||||
"src/ui/layerPanel.js",
|
"src/ui/layerPanel.js",
|
||||||
@@ -733,11 +768,13 @@
|
|||||||
"src/ui/rightPanelRail.js",
|
"src/ui/rightPanelRail.js",
|
||||||
"src/ui/sceneControls.js",
|
"src/ui/sceneControls.js",
|
||||||
"src/ui/scenePresentation.js",
|
"src/ui/scenePresentation.js",
|
||||||
|
"src/ui/actionButton.js",
|
||||||
"src/ui/scenes.js",
|
"src/ui/scenes.js",
|
||||||
"src/ui/shareRestoration.js",
|
"src/ui/shareRestoration.js",
|
||||||
"src/ui/shellElements.js",
|
"src/ui/shellElements.js",
|
||||||
"src/ui/shellFeedback.js",
|
"src/ui/shellFeedback.js",
|
||||||
"src/ui/styleParameters.js",
|
"src/ui/styleParameters.js",
|
||||||
|
"src/ui/slider.js",
|
||||||
"src/ui/surfaceKeyboard.js",
|
"src/ui/surfaceKeyboard.js",
|
||||||
"src/ui/uiLifetime.js",
|
"src/ui/uiLifetime.js",
|
||||||
"src/ui/visualEffects.js",
|
"src/ui/visualEffects.js",
|
||||||
@@ -763,10 +800,14 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/unitLabels.js",
|
"src/ui/unitLabels.js",
|
||||||
"src/ui/keyGuidance.js",
|
"src/ui/keyGuidance.js",
|
||||||
"src/ui/layerNames.js",
|
"src/ui/layerNames.js",
|
||||||
"src/ui/statusText.js"
|
"src/ui/statusText.js",
|
||||||
|
"src/ui/statusBadge.js",
|
||||||
|
"src/ui/scopeControls.js",
|
||||||
|
"src/scopeMaskPolicy.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -844,6 +885,7 @@
|
|||||||
"src/ui/scenes.js",
|
"src/ui/scenes.js",
|
||||||
"src/ui/sceneControls.js",
|
"src/ui/sceneControls.js",
|
||||||
"src/ui/scenePresentation.js",
|
"src/ui/scenePresentation.js",
|
||||||
|
"src/ui/actionButton.js",
|
||||||
"src/contextModePolicy.js",
|
"src/contextModePolicy.js",
|
||||||
"src/director/clock.js",
|
"src/director/clock.js",
|
||||||
"src/director/timeline.js",
|
"src/director/timeline.js",
|
||||||
@@ -880,6 +922,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/unitLabels.js"
|
"src/ui/unitLabels.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
@@ -916,6 +959,7 @@
|
|||||||
"src/data/layerState.js",
|
"src/data/layerState.js",
|
||||||
"src/data/layerStateTokenReservations.json",
|
"src/data/layerStateTokenReservations.json",
|
||||||
"src/data/modelVisualAnchor.js",
|
"src/data/modelVisualAnchor.js",
|
||||||
|
"src/data/aircraftModelLighting.js",
|
||||||
"src/data/motionModel.js",
|
"src/data/motionModel.js",
|
||||||
"src/data/renderAltitude.js",
|
"src/data/renderAltitude.js",
|
||||||
"src/data/routePlausible.js",
|
"src/data/routePlausible.js",
|
||||||
@@ -945,6 +989,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/unitLabels.js"
|
"src/ui/unitLabels.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
@@ -967,6 +1012,7 @@
|
|||||||
"src/data/layerState.js",
|
"src/data/layerState.js",
|
||||||
"src/data/layerStateTokenReservations.json",
|
"src/data/layerStateTokenReservations.json",
|
||||||
"src/data/modelVisualAnchor.js",
|
"src/data/modelVisualAnchor.js",
|
||||||
|
"src/data/aircraftModelLighting.js",
|
||||||
"src/data/motionModel.js",
|
"src/data/motionModel.js",
|
||||||
"src/data/renderAltitude.js",
|
"src/data/renderAltitude.js",
|
||||||
"src/data/trackedModelRegime.js",
|
"src/data/trackedModelRegime.js",
|
||||||
@@ -990,7 +1036,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -1011,6 +1058,7 @@
|
|||||||
"src/data/layerStateCoordinator.js",
|
"src/data/layerStateCoordinator.js",
|
||||||
"src/data/layerStateTokenReservations.json",
|
"src/data/layerStateTokenReservations.json",
|
||||||
"src/data/modelVisualAnchor.js",
|
"src/data/modelVisualAnchor.js",
|
||||||
|
"src/data/aircraftModelLighting.js",
|
||||||
"src/data/motionModel.js",
|
"src/data/motionModel.js",
|
||||||
"src/data/renderAltitude.js",
|
"src/data/renderAltitude.js",
|
||||||
"src/data/routePlausible.js",
|
"src/data/routePlausible.js",
|
||||||
@@ -1050,7 +1098,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -1079,6 +1128,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/unitLabels.js",
|
"src/ui/unitLabels.js",
|
||||||
"src/layers/recentImagery/display.js"
|
"src/layers/recentImagery/display.js"
|
||||||
],
|
],
|
||||||
@@ -1107,7 +1157,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -1138,6 +1189,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/unitLabels.js"
|
"src/ui/unitLabels.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "satellite.js"]
|
"external": ["cesium", "satellite.js"]
|
||||||
@@ -1169,7 +1221,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -1211,6 +1264,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/layers/installations/classLabel.js"
|
"src/layers/installations/classLabel.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
|
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
|
||||||
@@ -1245,6 +1299,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/unitLabels.js"
|
"src/ui/unitLabels.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
@@ -1293,7 +1348,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "pbf", "@mapbox/vector-tile"]
|
"external": ["cesium", "pbf", "@mapbox/vector-tile"]
|
||||||
},
|
},
|
||||||
@@ -1321,7 +1377,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -1368,7 +1425,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["hls.js", "cesium"]
|
"external": ["hls.js", "cesium"]
|
||||||
},
|
},
|
||||||
@@ -1377,7 +1435,9 @@
|
|||||||
"./layers/street-level",
|
"./layers/street-level",
|
||||||
"./layers/street-level/mapillary",
|
"./layers/street-level/mapillary",
|
||||||
"./layers/street-level/mapillary/source",
|
"./layers/street-level/mapillary/source",
|
||||||
"./layers/street-level/google"
|
"./layers/street-level/google",
|
||||||
|
"./layers/street-level/kartaview",
|
||||||
|
"./layers/street-level/panoramax"
|
||||||
],
|
],
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/layers/streetLevel/index.js",
|
"src/layers/streetLevel/index.js",
|
||||||
@@ -1429,6 +1489,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/layers/streetLevel/providers/mapillary/policy.js"
|
"src/layers/streetLevel/providers/mapillary/policy.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"]
|
"external": ["cesium", "pbf", "@mapbox/vector-tile", "mapillary-js"]
|
||||||
@@ -1456,7 +1517,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
|
"external": ["cesium", "@mapbox/vector-tile", "pbf"]
|
||||||
},
|
},
|
||||||
@@ -1495,7 +1557,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -1516,7 +1579,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -1571,7 +1635,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -1627,6 +1692,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/statusText.js",
|
"src/ui/statusText.js",
|
||||||
"src/voice/providerCommands.js",
|
"src/voice/providerCommands.js",
|
||||||
"src/voice/providerCapabilities.js",
|
"src/voice/providerCapabilities.js",
|
||||||
@@ -1715,6 +1781,8 @@
|
|||||||
"src/app/surfaceServices.js",
|
"src/app/surfaceServices.js",
|
||||||
"src/app/tools.js",
|
"src/app/tools.js",
|
||||||
"src/app/viewer.js",
|
"src/app/viewer.js",
|
||||||
|
"src/sky/starrySky.js",
|
||||||
|
"src/sky/stars.js",
|
||||||
"src/app/atmosphereCompat.js",
|
"src/app/atmosphereCompat.js",
|
||||||
"src/bloom.js",
|
"src/bloom.js",
|
||||||
"src/camera.js",
|
"src/camera.js",
|
||||||
@@ -1796,7 +1864,6 @@
|
|||||||
"src/data/localGeojson.js",
|
"src/data/localGeojson.js",
|
||||||
"src/data/localGeojsonCore.js",
|
"src/data/localGeojsonCore.js",
|
||||||
"src/data/localGeojsonLod.js",
|
"src/data/localGeojsonLod.js",
|
||||||
"src/data/localLayers.js",
|
|
||||||
"src/data/local_data/natural_earth/marine.json",
|
"src/data/local_data/natural_earth/marine.json",
|
||||||
"src/data/local_data/natural_earth/regions.json",
|
"src/data/local_data/natural_earth/regions.json",
|
||||||
"src/data/local_data/neighborhoods/san-francisco.json",
|
"src/data/local_data/neighborhoods/san-francisco.json",
|
||||||
@@ -1810,6 +1877,7 @@
|
|||||||
"src/data/militaryInstallations.js",
|
"src/data/militaryInstallations.js",
|
||||||
"src/data/militaryRegistry.js",
|
"src/data/militaryRegistry.js",
|
||||||
"src/data/modelVisualAnchor.js",
|
"src/data/modelVisualAnchor.js",
|
||||||
|
"src/data/aircraftModelLighting.js",
|
||||||
"src/data/motionModel.js",
|
"src/data/motionModel.js",
|
||||||
"src/data/naturalEarthRegions.js",
|
"src/data/naturalEarthRegions.js",
|
||||||
"src/data/adminBoundaries.js",
|
"src/data/adminBoundaries.js",
|
||||||
@@ -1850,6 +1918,7 @@
|
|||||||
"src/data/vesselLabels.js",
|
"src/data/vesselLabels.js",
|
||||||
"src/data/vesselTypes.js",
|
"src/data/vesselTypes.js",
|
||||||
"src/firstRunExperience.js",
|
"src/firstRunExperience.js",
|
||||||
|
"src/exclusiveSurfaces.js",
|
||||||
"src/hud.js",
|
"src/hud.js",
|
||||||
"src/hudLocality.js",
|
"src/hudLocality.js",
|
||||||
"src/hudLocalityFormat.js",
|
"src/hudLocalityFormat.js",
|
||||||
@@ -2137,6 +2206,8 @@
|
|||||||
"src/styles/surveillance.js",
|
"src/styles/surveillance.js",
|
||||||
"src/styles/thermal.js",
|
"src/styles/thermal.js",
|
||||||
"src/ui/applicationShell.js",
|
"src/ui/applicationShell.js",
|
||||||
|
"src/ui/mobileShell.js",
|
||||||
|
"src/exclusiveSurfaces.js",
|
||||||
"src/ui/localePicker.js",
|
"src/ui/localePicker.js",
|
||||||
"src/ui/cyberSonarControls.js",
|
"src/ui/cyberSonarControls.js",
|
||||||
"src/ui/locationNavigation.js",
|
"src/ui/locationNavigation.js",
|
||||||
@@ -2146,6 +2217,7 @@
|
|||||||
"src/ui/cctv.js",
|
"src/ui/cctv.js",
|
||||||
"src/ui/cctvBindings.js",
|
"src/ui/cctvBindings.js",
|
||||||
"src/ui/cctvCalibration.js",
|
"src/ui/cctvCalibration.js",
|
||||||
|
"src/ui/formField.js",
|
||||||
"src/ui/cctvControls.js",
|
"src/ui/cctvControls.js",
|
||||||
"src/ui/cctvFrames.js",
|
"src/ui/cctvFrames.js",
|
||||||
"src/ui/cctvVideo.js",
|
"src/ui/cctvVideo.js",
|
||||||
@@ -2175,6 +2247,7 @@
|
|||||||
"src/ui/contextSubscriptions.js",
|
"src/ui/contextSubscriptions.js",
|
||||||
"src/ui/contextTransactions.js",
|
"src/ui/contextTransactions.js",
|
||||||
"src/ui/displayControls.js",
|
"src/ui/displayControls.js",
|
||||||
|
"src/ui/segmentedControl.js",
|
||||||
"src/ui/effects.js",
|
"src/ui/effects.js",
|
||||||
"src/ui/frameRateMonitor.js",
|
"src/ui/frameRateMonitor.js",
|
||||||
"src/ui/layerPanel.js",
|
"src/ui/layerPanel.js",
|
||||||
@@ -2208,11 +2281,13 @@
|
|||||||
"src/ui/rightPanelRail.js",
|
"src/ui/rightPanelRail.js",
|
||||||
"src/ui/sceneControls.js",
|
"src/ui/sceneControls.js",
|
||||||
"src/ui/scenePresentation.js",
|
"src/ui/scenePresentation.js",
|
||||||
|
"src/ui/actionButton.js",
|
||||||
"src/ui/scenes.js",
|
"src/ui/scenes.js",
|
||||||
"src/ui/shareRestoration.js",
|
"src/ui/shareRestoration.js",
|
||||||
"src/ui/shellElements.js",
|
"src/ui/shellElements.js",
|
||||||
"src/ui/shellFeedback.js",
|
"src/ui/shellFeedback.js",
|
||||||
"src/ui/styleParameters.js",
|
"src/ui/styleParameters.js",
|
||||||
|
"src/ui/slider.js",
|
||||||
"src/ui/surfaceKeyboard.js",
|
"src/ui/surfaceKeyboard.js",
|
||||||
"src/ui/uiLifetime.js",
|
"src/ui/uiLifetime.js",
|
||||||
"src/ui/visualEffects.js",
|
"src/ui/visualEffects.js",
|
||||||
@@ -2482,6 +2557,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/unitLabels.js",
|
"src/ui/unitLabels.js",
|
||||||
"src/layers/recentImagery/display.js",
|
"src/layers/recentImagery/display.js",
|
||||||
"src/ui/keyGuidance.js",
|
"src/ui/keyGuidance.js",
|
||||||
@@ -2499,7 +2575,11 @@
|
|||||||
"src/voice/providerCapabilities.js",
|
"src/voice/providerCapabilities.js",
|
||||||
"src/voice/geminiSession.js",
|
"src/voice/geminiSession.js",
|
||||||
"src/voice/geminiAudio.js",
|
"src/voice/geminiAudio.js",
|
||||||
"src/voice/geminiCaptureWorklet.js"
|
"src/voice/geminiCaptureWorklet.js",
|
||||||
|
"src/ui/statusBadge.js",
|
||||||
|
"src/sceneCommandRelease.js",
|
||||||
|
"src/ui/scopeControls.js",
|
||||||
|
"src/scopeMaskPolicy.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"hls.js",
|
"hls.js",
|
||||||
@@ -2610,6 +2690,7 @@
|
|||||||
"src/data/installationNames.js",
|
"src/data/installationNames.js",
|
||||||
"src/data/militaryInstallationData.js",
|
"src/data/militaryInstallationData.js",
|
||||||
"src/data/modelVisualAnchor.js",
|
"src/data/modelVisualAnchor.js",
|
||||||
|
"src/data/aircraftModelLighting.js",
|
||||||
"src/data/motionModel.js",
|
"src/data/motionModel.js",
|
||||||
"src/data/naturalEarthRegions.js",
|
"src/data/naturalEarthRegions.js",
|
||||||
"src/data/adminBoundaries.js",
|
"src/data/adminBoundaries.js",
|
||||||
@@ -3056,6 +3137,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/unitLabels.js",
|
"src/ui/unitLabels.js",
|
||||||
"src/layers/recentImagery/display.js",
|
"src/layers/recentImagery/display.js",
|
||||||
"src/layers/installations/classLabel.js",
|
"src/layers/installations/classLabel.js",
|
||||||
@@ -3162,6 +3244,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/i18n/index.js"
|
"src/i18n/index.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
@@ -3182,7 +3265,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
@@ -3206,15 +3290,20 @@
|
|||||||
"src/ui/applicationShortcuts.js",
|
"src/ui/applicationShortcuts.js",
|
||||||
"src/ui/effects.js",
|
"src/ui/effects.js",
|
||||||
"src/ui/styleParameters.js",
|
"src/ui/styleParameters.js",
|
||||||
|
"src/ui/slider.js",
|
||||||
"src/ui/uiLifetime.js",
|
"src/ui/uiLifetime.js",
|
||||||
"src/ui/visualEffects.js",
|
"src/ui/visualEffects.js",
|
||||||
"src/ui/visualInput.js",
|
"src/ui/visualInput.js",
|
||||||
"src/ui/visualPresets.js",
|
"src/ui/visualPresets.js",
|
||||||
"src/ui/visualSettings.js",
|
"src/ui/visualSettings.js",
|
||||||
|
"src/ui/segmentedControl.js",
|
||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
|
"src/ui/scopeControls.js",
|
||||||
|
"src/scopeMaskPolicy.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -3237,7 +3326,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
@@ -3328,6 +3418,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/unitLabels.js"
|
"src/ui/unitLabels.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
@@ -3382,6 +3473,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/statusText.js",
|
"src/ui/statusText.js",
|
||||||
"src/voice/providerCapabilities.js"
|
"src/voice/providerCapabilities.js"
|
||||||
],
|
],
|
||||||
@@ -3592,6 +3684,7 @@
|
|||||||
"src/data/labelArbiter.js",
|
"src/data/labelArbiter.js",
|
||||||
"src/data/meshFloorSampler.js",
|
"src/data/meshFloorSampler.js",
|
||||||
"src/data/pickRegistry.js",
|
"src/data/pickRegistry.js",
|
||||||
|
"src/data/scenePick.js",
|
||||||
"src/data/spriteOrder.js",
|
"src/data/spriteOrder.js",
|
||||||
"src/data/terrainHeights.js",
|
"src/data/terrainHeights.js",
|
||||||
"src/data/transit.js",
|
"src/data/transit.js",
|
||||||
@@ -3634,7 +3727,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "egm96-universal", "pbf"]
|
"external": ["cesium", "egm96-universal", "pbf"]
|
||||||
},
|
},
|
||||||
@@ -3676,7 +3770,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "pbf"]
|
"external": ["cesium", "pbf"]
|
||||||
},
|
},
|
||||||
@@ -3728,7 +3823,8 @@
|
|||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json"
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -3744,6 +3840,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/statusText.js",
|
"src/ui/statusText.js",
|
||||||
"src/ui/layerNames.js"
|
"src/ui/layerNames.js"
|
||||||
],
|
],
|
||||||
@@ -3936,6 +4033,7 @@
|
|||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
"src/i18n/locales/es.json",
|
"src/i18n/locales/es.json",
|
||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/ui/statusText.js",
|
"src/ui/statusText.js",
|
||||||
"src/ui/layerNames.js"
|
"src/ui/layerNames.js"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -326,8 +326,10 @@ async function main() {
|
|||||||
'src/data/local_data/cctv_ground_heights/cctv_ground_heights.json',
|
'src/data/local_data/cctv_ground_heights/cctv_ground_heights.json',
|
||||||
);
|
);
|
||||||
let sidecar = await loadSidecar(output);
|
let sidecar = await loadSidecar(output);
|
||||||
|
// A cold catalog waits for its slowest pack (Tarktee gets 30 s), so allow
|
||||||
|
// more than that.
|
||||||
const response = await fetch(`${base}/api/cctv/sources`, {
|
const response = await fetch(`${base}/api/cctv/sources`, {
|
||||||
signal: AbortSignal.timeout(30_000),
|
signal: AbortSignal.timeout(60_000),
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(`Catalog HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`Catalog HTTP ${response.status}`);
|
||||||
const catalog = await response.json();
|
const catalog = await response.json();
|
||||||
|
|||||||
@@ -0,0 +1,335 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Scenes/CCTV action-button acceptance against dev or production preview:
|
||||||
|
* QA_BASE_URL=http://127.0.0.1:4179 node scripts/qa-action-buttons.mjs
|
||||||
|
* Uses the installed CCTV controller with fixture camera/action ports to prove
|
||||||
|
* native dispatch/disabled/teardown without providers or world calibration writes.
|
||||||
|
* Real Scene authoring/playback remains covered by qa-scene-controls.mjs.
|
||||||
|
*/
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--use-gl=angle', '--use-angle=swiftshader'],
|
||||||
|
});
|
||||||
|
const page = await browser.newPage();
|
||||||
|
const errors = [];
|
||||||
|
page.on('pageerror', (error) => errors.push(error.message));
|
||||||
|
let failures = 0;
|
||||||
|
const check = (name, passed, detail) => {
|
||||||
|
console.log(`[${passed ? 'PASS' : 'FAIL'}] ${name}`);
|
||||||
|
if (!passed) {
|
||||||
|
failures++;
|
||||||
|
console.log(JSON.stringify(detail));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const shots = 'qa-shots/action-buttons';
|
||||||
|
fs.mkdirSync(shots, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.setViewport({ width: 1440, height: 900 });
|
||||||
|
await page.goto(
|
||||||
|
`${process.env.QA_BASE_URL || 'http://127.0.0.1:4179'}/?welcome=0`,
|
||||||
|
{ waitUntil: 'domcontentloaded' },
|
||||||
|
);
|
||||||
|
await page.waitForFunction(
|
||||||
|
() =>
|
||||||
|
window.__godsEyeView?.styleManager?._cctvControls &&
|
||||||
|
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||||
|
{ timeout: 60000 },
|
||||||
|
);
|
||||||
|
const inventory = await page.evaluate(() => {
|
||||||
|
const ids = [
|
||||||
|
'scene-new-btn',
|
||||||
|
'scene-delete-btn',
|
||||||
|
'scene-capture-btn',
|
||||||
|
'scene-update-shot-btn',
|
||||||
|
'scene-start-btn',
|
||||||
|
'scene-stop-btn',
|
||||||
|
'scene-next-btn',
|
||||||
|
'scene-export-btn',
|
||||||
|
'scene-import-btn',
|
||||||
|
'scene-download-btn',
|
||||||
|
'cctv-nearest-btn',
|
||||||
|
'cctv-prev-btn',
|
||||||
|
'cctv-next-btn',
|
||||||
|
'cctv-focus-btn',
|
||||||
|
'cctv-calib-save-btn',
|
||||||
|
'cctv-calib-reset-btn',
|
||||||
|
];
|
||||||
|
const ordinary = ids.map((id) => document.getElementById(id));
|
||||||
|
const generated = [
|
||||||
|
...document.querySelectorAll(
|
||||||
|
'.scene-shot-btn, [data-director-authoring] button',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
const toggles = [
|
||||||
|
'cctv-enable-btn',
|
||||||
|
'cctv-coverage-btn',
|
||||||
|
'cctv-auto-hop-btn',
|
||||||
|
'cctv-projection-btn',
|
||||||
|
'cctv-adjust-btn',
|
||||||
|
].map((id) => document.getElementById(id));
|
||||||
|
return {
|
||||||
|
shared: [...ordinary, ...generated].every(
|
||||||
|
(node) =>
|
||||||
|
node?.type === 'button' &&
|
||||||
|
node.classList.contains('gev-action-button'),
|
||||||
|
),
|
||||||
|
generated: generated.length,
|
||||||
|
layout: ordinary.every((node) => {
|
||||||
|
const style = getComputedStyle(node);
|
||||||
|
return style.minWidth === '72px' && style.flexGrow === '1';
|
||||||
|
}),
|
||||||
|
togglesSeparate: toggles.every(
|
||||||
|
(node) => node && !node.classList.contains('gev-action-button'),
|
||||||
|
),
|
||||||
|
compactDanger: !!document.querySelector(
|
||||||
|
'.scene-shot-danger.gev-action-button--compact.gev-action-button--danger',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
check(
|
||||||
|
'static and generated actions share the native component',
|
||||||
|
inventory.shared && inventory.generated >= 4 && inventory.compactDanger,
|
||||||
|
inventory,
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
'persistent CCTV toggles retain their existing controls',
|
||||||
|
inventory.togglesSeparate,
|
||||||
|
);
|
||||||
|
check('ordinary actions retain their panel sizing', inventory.layout);
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const manager = window.__godsEyeView.styleManager;
|
||||||
|
const owner = manager._cctvControls;
|
||||||
|
owner._cctvUnsubscribe?.();
|
||||||
|
owner._cctvUnsubscribe = null;
|
||||||
|
window.__qaButtonCalls = [];
|
||||||
|
const record = (...args) => window.__qaButtonCalls.push(args);
|
||||||
|
owner.actions = {
|
||||||
|
...owner.actions,
|
||||||
|
isEnabled: () => true,
|
||||||
|
toggleEnabled: async () => {
|
||||||
|
record('enable');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
runExplicitFocus: (select, focus) => focus(select()),
|
||||||
|
setParams: (params, options) => record('params', params, options),
|
||||||
|
showToast: () => {},
|
||||||
|
};
|
||||||
|
owner.cctv = {
|
||||||
|
focusNearest: () => {
|
||||||
|
record('nearest');
|
||||||
|
return 'a';
|
||||||
|
},
|
||||||
|
cycleCamera: (direction) => {
|
||||||
|
record('cycle', direction);
|
||||||
|
return 'b';
|
||||||
|
},
|
||||||
|
focusCamera: (id, seconds) => record('focus', id, seconds),
|
||||||
|
};
|
||||||
|
window.__qaCameraState = {
|
||||||
|
enabled: true,
|
||||||
|
activeCameraId: 'a',
|
||||||
|
cameras: [
|
||||||
|
{ id: 'a', city: 'Fixture', name: 'A' },
|
||||||
|
{ id: 'b', city: 'Fixture', name: 'B' },
|
||||||
|
],
|
||||||
|
activeCamera: { id: 'a', name: 'A', basePose: {} },
|
||||||
|
};
|
||||||
|
owner._renderCctvState(window.__qaCameraState);
|
||||||
|
manager.setPanelCollapsed('cctv-panel', false, {
|
||||||
|
persist: false,
|
||||||
|
syncShare: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await page.focus('#cctv-next-btn');
|
||||||
|
await page.keyboard.press('Space');
|
||||||
|
await page.waitForFunction(() => window.__qaButtonCalls.length === 3);
|
||||||
|
check(
|
||||||
|
'native Space dispatches Next and its focus once',
|
||||||
|
await page.evaluate(
|
||||||
|
() =>
|
||||||
|
JSON.stringify(window.__qaButtonCalls) ===
|
||||||
|
JSON.stringify([['enable'], ['cycle', 1], ['focus', 'b', 1.4]]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
'native keyboard activation keeps a visible focus ring',
|
||||||
|
await page.$eval(
|
||||||
|
'#cctv-next-btn',
|
||||||
|
(node) =>
|
||||||
|
document.activeElement === node &&
|
||||||
|
getComputedStyle(node).outlineStyle === 'solid',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.__qaButtonCalls = [];
|
||||||
|
});
|
||||||
|
await page.focus('#cctv-prev-btn');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.waitForFunction(() => window.__qaButtonCalls.length === 3);
|
||||||
|
check(
|
||||||
|
'native Enter dispatches Previous once',
|
||||||
|
await page.evaluate(
|
||||||
|
() =>
|
||||||
|
JSON.stringify(window.__qaButtonCalls) ===
|
||||||
|
JSON.stringify([['enable'], ['cycle', -1], ['focus', 'b', 1.4]]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [id, expected] of [
|
||||||
|
['cctv-nearest-btn', [['enable'], ['nearest'], ['focus', 'a', 1.8]]],
|
||||||
|
[
|
||||||
|
'cctv-focus-btn',
|
||||||
|
[
|
||||||
|
['enable'],
|
||||||
|
['focus', 'a', 1.9],
|
||||||
|
['params', { selectedCameraId: 'a' }, { origin: 'user' }],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]) {
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.__qaButtonCalls = [];
|
||||||
|
});
|
||||||
|
await page.click(`#${id}`);
|
||||||
|
await page.waitForFunction(() => window.__qaButtonCalls.length === 3);
|
||||||
|
const calls = await page.evaluate(() => window.__qaButtonCalls);
|
||||||
|
check(
|
||||||
|
`${id} retains its existing action and focus timing`,
|
||||||
|
JSON.stringify(calls) === JSON.stringify(expected),
|
||||||
|
calls,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.__qaButtonCalls = [];
|
||||||
|
});
|
||||||
|
await page.click('#cctv-calib-save-btn');
|
||||||
|
await page.click('#cctv-calib-reset-btn');
|
||||||
|
const calibration = await page.evaluate(() => window.__qaButtonCalls);
|
||||||
|
check(
|
||||||
|
'Save and Reset retain camera identity and user-origin dispatch',
|
||||||
|
JSON.stringify(calibration) ===
|
||||||
|
JSON.stringify([
|
||||||
|
[
|
||||||
|
'params',
|
||||||
|
{ selectedCameraId: 'a', calibration: { cameraId: 'a', save: true } },
|
||||||
|
{ origin: 'user' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'params',
|
||||||
|
{
|
||||||
|
selectedCameraId: 'a',
|
||||||
|
calibration: { cameraId: 'a', reset: true },
|
||||||
|
},
|
||||||
|
{ origin: 'user' },
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
calibration,
|
||||||
|
);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.__qaButtonCalls = [];
|
||||||
|
window.__godsEyeView.styleManager._cctvControls._renderCctvState({
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await page.click('#cctv-next-btn');
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => {
|
||||||
|
const node = document.getElementById('cctv-next-btn');
|
||||||
|
return node.disabled && Number(getComputedStyle(node).opacity) === 0.45;
|
||||||
|
},
|
||||||
|
{ timeout: 3000 },
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
'disabled actions stay inert and keep disabled presentation',
|
||||||
|
await page.$eval(
|
||||||
|
'#cctv-next-btn',
|
||||||
|
(node) =>
|
||||||
|
node.disabled &&
|
||||||
|
getComputedStyle(node).opacity === '0.45' &&
|
||||||
|
window.__qaButtonCalls.length === 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const width of [1440, 620, 390]) {
|
||||||
|
await page.setViewport({ width, height: 900 });
|
||||||
|
for (const panel of ['scene-panel', 'cctv-panel']) {
|
||||||
|
await page.evaluate((panel) => {
|
||||||
|
window.__godsEyeView.styleManager.setPanelCollapsed(panel, false, {
|
||||||
|
persist: false,
|
||||||
|
syncShare: false,
|
||||||
|
});
|
||||||
|
}, panel);
|
||||||
|
const fit = await page.$eval(`#${panel}`, (root) => {
|
||||||
|
const buttons = [...root.querySelectorAll('.gev-action-button')];
|
||||||
|
const rect = root.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
count: buttons.length,
|
||||||
|
fits:
|
||||||
|
buttons.length > 0 &&
|
||||||
|
buttons.every((button) => {
|
||||||
|
const box = button.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
box.width > 0 &&
|
||||||
|
box.left >= rect.left &&
|
||||||
|
box.right <= rect.right + 1 &&
|
||||||
|
box.left >= 0 &&
|
||||||
|
box.right <= innerWidth + 1 &&
|
||||||
|
button.scrollWidth <= button.clientWidth
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
check(`${panel} actions fit at ${width}px`, fit.fits, fit);
|
||||||
|
if (width !== 620)
|
||||||
|
await page.screenshot({ path: `${shots}/${panel}-${width}.png` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const owner = window.__godsEyeView.styleManager._cctvControls;
|
||||||
|
owner.destroy();
|
||||||
|
document.getElementById('cctv-next-btn').disabled = false;
|
||||||
|
window.__qaButtonCalls = [];
|
||||||
|
});
|
||||||
|
await page.click('#cctv-next-btn');
|
||||||
|
check(
|
||||||
|
'destroyed controllers revoke action listeners',
|
||||||
|
await page.evaluate(() => window.__qaButtonCalls.length === 0),
|
||||||
|
);
|
||||||
|
const localized = new URL(page.url());
|
||||||
|
localized.searchParams.set('lang', 'ru');
|
||||||
|
await page.goto(localized.href, { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForFunction(
|
||||||
|
() =>
|
||||||
|
window.__godsEyeView?.styleManager?._cctvControls &&
|
||||||
|
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||||
|
{ timeout: 60000 },
|
||||||
|
);
|
||||||
|
for (const panel of ['scene-panel', 'cctv-panel']) {
|
||||||
|
await page.evaluate((panel) => {
|
||||||
|
window.__godsEyeView.styleManager.setPanelCollapsed(panel, false, {
|
||||||
|
persist: false,
|
||||||
|
syncShare: false,
|
||||||
|
});
|
||||||
|
}, panel);
|
||||||
|
const translated = await page.$eval(`#${panel}`, (root) => {
|
||||||
|
const buttons = [...root.querySelectorAll('.gev-action-button')];
|
||||||
|
return (
|
||||||
|
document.documentElement.lang === 'ru' &&
|
||||||
|
buttons.length > 0 &&
|
||||||
|
buttons.every((node) => node.scrollWidth <= node.clientWidth)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
check(`Russian ${panel} action labels fit at 390px`, translated);
|
||||||
|
await page.screenshot({ path: `${shots}/${panel}-ru-390.png` });
|
||||||
|
}
|
||||||
|
check('no uncaught browser errors', errors.length === 0, errors);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
console.log(`RESULT: ${failures} failures`);
|
||||||
|
process.exitCode = failures ? 1 : 0;
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* qa-anime-sky — the anime style must leave the sky without bands.
|
||||||
|
*
|
||||||
|
* Anime's luminance bands turn the faint Milky Way into blotches, so
|
||||||
|
* src/ui/visualEffects.js fades the glow out with the style. Headless against
|
||||||
|
* a Vite dev server, both sky assets must be ready. Normal must show the
|
||||||
|
* Galactic Centre glow (at least 2 % of pixels at luma >= 10/255), then
|
||||||
|
* Anime must retain stars while at most 0.5 % reaches the first cel band.
|
||||||
|
* The Normal baseline measured 43 % in headless Chromium at 1000×700;
|
||||||
|
* the 2 % floor leaves room for GPU differences while rejecting a black sky.
|
||||||
|
*
|
||||||
|
* Usage: node scripts/qa-anime-sky.mjs [--url http://localhost:4333/]
|
||||||
|
* Exit: 0 pass, 1 fail, 2 the anime style was not reached.
|
||||||
|
*/
|
||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
|
||||||
|
const i = process.argv.indexOf('--url');
|
||||||
|
const URL_ =
|
||||||
|
i > 0 && process.argv[i + 1] ? process.argv[i + 1] : 'http://localhost:4333/';
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: 'new',
|
||||||
|
args: ['--no-sandbox', '--ignore-gpu-blocklist'],
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setRequestInterception(true);
|
||||||
|
page.on('request', (request) => {
|
||||||
|
const host = new URL(request.url()).hostname;
|
||||||
|
if (host.endsWith('halfpixel.ai') || host.includes('worldfx'))
|
||||||
|
request.abort();
|
||||||
|
else request.continue();
|
||||||
|
});
|
||||||
|
page.on('pageerror', (e) => console.log('PAGEERR', String(e).slice(0, 300)));
|
||||||
|
await page.setViewport({ width: 1000, height: 700 });
|
||||||
|
await page.evaluateOnNewDocument(() =>
|
||||||
|
localStorage.setItem('gev:first-run-mission:v1', 'suppressed'),
|
||||||
|
);
|
||||||
|
await page.goto(URL_, { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForFunction(() => window.__godsEyeView?.viewer?.scene, {
|
||||||
|
timeout: 120000,
|
||||||
|
});
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => {
|
||||||
|
const sky = window.__godsEyeView.viewer.scene.skyBox;
|
||||||
|
return (
|
||||||
|
sky?._milkyWayCubeMap &&
|
||||||
|
sky?._starCommand &&
|
||||||
|
document.getElementById('loading-screen')?.classList.contains('hidden')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ timeout: 60000 },
|
||||||
|
);
|
||||||
|
const r = await page.evaluate(async () => {
|
||||||
|
const entry = performance
|
||||||
|
.getEntriesByType('resource')
|
||||||
|
.map((e) => e.name)
|
||||||
|
.find((n) => /\.vite\/deps\/cesium\.js/.test(n));
|
||||||
|
if (!entry)
|
||||||
|
throw new Error(
|
||||||
|
'run against the Vite dev server (needs .vite/deps/cesium.js)',
|
||||||
|
);
|
||||||
|
const C = await import(entry),
|
||||||
|
g = window.__godsEyeView,
|
||||||
|
v = g.viewer,
|
||||||
|
s = v.scene;
|
||||||
|
g.styleManager.setStyle('normal', { userInitiated: true });
|
||||||
|
const t = v.clock.currentTime;
|
||||||
|
await C.Transforms.preloadIcrfFixed(
|
||||||
|
new C.TimeInterval({
|
||||||
|
start: C.JulianDate.addDays(t, -1, new C.JulianDate()),
|
||||||
|
stop: C.JulianDate.addDays(t, 1, new C.JulianDate()),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const m = C.Transforms.computeIcrfToFixedMatrix(t);
|
||||||
|
const ra = C.Math.toRadians(266.4),
|
||||||
|
dec = C.Math.toRadians(-29);
|
||||||
|
const look = C.Matrix3.multiplyByVector(
|
||||||
|
m,
|
||||||
|
new C.Cartesian3(
|
||||||
|
Math.cos(dec) * Math.cos(ra),
|
||||||
|
Math.cos(dec) * Math.sin(ra),
|
||||||
|
Math.sin(dec),
|
||||||
|
),
|
||||||
|
new C.Cartesian3(),
|
||||||
|
);
|
||||||
|
const north = C.Matrix3.multiplyByVector(
|
||||||
|
m,
|
||||||
|
C.Cartesian3.UNIT_Z,
|
||||||
|
new C.Cartesian3(),
|
||||||
|
);
|
||||||
|
const right = C.Cartesian3.normalize(
|
||||||
|
C.Cartesian3.cross(look, north, new C.Cartesian3()),
|
||||||
|
new C.Cartesian3(),
|
||||||
|
);
|
||||||
|
s.camera.cancelFlight();
|
||||||
|
s.tweens.removeAll();
|
||||||
|
s.camera.setView({
|
||||||
|
destination: C.Cartesian3.multiplyByScalar(look, 3e7, new C.Cartesian3()),
|
||||||
|
orientation: {
|
||||||
|
direction: look,
|
||||||
|
up: C.Cartesian3.cross(right, look, new C.Cartesian3()),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const gl = s.context._gl,
|
||||||
|
w = gl.drawingBufferWidth,
|
||||||
|
h = gl.drawingBufferHeight,
|
||||||
|
px = new Uint8Array(w * h * 4);
|
||||||
|
const measure = async () => {
|
||||||
|
await new Promise((res) => setTimeout(res, 2500));
|
||||||
|
s.requestRender();
|
||||||
|
s.render();
|
||||||
|
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
|
||||||
|
let banded = 0;
|
||||||
|
let visible = 0;
|
||||||
|
for (let i = 0; i < px.length; i += 4) {
|
||||||
|
const luma = 0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2];
|
||||||
|
if (luma >= 51) banded++;
|
||||||
|
if (luma >= 10) visible++;
|
||||||
|
}
|
||||||
|
return { banded, visible };
|
||||||
|
};
|
||||||
|
const normal = await measure();
|
||||||
|
g.styleManager.setStyle('anime', { userInitiated: true });
|
||||||
|
const anime = await measure();
|
||||||
|
let animeOn = false;
|
||||||
|
for (let i = 0; i < s.postProcessStages.length; i++) {
|
||||||
|
const stage = s.postProcessStages.get(i);
|
||||||
|
if (stage.name === 'godsEyeView_anime')
|
||||||
|
animeOn = stage.enabled && stage.uniforms.intensity > 0.99;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
total: px.length / 4,
|
||||||
|
normal,
|
||||||
|
...anime,
|
||||||
|
assetsReady: Boolean(s.skyBox._milkyWayCubeMap && s.skyBox._starCommand),
|
||||||
|
visibility: s.skyBox.milkyWayVisibility,
|
||||||
|
animeOn,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const tag =
|
||||||
|
!r.animeOn || !r.assetsReady
|
||||||
|
? 'UNREACHED'
|
||||||
|
: r.normal.visible >= 0.02 * r.total &&
|
||||||
|
r.banded > 0 &&
|
||||||
|
r.banded <= 0.005 * r.total &&
|
||||||
|
r.visibility === 0
|
||||||
|
? 'PASS'
|
||||||
|
: 'FAIL';
|
||||||
|
console.log(
|
||||||
|
`${tag}: ${r.banded} of ${r.total} sky pixels at or above the first cel band (luma >= 0.2)\n ${JSON.stringify(r)}`,
|
||||||
|
);
|
||||||
|
process.exitCode = tag === 'PASS' ? 0 : tag === 'FAIL' ? 1 : 2;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`UNREACHED: ${error.message}`);
|
||||||
|
process.exitCode = 2;
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
@@ -370,6 +370,9 @@ try {
|
|||||||
await page.screenshot({ path: 'qa-shots/camera-controls/angle.png' });
|
await page.screenshot({ path: 'qa-shots/camera-controls/angle.png' });
|
||||||
await page.setViewport({ width: 390, height: 844 });
|
await page.setViewport({ width: 390, height: 844 });
|
||||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||||
|
// Phones present CCTV in the Cameras tab's sheet.
|
||||||
|
await page.click('#mobile-tab-bar [data-mobile-sheet="cameras"]');
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||||
await page.screenshot({ path: 'qa-shots/camera-controls/narrow.png' });
|
await page.screenshot({ path: 'qa-shots/camera-controls/narrow.png' });
|
||||||
check(
|
check(
|
||||||
'narrow camera panel remains within the viewport',
|
'narrow camera panel remains within the viewport',
|
||||||
|
|||||||
+241
-107
@@ -11,8 +11,10 @@
|
|||||||
* exactly once per camera ACTIVATION (the §9.1 obstruction probe) and
|
* exactly once per camera ACTIVATION (the §9.1 obstruction probe) and
|
||||||
* never again in steady state; shared mesh-floor sampling fires at most
|
* never again in steady state; shared mesh-floor sampling fires at most
|
||||||
* once per coarse cell during the staggered geometry-load drain, and
|
* once per coarse cell during the staggered geometry-load drain, and
|
||||||
* actually COMPLETES once tiles load (>=1 real sample — the
|
* actually COMPLETES once tiles load in photoreal mode (>=1 real sample
|
||||||
* ceiling alone would pass trivially at Δ=0 if the snap never finished),
|
* — the ceiling alone would pass trivially at Δ=0 if the snap never
|
||||||
|
* finished). Globe mode instead proves the rendered mount uses a real
|
||||||
|
* terrain height with zero mesh samples. Sampling
|
||||||
* stays flat while idle, and stays flat for non-positional pose edits.
|
* stays flat while idle, and stays flat for non-positional pose edits.
|
||||||
* 2. Geometry contract: the 5-polyline frustum wireframe + far-cap plane
|
* 2. Geometry contract: the 5-polyline frustum wireframe + far-cap plane
|
||||||
* exist for the active camera, the plane's corners coincide with the
|
* exist for the active camera, the plane's corners coincide with the
|
||||||
@@ -139,6 +141,56 @@ function record(name, ok, detail) {
|
|||||||
|
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
// Each page.evaluate is bounded by puppeteer's `protocolTimeout` (default
|
||||||
|
// 180s). Waits never come close: they poll from Node (below). The headroom is
|
||||||
|
// for the few long evaluates, such as enabling the full ~3,700-camera catalog
|
||||||
|
// on a real GPU, and matches the other qa-*.mjs harnesses.
|
||||||
|
const PROTOCOL_TIMEOUT_MS = 300_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polls an in-page predicate from Node, one short page.evaluate per tick,
|
||||||
|
* until it holds or the budget runs out. Used instead of page.waitForFunction
|
||||||
|
* for two reasons: its default rAF polling stops in a minimised or hidden
|
||||||
|
* headful window (timers keep running, rAF does not), and its result is one
|
||||||
|
* CDP call that protocolTimeout cuts off, while the headful drain budget
|
||||||
|
* reaches ~50 minutes. A predicate that throws fails the run instead of
|
||||||
|
* silently spending the budget.
|
||||||
|
* @param {import('puppeteer').Page} page
|
||||||
|
* @param {Function} predicate - In-page predicate.
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {number} opts.timeoutMs - Overall budget.
|
||||||
|
* @param {number} [opts.intervalMs=250] - Delay between checks.
|
||||||
|
* @param {unknown[]} [opts.args=[]] - Arguments passed to the predicate.
|
||||||
|
* The predicate must return a boolean: its result crosses CDP by value.
|
||||||
|
* @returns {Promise<boolean>} True if the predicate held within the budget.
|
||||||
|
*/
|
||||||
|
async function waitForPredicate(
|
||||||
|
page,
|
||||||
|
predicate,
|
||||||
|
{ timeoutMs, intervalMs = 250, args = [] },
|
||||||
|
) {
|
||||||
|
// performance.now() is monotonic and doesn't advance while the machine
|
||||||
|
// sleeps, so neither a clock change nor a laptop sleep eats the budget.
|
||||||
|
const deadline = performance.now() + timeoutMs;
|
||||||
|
for (;;) {
|
||||||
|
try {
|
||||||
|
if (await page.evaluate(predicate, ...args)) return true;
|
||||||
|
} catch (err) {
|
||||||
|
// A navigation or Vite reload replaces the execution context: poll again.
|
||||||
|
if (
|
||||||
|
!/Execution context was destroyed|Cannot find context/.test(
|
||||||
|
err?.message ?? '',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const remaining = deadline - performance.now();
|
||||||
|
if (remaining <= 0) return false;
|
||||||
|
await sleep(Math.min(intervalMs, remaining));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SwiftShader can spend tens of seconds servicing the hover drillPick emitted
|
// SwiftShader can spend tens of seconds servicing the hover drillPick emitted
|
||||||
// by page.mouse.move(), before the LEFT_DOWN under test even runs. Position the
|
// by page.mouse.move(), before the LEFT_DOWN under test even runs. Position the
|
||||||
// pointer while ADJUST is disabled, then re-enable it and explicitly render a
|
// pointer while ADJUST is disabled, then re-enable it and explicitly render a
|
||||||
@@ -155,29 +207,58 @@ async function positionPointerForGizmoDrag(page, point, frameCount = 3) {
|
|||||||
gev.dataManager.layers
|
gev.dataManager.layers
|
||||||
.get('cctv')
|
.get('cctv')
|
||||||
.module.setParams({ calibrationMode: true });
|
.module.setParams({ calibrationMode: true });
|
||||||
const scene = gev.viewer.scene;
|
return (await window.__qaRenderFrames(count, 5000)) >= count;
|
||||||
let rendered = 0;
|
}, frameCount);
|
||||||
return new Promise((resolve) => {
|
}
|
||||||
let settled = false;
|
|
||||||
const finish = (complete) => {
|
/**
|
||||||
if (settled) return;
|
* Saves a screenshot for human review. These are never a gate, so a capture
|
||||||
settled = true;
|
* that fails (for example when the renderer stalls in a hidden headful window)
|
||||||
clearTimeout(timer);
|
* is logged instead of ending the run.
|
||||||
remove();
|
* @param {import('puppeteer').Page} page
|
||||||
resolve(complete);
|
* @param {string} file
|
||||||
};
|
* @param {string} label
|
||||||
const remove = scene.postRender.addEventListener(() => {
|
*/
|
||||||
rendered += 1;
|
async function saveReviewShot(page, file, label) {
|
||||||
if (rendered >= count) {
|
try {
|
||||||
finish(true);
|
await page.screenshot({ path: file });
|
||||||
return;
|
console.log(` screenshot (${label}) → ${path.relative(REPO_ROOT, file)}`);
|
||||||
}
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
` screenshot skipped (${label}): ${path.relative(REPO_ROOT, file)}: ${err?.message ?? err}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs window.__qaRenderFrames(count, capMs) in the page: requests renders
|
||||||
|
* until `count` frames have passed scene.postRender or capMs runs out, and
|
||||||
|
* resolves with the number of frames rendered. The cap keeps a stalled
|
||||||
|
* renderer (headful window asleep or hidden) from hanging the CDP call.
|
||||||
|
* @param {import('puppeteer').Page} page
|
||||||
|
*/
|
||||||
|
function installRenderFramesHelper(page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
window.__qaRenderFrames = (count, capMs) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const scene = window.__godsEyeView.viewer.scene;
|
||||||
|
let frames = 0;
|
||||||
|
const finish = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
remove();
|
||||||
|
resolve(frames);
|
||||||
|
};
|
||||||
|
const remove = scene.postRender.addEventListener(() => {
|
||||||
|
if (++frames >= count) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scene.requestRender();
|
||||||
|
});
|
||||||
|
const timer = setTimeout(finish, capMs);
|
||||||
scene.requestRender();
|
scene.requestRender();
|
||||||
});
|
});
|
||||||
const timer = setTimeout(() => finish(false), 5000);
|
});
|
||||||
scene.requestRender();
|
|
||||||
});
|
|
||||||
}, frameCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-page helper (stringified and re-defined inside each evaluate that needs
|
// In-page helper (stringified and re-defined inside each evaluate that needs
|
||||||
@@ -280,23 +361,21 @@ const BEARING_SHIFT_SRC = `
|
|||||||
* @returns {Promise<boolean>} True if tilesLoaded was observed within the timeout.
|
* @returns {Promise<boolean>} True if tilesLoaded was observed within the timeout.
|
||||||
*/
|
*/
|
||||||
function waitForTilesLoaded(page, timeoutMs = 15000) {
|
function waitForTilesLoaded(page, timeoutMs = 15000) {
|
||||||
return page
|
return waitForPredicate(
|
||||||
.waitForFunction(
|
page,
|
||||||
() => {
|
() => {
|
||||||
const scene = window.__godsEyeView.viewer.scene;
|
const scene = window.__godsEyeView.viewer.scene;
|
||||||
const prims = scene.primitives;
|
const prims = scene.primitives;
|
||||||
for (let i = 0; i < prims.length; i++) {
|
for (let i = 0; i < prims.length; i++) {
|
||||||
const p = prims.get(i);
|
const p = prims.get(i);
|
||||||
if (p && p.constructor && p.constructor.name === 'Cesium3DTileset') {
|
if (p && p.constructor && p.constructor.name === 'Cesium3DTileset') {
|
||||||
return p.tilesLoaded === true;
|
return p.tilesLoaded === true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true; // no tileset in the scene — nothing to wait for
|
}
|
||||||
},
|
return true; // no tileset in the scene — nothing to wait for
|
||||||
{ timeout: timeoutMs },
|
},
|
||||||
)
|
{ timeoutMs },
|
||||||
.then(() => true)
|
);
|
||||||
.catch(() => false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -364,6 +443,10 @@ async function main() {
|
|||||||
const chromeExecutable = findChromeExecutable();
|
const chromeExecutable = findChromeExecutable();
|
||||||
const browser = await puppeteer.launch({
|
const browser = await puppeteer.launch({
|
||||||
headless: HEADFUL ? false : 'new',
|
headless: HEADFUL ? false : 'new',
|
||||||
|
protocolTimeout: PROTOCOL_TIMEOUT_MS,
|
||||||
|
// Talk CDP over a pipe, so no debugging port stays open on localhost for
|
||||||
|
// the length of a headful run.
|
||||||
|
pipe: true,
|
||||||
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
|
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
|
||||||
args: [
|
args: [
|
||||||
'--no-sandbox',
|
'--no-sandbox',
|
||||||
@@ -397,13 +480,18 @@ async function main() {
|
|||||||
});
|
});
|
||||||
console.log('Loading app...');
|
console.log('Loading app...');
|
||||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||||
await page.waitForFunction(
|
const appReady = await waitForPredicate(
|
||||||
|
page,
|
||||||
() =>
|
() =>
|
||||||
window.__godsEyeView &&
|
Boolean(
|
||||||
window.__godsEyeView.viewer &&
|
window.__godsEyeView &&
|
||||||
window.__godsEyeView.dataManager,
|
window.__godsEyeView.viewer &&
|
||||||
{ timeout: 60000 },
|
window.__godsEyeView.dataManager,
|
||||||
|
),
|
||||||
|
{ timeoutMs: 60000 },
|
||||||
);
|
);
|
||||||
|
if (!appReady) throw new Error('app did not initialise within 60s');
|
||||||
|
await installRenderFramesHelper(page);
|
||||||
// Let the initial fly-to Austin and first tiles settle.
|
// Let the initial fly-to Austin and first tiles settle.
|
||||||
await sleep(4000);
|
await sleep(4000);
|
||||||
|
|
||||||
@@ -475,18 +563,15 @@ async function main() {
|
|||||||
console.log(
|
console.log(
|
||||||
`Waiting for geometry-load queue to drain (N=${camCount}, budget ${Math.round(drainBudgetMs / 1000)}s)...`,
|
`Waiting for geometry-load queue to drain (N=${camCount}, budget ${Math.round(drainBudgetMs / 1000)}s)...`,
|
||||||
);
|
);
|
||||||
const drained = await page
|
const drained = await waitForPredicate(
|
||||||
.waitForFunction(
|
page,
|
||||||
() => {
|
() => {
|
||||||
const mod =
|
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||||
window.__godsEyeView.dataManager.layers.get('cctv').module;
|
const ui = mod.getUIState();
|
||||||
const ui = mod.getUIState();
|
return Boolean(ui.loading) && ui.loading.active === false;
|
||||||
return ui.loading && ui.loading.active === false;
|
},
|
||||||
},
|
{ timeoutMs: drainBudgetMs, intervalMs: 1000 },
|
||||||
{ timeout: drainBudgetMs },
|
);
|
||||||
)
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false);
|
|
||||||
record(
|
record(
|
||||||
`geometry-load queue drains within ${Math.round(drainBudgetMs / 1000)}s (N=${camCount})`,
|
`geometry-load queue drains within ${Math.round(drainBudgetMs / 1000)}s (N=${camCount})`,
|
||||||
drained ? true : HEADFUL ? false : null,
|
drained ? true : HEADFUL ? false : null,
|
||||||
@@ -575,27 +660,87 @@ async function main() {
|
|||||||
// (existence, not just ceiling). The wait below tolerates the update-tick
|
// (existence, not just ceiling). The wait below tolerates the update-tick
|
||||||
// latency of the completion pass (update interval is 10s).
|
// latency of the completion pass (update interval is 10s).
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
console.log('Waiting for tiles + the one-shot completion pass...');
|
const globeMode = await page.evaluate(
|
||||||
const tilesSeenLoaded = await waitForTilesLoaded(page, 45000);
|
() => window.__godsEyeView.viewer.scene.globe?.show !== false,
|
||||||
let sampleFloorOk = false;
|
|
||||||
if (tilesSeenLoaded) {
|
|
||||||
sampleFloorOk = await page
|
|
||||||
.waitForFunction(
|
|
||||||
(base) => window.__qaCounters.sampleHeight - base >= 1,
|
|
||||||
{ timeout: 30000 },
|
|
||||||
c0.sampleHeight,
|
|
||||||
)
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false);
|
|
||||||
}
|
|
||||||
const cFloor = await readCounters();
|
|
||||||
record(
|
|
||||||
'>=1 real ground sample once tilesLoaded observed (one-shot snap completed)',
|
|
||||||
tilesSeenLoaded ? sampleFloorOk : null,
|
|
||||||
tilesSeenLoaded
|
|
||||||
? `total real samples since enable=${cFloor.sampleHeight - c0.sampleHeight}`
|
|
||||||
: 'tilesLoaded never observed within 45s — floor not assessable this run',
|
|
||||||
);
|
);
|
||||||
|
if (globeMode) {
|
||||||
|
// Globe stacks intentionally perform no scene.sampleHeight query.
|
||||||
|
// Require independently fetched DEM heights and the actual rendered
|
||||||
|
// mount, so a catalog/geoid fallback cannot turn zero samples into a pass.
|
||||||
|
console.log(
|
||||||
|
'Checking terrain-globe completion against live DEM heights...',
|
||||||
|
);
|
||||||
|
const terrain = await page.evaluate(async () => {
|
||||||
|
const camera = window.__godsEyeView.dataManager.layers
|
||||||
|
.get('cctv')
|
||||||
|
.module.getUIState().activeCamera;
|
||||||
|
const exact = `${camera.lon.toFixed(5)},${camera.lat.toFixed(5)}`;
|
||||||
|
const cell = `${camera.lon.toFixed(3)},${camera.lat.toFixed(3)}`;
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/terrain/heights?points=${encodeURIComponent(`${exact};${cell}`)}`,
|
||||||
|
{ signal: AbortSignal.timeout(30000) },
|
||||||
|
);
|
||||||
|
const body = await response.json();
|
||||||
|
return { ok: response.ok, results: body.results };
|
||||||
|
});
|
||||||
|
const heights = terrain.results?.map((result) => result?.ellipsoid);
|
||||||
|
const terrainReady =
|
||||||
|
terrain.ok && heights?.length === 2 && heights.every(Number.isFinite);
|
||||||
|
const mountResolved =
|
||||||
|
terrainReady &&
|
||||||
|
(await waitForPredicate(
|
||||||
|
page,
|
||||||
|
(expected, base) => {
|
||||||
|
const { viewer, dataManager } = window.__godsEyeView;
|
||||||
|
const camera = dataManager.layers
|
||||||
|
.get('cctv')
|
||||||
|
.module.getUIState().activeCamera;
|
||||||
|
const ray = viewer.entities.getById(`cctv-${camera.id}-ray-tl`);
|
||||||
|
const mount = ray?.polyline?.positions.getValue(
|
||||||
|
viewer.clock.currentTime,
|
||||||
|
)?.[0];
|
||||||
|
const height =
|
||||||
|
mount &&
|
||||||
|
viewer.scene.globe.ellipsoid.cartesianToCartographic(mount)
|
||||||
|
.height;
|
||||||
|
return (
|
||||||
|
Number.isFinite(camera.groundPriorM) &&
|
||||||
|
Math.abs(camera.groundPriorM - expected[0]) < 0.01 &&
|
||||||
|
Number.isFinite(height) &&
|
||||||
|
expected.some(
|
||||||
|
(ground) =>
|
||||||
|
Math.abs(height - ground - camera.mountHeightM) < 0.05,
|
||||||
|
) &&
|
||||||
|
window.__qaCounters.sampleHeight === base
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ timeoutMs: 30000, args: [heights, c0.sampleHeight] },
|
||||||
|
));
|
||||||
|
record(
|
||||||
|
'terrain-globe completion uses a real DEM height at the rendered mount without mesh samples',
|
||||||
|
Boolean(mountResolved),
|
||||||
|
`terrain=${JSON.stringify(heights)}, mesh sample Δ=${(await readCounters()).sampleHeight - c0.sampleHeight}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('Waiting for tiles + the one-shot completion pass...');
|
||||||
|
const tilesSeenLoaded = await waitForTilesLoaded(page, 45000);
|
||||||
|
let sampleFloorOk = false;
|
||||||
|
if (tilesSeenLoaded) {
|
||||||
|
sampleFloorOk = await waitForPredicate(
|
||||||
|
page,
|
||||||
|
(base) => window.__qaCounters.sampleHeight - base >= 1,
|
||||||
|
{ timeoutMs: 30000, args: [c0.sampleHeight] },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const cFloor = await readCounters();
|
||||||
|
record(
|
||||||
|
'>=1 real ground sample once tilesLoaded observed (one-shot snap completed)',
|
||||||
|
tilesSeenLoaded ? sampleFloorOk : null,
|
||||||
|
tilesSeenLoaded
|
||||||
|
? `total real samples since enable=${cFloor.sampleHeight - c0.sampleHeight}`
|
||||||
|
: 'tilesLoaded never observed within 45s — floor not assessable this run',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Let the completion pass finish before the steady-state window: require
|
// Let the completion pass finish before the steady-state window: require
|
||||||
// the sampleHeight counter quiet for 12s (> the 10s update interval, so
|
// the sampleHeight counter quiet for 12s (> the 10s update interval, so
|
||||||
@@ -1358,18 +1503,7 @@ async function main() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
window.__qaCctvSiblingOwner = owner;
|
window.__qaCctvSiblingOwner = owner;
|
||||||
await new Promise((resolve) => {
|
const framesRendered = await window.__qaRenderFrames(3, 10000);
|
||||||
let frames = 0;
|
|
||||||
const remove = scene.postRender.addEventListener(() => {
|
|
||||||
if (++frames < 3) {
|
|
||||||
scene.requestRender();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
remove();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
scene.requestRender();
|
|
||||||
});
|
|
||||||
const rect = scene.canvas.getBoundingClientRect();
|
const rect = scene.canvas.getBoundingClientRect();
|
||||||
const picked = scene.pick(canvasPoint);
|
const picked = scene.pick(canvasPoint);
|
||||||
const pickedId = picked?.id?.id ?? picked?.id ?? null;
|
const pickedId = picked?.id?.id ?? picked?.id ?? null;
|
||||||
@@ -1382,26 +1516,33 @@ async function main() {
|
|||||||
typeof pickedId === 'string' ? pickedId : (pickedId?.id ?? null),
|
typeof pickedId === 'string' ? pickedId : (pickedId?.id ?? null),
|
||||||
ownerId: owner.id,
|
ownerId: owner.id,
|
||||||
ownsExactObject: picked?.id === owner,
|
ownsExactObject: picked?.id === owner,
|
||||||
|
framesRendered,
|
||||||
projectionDelta: Math.hypot(
|
projectionDelta: Math.hypot(
|
||||||
projected.x - canvasPoint.x,
|
projected.x - canvasPoint.x,
|
||||||
projected.y - canvasPoint.y,
|
projected.y - canvasPoint.y,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}, emptyClickPoint);
|
}, emptyClickPoint);
|
||||||
|
const siblingOwnsPick =
|
||||||
|
siblingTarget?.pickedId === 'qa-cctv-sibling-owner' &&
|
||||||
|
siblingTarget.ownsExactObject;
|
||||||
|
// No frame at all within the 10s cap points at a stalled renderer rather
|
||||||
|
// than at picking, though the harness can't rule out both. Fail it on the
|
||||||
|
// real-GPU sign-off path; leave it inconclusive under headless GL. A slow
|
||||||
|
// SwiftShader frame (1-2 of 3) still counts as a real miss.
|
||||||
|
const siblingRendererStalled =
|
||||||
|
siblingTarget && !siblingOwnsPick && siblingTarget.framesRendered === 0;
|
||||||
record(
|
record(
|
||||||
'sibling test object owns its canvas pick',
|
'sibling test object owns its canvas pick',
|
||||||
|
!siblingTarget
|
||||||
|
? null
|
||||||
|
: siblingOwnsPick ||
|
||||||
|
(siblingRendererStalled && !HEADFUL ? null : false),
|
||||||
siblingTarget
|
siblingTarget
|
||||||
? siblingTarget.pickedId === 'qa-cctv-sibling-owner' &&
|
? `${siblingRendererStalled ? 'renderer stalled: ' : ''}frames=${siblingTarget.framesRendered}/3 picked=${siblingTarget.pickedId} exactOwner=${siblingTarget.ownsExactObject} projectionΔ=${siblingTarget.projectionDelta}`
|
||||||
siblingTarget.ownsExactObject
|
|
||||||
: null,
|
|
||||||
siblingTarget
|
|
||||||
? `picked=${siblingTarget.pickedId} exactOwner=${siblingTarget.ownsExactObject} projectionΔ=${siblingTarget.projectionDelta}`
|
|
||||||
: 'no previously verified empty canvas ray was available',
|
: 'no previously verified empty canvas ray was available',
|
||||||
);
|
);
|
||||||
if (
|
if (siblingOwnsPick) {
|
||||||
siblingTarget?.pickedId === 'qa-cctv-sibling-owner' &&
|
|
||||||
siblingTarget.ownsExactObject
|
|
||||||
) {
|
|
||||||
await page.mouse.click(siblingTarget.x, siblingTarget.y);
|
await page.mouse.click(siblingTarget.x, siblingTarget.y);
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
}
|
}
|
||||||
@@ -1426,8 +1567,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
record(
|
record(
|
||||||
'sibling canvas click passes through without CCTV selection or deselection',
|
'sibling canvas click passes through without CCTV selection or deselection',
|
||||||
siblingTarget?.pickedId === 'qa-cctv-sibling-owner' &&
|
siblingOwnsPick
|
||||||
siblingTarget.ownsExactObject
|
|
||||||
? siblingClick.activeId === clickEvidenceSetup.activeId &&
|
? siblingClick.activeId === clickEvidenceSetup.activeId &&
|
||||||
siblingClick.transitions === siblingClick.baselineTransitions &&
|
siblingClick.transitions === siblingClick.baselineTransitions &&
|
||||||
siblingClick.focusEvents === 0 &&
|
siblingClick.focusEvents === 0 &&
|
||||||
@@ -1626,10 +1766,7 @@ async function main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const viewshedShot = path.join(SHOTS_DIR, 'cctv-v3-viewshed.png');
|
const viewshedShot = path.join(SHOTS_DIR, 'cctv-v3-viewshed.png');
|
||||||
await page.screenshot({ path: viewshedShot });
|
await saveReviewShot(page, viewshedShot, 'visual review only');
|
||||||
console.log(
|
|
||||||
` screenshot (visual review only) → ${path.relative(REPO_ROOT, viewshedShot)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Group 7: calibration gizmo (v3 design §3c — ADJUST mode + drag)
|
// Group 7: calibration gizmo (v3 design §3c — ADJUST mode + drag)
|
||||||
@@ -1963,10 +2100,7 @@ async function main() {
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
const shot = path.join(SHOTS_DIR, 'cctv-v2-active.png');
|
const shot = path.join(SHOTS_DIR, 'cctv-v2-active.png');
|
||||||
await page.screenshot({ path: shot });
|
await saveReviewShot(page, shot, 'visual review only, not a gate');
|
||||||
console.log(
|
|
||||||
` screenshot (visual review only, not a gate) → ${path.relative(REPO_ROOT, shot)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// No console errors during the whole exercise.
|
// No console errors during the whole exercise.
|
||||||
record(
|
record(
|
||||||
|
|||||||
Loaded 100 of 587 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user