Merge branch 'main' into i18n-spanish

This commit is contained in:
Meir Michanie authored and GitHub committed 2026-10-14 19:17:53 +02:00
commit d2bbb5542f
4 files changed
+204 -1

No files matched your search

+4
View File
@@ -1,5 +1,9 @@
# Changelog
- Bing Aerial and Bing Labels request HTTPS tiles on HTTP localhost through
their ion-backed providers, retaining ion and per-tile attribution without
widening the Content Security Policy.
- Add New York City traffic cameras from the NYC DOT Traffic Management Center
as an opt-in CCTV pack. NYC DOT requires a data-sharing agreement for the
camera feed, so the pack is off unless `CCTV_NYC_ENABLED=1` is set by an
+6
View File
@@ -5222,6 +5222,12 @@ photoreal globe is hidden.
Map credentials are passed to each source constructor rather than changing SDK-wide Google or ion defaults.
Bing Aerial and Bing Labels resolve their ion imagery endpoint with the supplied
token, then construct the Bing provider with `tileProtocol: 'https'`. Both styles
request HTTPS tiles even on HTTP localhost, with the existing Content Security
Policy unchanged. Ion attribution and Bing's per-tile credits are preserved;
other ion imagery assets continue through the SDK's standard ion factory.
The controller accepts other source registries without adding provider branches.
Each instance caches source construction, ignores superseded scene changes and
releases imagery layers/listeners on replacement. Destroy invalidates pending
+37
View File
@@ -25,5 +25,42 @@ export function createEsriImagery() {
export function createIonImagery(style, accessToken) {
accessToken = String(accessToken || '').trim();
if (!accessToken) throw new Error('Ion imagery requires an explicit token');
if (
style === Cesium.IonWorldImageryStyle.AERIAL ||
style === Cesium.IonWorldImageryStyle.AERIAL_WITH_LABELS
) {
return createBingIonImagery(style, accessToken);
}
return Cesium.IonImageryProvider.fromAssetId(style, { accessToken });
}
async function createBingIonImagery(style, accessToken) {
const endpoint = await new Cesium.Resource({
url: `https://api.cesium.com/v1/assets/${style}/endpoint`,
queryParameters: { access_token: accessToken },
}).fetchJson();
if (
endpoint?.type !== 'IMAGERY' ||
endpoint.externalType !== 'BING' ||
typeof endpoint.options?.url !== 'string'
) {
throw new Error('Ion world imagery did not return a Bing imagery endpoint');
}
// Bing otherwise follows the page's HTTP scheme on localhost. Resolve its
// ion-issued credentials through public APIs so tiles use HTTPS without
// weakening CSP, changing SDK defaults or touching provider internals.
const { url, ...options } = endpoint.options;
const provider = await Cesium.BingMapsImageryProvider.fromUrl(url, {
...options,
tileProtocol: 'https',
});
const ionCredits = (endpoint.attributions ?? []).map(
({ html, collapsible }) => new Cesium.Credit(html, collapsible === false),
);
const getTileCredits = provider.getTileCredits.bind(provider);
provider.getTileCredits = (x, y, level) => [
...ionCredits,
...(getTileCredits(x, y, level) ?? []),
];
return provider;
}
+157 -1
View File
@@ -89,7 +89,7 @@ test('imagery and terrain pass their own ion token without relying on SDK defaul
calls.push({ kind: 'terrain', resource, options });
return { id: 'terrain' };
};
await createIonImagery(Cesium.IonWorldImageryStyle.AERIAL, 'imagery-token');
await createIonImagery(12345, 'imagery-token');
const result = await createWorldTerrain('terrain-token');
assert.equal(calls[0].options.accessToken, 'imagery-token');
assert.equal(calls[1].options.accessToken, 'terrain-token');
@@ -104,6 +104,162 @@ test('imagery and terrain pass their own ion token without relying on SDK defaul
}
});
test('both Bing ion stacks use HTTPS and retain ion and per-tile attribution', async (t) => {
const defaultToken = Cesium.Ion.defaultAccessToken;
const metadataCalls = [];
const bingCalls = [];
const perTileCalls = [];
const bingCredit = new Cesium.Credit('Bing tile credit', true);
const endpoint = {
type: 'IMAGERY',
externalType: 'BING',
options: Object.freeze({
url: 'https://dev.virtualearth.net',
key: 'ion-issued-bing-fixture',
mapStyle: 'Aerial',
tileProtocol: 'http',
}),
attributions: [
{ html: 'Ion required credit', collapsible: false },
{ html: 'Ion optional credit', collapsible: true },
],
};
t.mock.method(Cesium.Resource.prototype, 'fetchJson', async function () {
metadataCalls.push({
url: this.url,
token: this.queryParameters.access_token,
});
const mapStyle = this.url.includes(
`/assets/${Cesium.IonWorldImageryStyle.AERIAL_WITH_LABELS}/`,
)
? 'AerialWithLabels'
: 'Aerial';
return {
...endpoint,
options: Object.freeze({ ...endpoint.options, mapStyle }),
};
});
t.mock.method(Cesium.IonImageryProvider, 'fromAssetId', () =>
assert.fail('Bing must not inherit the local HTTP page protocol'),
);
t.mock.method(
Cesium.BingMapsImageryProvider,
'fromUrl',
async (url, options) => {
bingCalls.push({ url, options });
const provider = {
errorEvent: new Cesium.Event(),
getTileCredits(x, y, level) {
assert.equal(this, provider);
perTileCalls.push([x, y, level]);
return level === 0 ? undefined : [bingCredit];
},
};
return provider;
},
);
for (const style of [
Cesium.IonWorldImageryStyle.AERIAL,
Cesium.IonWorldImageryStyle.AERIAL_WITH_LABELS,
]) {
const provider = await createIonImagery(style, ' own-ion-token ');
const credits = provider.getTileCredits(3, 4, 5);
assert.deepEqual(
credits.map(({ html, showOnScreen }) => ({ html, showOnScreen })),
[
{ html: 'Ion required credit', showOnScreen: true },
{ html: 'Ion optional credit', showOnScreen: false },
{ html: 'Bing tile credit', showOnScreen: true },
],
);
assert.equal(credits[2], bingCredit);
assert.equal(provider.getTileCredits(0, 0, 0).length, 2);
assert.ok(provider.errorEvent instanceof Cesium.Event);
assert.equal(metadataCalls.at(-1).token, 'own-ion-token');
const requested = new URL(metadataCalls.at(-1).url);
assert.equal(requested.origin, 'https://api.cesium.com');
assert.equal(requested.pathname, `/v1/assets/${style}/endpoint`);
}
assert.deepEqual(perTileCalls, [
[3, 4, 5],
[0, 0, 0],
[3, 4, 5],
[0, 0, 0],
]);
assert.equal(bingCalls.length, 2);
for (const [index, { url, options }] of bingCalls.entries()) {
assert.equal(url, 'https://dev.virtualearth.net');
assert.deepEqual(options, {
key: 'ion-issued-bing-fixture',
mapStyle: index === 0 ? 'Aerial' : 'AerialWithLabels',
tileProtocol: 'https',
});
}
assert.equal(endpoint.options.tileProtocol, 'http');
assert.equal(Cesium.Ion.defaultAccessToken, defaultToken);
});
test('Bing ion metadata and construction failures propagate without another source', async (t) => {
const style = Cesium.IonWorldImageryStyle.AERIAL;
const metadataError = new Error('ion metadata fixture failure');
const providerError = new Error('Bing construction fixture failure');
let metadata = () => {
throw metadataError;
};
let bingCalls = 0;
t.mock.method(Cesium.Resource.prototype, 'fetchJson', async () => metadata());
t.mock.method(Cesium.BingMapsImageryProvider, 'fromUrl', async () => {
bingCalls += 1;
throw providerError;
});
t.mock.method(Cesium.IonImageryProvider, 'fromAssetId', () =>
assert.fail('Bing failures must not silently use an alternate provider'),
);
await assert.rejects(createIonImagery(style, 'test-token'), metadataError);
for (const endpoint of [
undefined,
{
type: 'TERRAIN',
externalType: 'BING',
options: { url: 'https://example.test' },
},
{
type: 'IMAGERY',
externalType: 'OTHER',
options: { url: 'https://example.test' },
},
{ type: 'IMAGERY', externalType: 'BING', options: {} },
]) {
metadata = () => endpoint;
await assert.rejects(
createIonImagery(style, 'test-token'),
/Bing imagery endpoint/,
);
}
assert.equal(bingCalls, 0);
metadata = () => ({
type: 'IMAGERY',
externalType: 'BING',
options: { url: 'https://dev.virtualearth.net', key: 'bing-fixture' },
});
await assert.rejects(createIonImagery(style, 'test-token'), providerError);
assert.equal(bingCalls, 1);
});
test('other ion imagery assets keep the standard SDK factory', async (t) => {
const expected = {};
const calls = [];
t.mock.method(Cesium.Resource.prototype, 'fetchJson', () =>
assert.fail('generic assets must use the SDK ion factory'),
);
t.mock.method(Cesium.IonImageryProvider, 'fromAssetId', async (...args) => {
calls.push(args);
return expected;
});
assert.equal(await createIonImagery(12345, ' custom-token '), expected);
assert.deepEqual(calls, [[12345, { accessToken: 'custom-token' }]]);
});
test('cancellation after ion metadata prevents terrain construction', async () => {
const originalResource = Cesium.IonResource.fromAssetId;
const originalTerrain = Cesium.CesiumTerrainProvider.fromUrl;