Merge remote-tracking branch 'upstream/main' into core-rb2

# Conflicts:
#	CHANGELOG.md
#	build/vite.js
#	scripts/package-boundaries.json
#	server/standalone/vite.config.js
#	src/app/scene.js
#	src/layers/vessels/queries.js
#	src/standalone/application.js
#	src/tooling/viteBuild.test.mjs
#	src/ui/cockpitInstruments.js
#	src/ui/layerPanel.js
#	src/ui/panelPositionControls.js
This commit is contained in:
Meir Michanie committed 2026-10-10 23:15:54 +02:00
commit 71f7a58169
475 files changed
+67826 -4965

No files matched your search

+56 -10
View File
@@ -16,30 +16,43 @@ export function selectMapStartupRoute({
/**
* Load Google Photorealistic 3D Tiles through direct Google access when
* configured, otherwise through Cesium ion's hosted Google asset. If the
* direct request fails and an ion token is available, ion is the recovery path.
* configured, otherwise through Cesium ion's hosted Google asset. Direct
* access uses a browser key, or else short-lived tokens from the app's
* server (see googleTokens.js). If direct access fails and an ion token is
* available, ion is the recovery path.
*
* @param {object} Cesium
* @param {{googleApiKey?: string, cesiumToken?: string}} credentials
* @returns {Promise<{tileset: object|null, route: 'google-direct'|'google-ion'|'osm', errors: Error[]}>}
* @param {{googleApiKey?: string, cesiumToken?: string, googleTokens?: {token: Function}|null}} credentials
* @returns {Promise<{tileset: object|null, route: 'google-direct'|'google-token'|'google-ion'|'osm', errors: Error[]}>}
*/
export async function loadPhotorealisticTileset(
Cesium,
{ googleApiKey = '', cesiumToken = '' } = {},
{ googleApiKey = '', cesiumToken = '', googleTokens = null } = {},
) {
const googleKey = clean(googleApiKey);
const ionToken = clean(cesiumToken);
const errors = [];
const attempts = [];
if (googleKey) attempts.push({ route: 'google-direct', googleKey });
if (ionToken) attempts.push({ route: 'google-ion', googleKey: undefined });
if (googleKey)
attempts.push({
route: 'google-direct',
create: () => createGoogleDirectTileset(Cesium, googleKey),
});
else if (googleTokens)
attempts.push({
route: 'google-token',
create: () => createGoogleTokenTileset(Cesium, googleTokens),
});
if (ionToken)
attempts.push({
route: 'google-ion',
create: () => createGoogleIonTileset(Cesium, ionToken),
});
for (const attempt of attempts) {
try {
const tileset = attempt.googleKey
? await createGoogleDirectTileset(Cesium, attempt.googleKey)
: await createGoogleIonTileset(Cesium, ionToken);
const tileset = await attempt.create();
return { tileset, route: attempt.route, errors };
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
@@ -60,6 +73,39 @@ export function createGoogleDirectTileset(Cesium, key) {
);
}
/**
* Google 3D with short-lived tokens instead of a key. Every tile request
* carries the token as a header; a tile refused for an expired token gets
* the renewed one and is tried once more.
*/
export async function createGoogleTokenTileset(Cesium, tokens) {
const token = await tokens.token();
if (!token) throw new Error('Google 3D tokens are not offered');
const credit = Cesium.GoogleMaps.getDefaultCredit?.();
const resource = new Cesium.Resource({
url: `${Cesium.GoogleMaps.mapTilesApiEndpoint}v1/3dtiles/root.json`,
headers: { Authorization: `Bearer ${token}` },
credits: credit ? [credit] : undefined,
retryAttempts: 1,
async retryCallback(failed, error) {
if (![401, 403].includes(error?.statusCode)) return false;
const used = failed.headers.Authorization?.replace(/^Bearer /, '');
const renewed = await tokens.token({ replacing: used });
if (!renewed || renewed === used) return false;
failed.headers.Authorization = `Bearer ${renewed}`;
return true;
},
});
// The settings Cesium's Google helper applies to the key route.
return Cesium.Cesium3DTileset.fromUrl(resource, {
cacheBytes: 1536 * 1024 * 1024,
maximumCacheOverflowBytes: 1024 * 1024 * 1024,
enableCollision: true,
// Tiles keep drawing their own texture while draped weather loads.
asynchronouslyLoadImagery: true,
});
}
export async function createGoogleIonTileset(
Cesium,
accessToken,
+62
View File
@@ -0,0 +1,62 @@
/**
* Short-lived Google access tokens the app's server may offer in place of a
* browser key. A server that offers them answers GET GOOGLE_TOKEN_PATH with
* `{ accessToken, expiresAt }` (expiresAt in epoch milliseconds); any other
* answer means it does not, and the app carries on without them.
*/
export const GOOGLE_TOKEN_PATH = '/api/google/tiles-token';
/** Renew a token this long before it expires. */
const RENEW_EARLY_MS = 60_000;
function parseToken(data, now) {
const accessToken = data?.accessToken;
const expiresAt = Number(data?.expiresAt);
if (typeof accessToken !== 'string' || accessToken.length === 0) return null;
if (!Number.isFinite(expiresAt) || expiresAt <= now) return null;
return { accessToken, expiresAt };
}
/**
* A source of tokens from the app's server. `token()` resolves the current
* token, renewing it shortly before it expires; `token({ replacing })`
* renews only if `replacing` is still the current token, so many tile
* requests rejected for the same expired token renew it once. Resolves null
* when the server does not offer tokens.
*/
export function createGoogleTokenSource({
fetchImpl = (...args) => fetch(...args),
url = GOOGLE_TOKEN_PATH,
now = () => Date.now(),
} = {}) {
let current = null;
let pending = null;
async function request() {
try {
const response = await fetchImpl(url, {
headers: { Accept: 'application/json' },
cache: 'no-store',
});
if (!response.ok) return null;
return parseToken(await response.json(), now());
} catch {
return null;
}
}
return {
async token({ replacing } = {}) {
const usable =
current &&
current.expiresAt - RENEW_EARLY_MS > now() &&
(replacing === undefined || current.accessToken !== replacing);
if (usable) return current.accessToken;
pending ??= request().finally(() => {
pending = null;
});
current = await pending;
return current?.accessToken ?? null;
},
};
}
+64
View File
@@ -0,0 +1,64 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createGoogleTokenSource, GOOGLE_TOKEN_PATH } from './googleTokens.js';
function server(answers) {
const calls = [];
return {
calls,
fetchImpl: async (url) => {
calls.push(url);
const answer = answers.shift();
if (answer instanceof Error) throw answer;
return answer;
},
};
}
const offer = (accessToken, expiresAt) =>
Response.json({ accessToken, expiresAt });
test('a token is reused until shortly before it expires', async () => {
let now = 1_000_000;
const { calls, fetchImpl } = server([
offer('first', now + 900_000),
offer('second', now + 1_800_000),
]);
const tokens = createGoogleTokenSource({ fetchImpl, now: () => now });
assert.equal(await tokens.token(), 'first');
assert.equal(await tokens.token(), 'first');
assert.deepEqual(calls, [GOOGLE_TOKEN_PATH]);
now += 850_000;
assert.equal(await tokens.token(), 'second');
assert.equal(calls.length, 2);
});
test('requests refused for the same token renew it once', async () => {
const now = 1_000_000;
const { calls, fetchImpl } = server([
offer('first', now + 900_000),
offer('second', now + 900_000),
]);
const tokens = createGoogleTokenSource({ fetchImpl, now: () => now });
await tokens.token();
const renewed = await Promise.all([
tokens.token({ replacing: 'first' }),
tokens.token({ replacing: 'first' }),
]);
assert.deepEqual(renewed, ['second', 'second']);
assert.equal(await tokens.token({ replacing: 'first' }), 'second');
assert.equal(calls.length, 2);
});
test('a server that does not offer tokens yields none', async () => {
for (const answer of [
new Response('Not found', { status: 404 }),
new Response('<!doctype html>', { status: 200 }),
Response.json({ accessToken: '', expiresAt: Date.now() + 60_000 }),
Response.json({ accessToken: 'stale', expiresAt: 1 }),
new TypeError('offline'),
]) {
const { fetchImpl } = server([answer]);
assert.equal(await createGoogleTokenSource({ fetchImpl }).token(), null);
}
});