mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
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:
commit
71f7a58169
475 files changed
+67826
-4965
No files matched your search
+56
-10
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user