Merge branch 'german/refactor-e6-tiles-fixture' into german/navara-refactor

# Conflicts:
#	scripts/qa-harness-baseline.json
This commit is contained in:
German Khokhlov committed 2026-11-10 15:54:13 +04:00
commit 1ac9f28c4a
18 files changed
+1857 -19

No files matched your search

+257
View File
@@ -0,0 +1,257 @@
import assert from 'node:assert/strict';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
import * as geo from '../src/geo/index.js';
const directory = new URL('./fixtures/tiles3d/', import.meta.url);
const origin = { lon: -97.7431, lat: 30.2672 };
const transform = geo.transforms.eastNorthUpToFixedFrame(
geo.cartesian3.fromDegrees(origin.lon, origin.lat),
);
const box = (west, south, east, north, bottom, top) => [
[
[west, south, top],
[east, south, top],
[east, north, top],
[west, north, top],
],
[
[west, north, bottom],
[east, north, bottom],
[east, south, bottom],
[west, south, bottom],
],
[
[west, south, bottom],
[east, south, bottom],
[east, south, top],
[west, south, top],
],
[
[east, south, bottom],
[east, north, bottom],
[east, north, top],
[east, south, top],
],
[
[east, north, bottom],
[west, north, bottom],
[west, north, top],
[east, north, top],
],
[
[west, north, bottom],
[west, south, bottom],
[west, south, top],
[west, north, top],
],
];
// glTF is Y-up; the tileset transform is east/north/up after the 3D Tiles
// loader's Y-to-Z rotation. Untextured flat faces need no external assets.
function glb(faces) {
const positions = [],
normals = [];
for (const face of faces) {
const vertices = face.map(([x, y, z]) => [x, z, -y]);
const [a, b, c] = vertices;
const u = b.map((v, i) => v - a[i]);
const v = c.map((v, i) => v - a[i]);
const normal = [
u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0],
];
const length = Math.hypot(...normal);
for (let i = 1; i < vertices.length - 1; i++)
for (const vertex of [a, vertices[i], vertices[i + 1]]) {
positions.push(...vertex);
normals.push(...normal.map((n) => n / length));
}
}
const bytes = Buffer.from(
new Float32Array([...positions, ...normals]).buffer,
);
const bounds = (fn) =>
[0, 1, 2].map((axis) => fn(...positions.filter((_, i) => i % 3 === axis)));
const json = Buffer.from(
JSON.stringify({
asset: { version: '2.0' },
scene: 0,
scenes: [{ nodes: [0] }],
nodes: [{ mesh: 0 }],
meshes: [
{
primitives: [{ attributes: { POSITION: 0, NORMAL: 1 }, material: 0 }],
},
],
materials: [
{
pbrMetallicRoughness: {
baseColorFactor: [0.5, 0.5, 0.5, 1],
metallicFactor: 0,
roughnessFactor: 1,
},
},
],
buffers: [{ byteLength: bytes.length }],
bufferViews: [0, 1].map((i) => ({
buffer: 0,
byteOffset: i * positions.length * 4,
byteLength: positions.length * 4,
target: 34962,
})),
accessors: [
{
bufferView: 0,
componentType: 5126,
count: positions.length / 3,
type: 'VEC3',
min: bounds(Math.min),
max: bounds(Math.max),
},
{
bufferView: 1,
componentType: 5126,
count: positions.length / 3,
type: 'VEC3',
},
],
}),
);
const padded = Buffer.alloc(Math.ceil(json.length / 4) * 4, 0x20);
json.copy(padded);
const result = Buffer.alloc(12 + 8 + padded.length + 8 + bytes.length);
result.writeUInt32LE(0x46546c67, 0);
result.writeUInt32LE(2, 4);
result.writeUInt32LE(result.length, 8);
result.writeUInt32LE(padded.length, 12);
result.writeUInt32LE(0x4e4f534a, 16);
padded.copy(result, 20);
result.writeUInt32LE(bytes.length, 20 + padded.length);
result.writeUInt32LE(0x004e4942, 24 + padded.length);
bytes.copy(result, 28 + padded.length);
return result;
}
function probe(x, y, z) {
const position = geo.matrix4.multiplyByPoint(transform, { x, y, z }, {});
const cartographic = geo.cartographic.fromCartesian(position);
return {
position,
cartographic,
lon: geo.math.toDegrees(cartographic.longitude),
lat: geo.math.toDegrees(cartographic.latitude),
height: cartographic.height,
};
}
export function buildTilesFixture() {
const roof = box(110, -60, 210, 60, 0, 60);
roof.shift();
roof.push(
[
[110, -60, 60],
[160, -60, 100],
[160, 60, 100],
[110, 60, 60],
],
[
[160, -60, 100],
[210, -60, 60],
[210, 60, 60],
[160, 60, 100],
],
[
[110, -60, 60],
[210, -60, 60],
[160, -60, 100],
],
[
[210, 60, 60],
[110, 60, 60],
[160, 60, 100],
],
);
const tiles = [
[
'plate',
box(-300, -300, 300, 300, -2, 0),
[0, 0, -1, 300, 0, 0, 0, 300, 0, 0, 0, 1],
],
[
'block50',
box(-210, -60, -110, 60, 0, 50),
[-160, 0, 25, 50, 0, 0, 0, 60, 0, 0, 0, 25],
],
[
'block200',
box(-50, 120, 50, 220, 0, 200),
[0, 170, 100, 50, 0, 0, 0, 50, 0, 0, 0, 100],
],
['roof', roof, [160, 0, 50, 50, 0, 0, 0, 60, 0, 0, 0, 50]],
];
const tileset = {
asset: { version: '1.1' },
geometricError: 1000,
root: {
transform: Array.from(transform),
boundingVolume: { box: [0, 0, 99, 300, 0, 0, 0, 300, 0, 0, 0, 101] },
geometricError: 1000,
refine: 'ADD',
children: tiles.map(([name, , box]) => ({
boundingVolume: { box },
geometricError: 0,
content: { uri: `${name}.glb` },
})),
},
extras: {
origin,
datum: 'WGS84 ellipsoid; local ENU heights in metres',
probes: {
plate: probe(0, -170, 0),
block50: probe(-160, 0, 50),
block200: probe(0, 170, 200),
roof: probe(180, 0, 84),
},
// More than one pass of surfacePoints, all away from the roof edges.
points: Array.from({ length: 40 }, (_, i) => {
const x = 140 + (i % 8) * 5;
return probe(
x,
-40 + Math.floor(i / 8) * 20,
100 - 0.8 * Math.abs(x - 160),
);
}),
},
};
const files = new Map(
tiles.map(([name, faces]) => [`${name}.glb`, glb(faces)]),
);
files.set(
'tileset.json',
Buffer.from(JSON.stringify(tileset, null, 2) + '\n'),
);
assert.ok(
[...files.values()].reduce((sum, b) => sum + b.length, 0) < 200_000,
);
return files;
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
const check = process.argv.includes('--check');
const files = buildTilesFixture();
if (!check) mkdirSync(directory, { recursive: true });
for (const [name, bytes] of files) {
const file = new URL(name, directory);
if (check)
assert.deepEqual(readFileSync(file), bytes, `${name} is reproducible`);
else writeFileSync(file, bytes);
}
console.log(
`${check ? 'Checked' : 'Generated'} ${files.size} fixture files (${[...files.values()].reduce((sum, b) => sum + b.length, 0)} bytes)`,
);
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+805
View File
@@ -0,0 +1,805 @@
{
"asset": {
"version": "1.1"
},
"geometricError": 1000,
"root": {
"transform": [
0.9908821299915158,
-0.13473160157690048,
0,
0,
0.06790921036869463,
0.4994375649707324,
0.8636842350322285,
0,
-0.11636556024261228,
-0.8558092744488276,
0.5040332748507723,
0,
-742827.4189515795,
-5463116.347553584,
3195991.10594888,
1
],
"boundingVolume": {
"box": [
0,
0,
99,
300,
0,
0,
0,
300,
0,
0,
0,
101
]
},
"geometricError": 1000,
"refine": "ADD",
"children": [
{
"boundingVolume": {
"box": [
0,
0,
-1,
300,
0,
0,
0,
300,
0,
0,
0,
1
]
},
"geometricError": 0,
"content": {
"uri": "plate.glb"
}
},
{
"boundingVolume": {
"box": [
-160,
0,
25,
50,
0,
0,
0,
60,
0,
0,
0,
25
]
},
"geometricError": 0,
"content": {
"uri": "block50.glb"
}
},
{
"boundingVolume": {
"box": [
0,
170,
100,
50,
0,
0,
0,
50,
0,
0,
0,
100
]
},
"geometricError": 0,
"content": {
"uri": "block200.glb"
}
},
{
"boundingVolume": {
"box": [
160,
0,
50,
50,
0,
0,
0,
60,
0,
0,
0,
50
]
},
"geometricError": 0,
"content": {
"uri": "roof.glb"
}
}
]
},
"extras": {
"origin": {
"lon": -97.7431,
"lat": 30.2672
},
"datum": "WGS84 ellipsoid; local ENU heights in metres",
"probes": {
"plate": {
"position": {
"x": -742838.9635173421,
"y": -5463201.251939629,
"z": 3195844.2796289246
},
"cartographic": {
"longitude": -1.7059389161060694,
"latitude": 0.5282355305977933,
"height": 0.002275014548483431
},
"lon": -97.7431,
"lat": 30.265666492107215,
"height": 0.002275014548483431
},
"block50": {
"position": {
"x": -742991.7783703903,
"y": -5463137.580961054,
"z": 3196016.307612622
},
"cartographic": {
"longitude": -1.7059679361541311,
"latitude": 0.5282622951753985,
"height": 50.00200513183283
},
"lon": -97.7447627262752,
"lat": 30.267199989444443,
"height": 50.00200513183283
},
"block200": {
"position": {
"x": -742839.1474978654,
"y": -5463202.605022429,
"z": 3196238.7389238053
},
"cartographic": {
"longitude": -1.7059389161060696,
"latitude": 0.5282890592724478,
"height": 200.0022749319483
},
"lon": -97.74310000000001,
"lat": 30.26873345924785,
"height": 200.0022749319483
},
"roof": {
"position": {
"x": -742658.8348752414,
"y": -5463212.487220922,
"z": 3196033.444743967
},
"cartographic": {
"longitude": -1.7059062687258868,
"latitude": 0.528262295126465,
"height": 84.00253773142123
},
"lon": -97.74122944290338,
"lat": 30.267199986640765,
"height": 84.00253773142123
}
},
"points": [
{
"position": {
"x": -742701.1865288558,
"y": -5463227.075459457,
"z": 3195998.897374566
},
"cartographic": {
"longitude": -1.7059135237921106,
"latitude": 0.5282559977114081,
"height": 84.00166112171651
},
"lon": -97.74164512757808,
"lat": 30.26683917133616,
"height": 84.00166112171651
},
{
"position": {
"x": -742696.6975804468,
"y": -5463231.172354563,
"z": 3196000.9135076655
},
"cartographic": {
"longitude": -1.7059126169402343,
"latitude": 0.5282559977051193,
"height": 88.00177273310982
},
"lon": -97.74159316879293,
"lat": 30.26683917097584,
"height": 88.00177273310982
},
{
"position": {
"x": -742692.2086320379,
"y": -5463235.269249668,
"z": 3196002.929640765
},
"cartographic": {
"longitude": -1.7059117100894945,
"latitude": 0.528255997698471,
"height": 92.00188826158782
},
"lon": -97.7415412100729,
"lat": 30.266839170594917,
"height": 92.00188826158782
},
{
"position": {
"x": -742687.7196836289,
"y": -5463239.3661447745,
"z": 3196004.945773864
},
"cartographic": {
"longitude": -1.705910803239891,
"latitude": 0.5282559976914627,
"height": 96.00200770555026
},
"lon": -97.74148925141796,
"lat": 30.26683917019337,
"height": 96.00200770555026
},
{
"position": {
"x": -742683.2307352199,
"y": -5463243.46303988,
"z": 3196006.9619069635
},
"cartographic": {
"longitude": -1.7059098963914245,
"latitude": 0.5282559976840947,
"height": 100.00213106686036
},
"lon": -97.74143729282817,
"lat": 30.266839169771217,
"height": 100.00213106686036
},
{
"position": {
"x": -742677.810862329,
"y": -5463240.71346079,
"z": 3196004.945773864
},
"cartographic": {
"longitude": -1.7059089895065904,
"latitude": 0.5282559976684348,
"height": 96.00225834565195
},
"lon": -97.74138533215466,
"lat": 30.266839168873968,
"height": 96.00225834565195
},
{
"position": {
"x": -742672.390989438,
"y": -5463237.9638817,
"z": 3196002.929640765
},
"cartographic": {
"longitude": -1.7059080826206199,
"latitude": 0.5282559976524149,
"height": 92.002389541416
},
"lon": -97.74133337141606,
"lat": 30.2668391679561,
"height": 92.002389541416
},
{
"position": {
"x": -742666.9711165471,
"y": -5463235.21430261,
"z": 3196000.9135076655
},
"cartographic": {
"longitude": -1.705907175733513,
"latitude": 0.5282559976360354,
"height": 88.0025246520816
},
"lon": -97.74128141061233,
"lat": 30.26683916701762,
"height": 88.0025246520816
},
{
"position": {
"x": -742699.8283446485,
"y": -5463217.086708157,
"z": 3196016.1710592667
},
"cartographic": {
"longitude": -1.705913523745684,
"latitude": 0.5282591464650368,
"height": 84.0015666583481
},
"lon": -97.74164512491804,
"lat": 30.26701958162981,
"height": 84.0015666583481
},
{
"position": {
"x": -742695.3393962395,
"y": -5463221.183603263,
"z": 3196018.187192366
},
"cartographic": {
"longitude": -1.7059126168921497,
"latitude": 0.5282591464567652,
"height": 88.00167827023611
},
"lon": -97.74159316603789,
"lat": 30.267019581155882,
"height": 88.00167827023611
},
{
"position": {
"x": -742690.8504478304,
"y": -5463225.280498369,
"z": 3196020.2033254653
},
"cartographic": {
"longitude": -1.7059117100397518,
"latitude": 0.5282591464481338,
"height": 92.0017937984389
},
"lon": -97.74154120722285,
"lat": 30.267019580661337,
"height": 92.0017937984389
},
{
"position": {
"x": -742686.3614994214,
"y": -5463229.377393475,
"z": 3196022.2194585647
},
"cartographic": {
"longitude": -1.7059108031884906,
"latitude": 0.5282591464391424,
"height": 96.00191324267489
},
"lon": -97.74148924847293,
"lat": 30.26701958014617,
"height": 96.00191324267489
},
{
"position": {
"x": -742681.8725510124,
"y": -5463233.47428858,
"z": 3196024.2355916644
},
"cartographic": {
"longitude": -1.7059098963383656,
"latitude": 0.5282591464297914,
"height": 100.00203660347505
},
"lon": -97.74143728978812,
"lat": 30.2670195796104,
"height": 100.00203660347505
},
{
"position": {
"x": -742676.4526781215,
"y": -5463230.72470949,
"z": 3196022.2194585647
},
"cartographic": {
"longitude": -1.7059089894518735,
"latitude": 0.5282591464161143,
"height": 96.00216388302483
},
"lon": -97.74138532901962,
"lat": 30.267019578826755,
"height": 96.00216388302483
},
{
"position": {
"x": -742671.0328052306,
"y": -5463227.975130401,
"z": 3196020.2033254653
},
"cartographic": {
"longitude": -1.705908082564245,
"latitude": 0.5282591464020774,
"height": 92.00229507980886
},
"lon": -97.74133336818602,
"lat": 30.267019578022502,
"height": 92.00229507980886
},
{
"position": {
"x": -742665.6129323397,
"y": -5463225.225551311,
"z": 3196018.187192366
},
"cartographic": {
"longitude": -1.7059071756754798,
"latitude": 0.5282591463876805,
"height": 88.00243019067032
},
"lon": -97.74128140728727,
"lat": 30.26701957719762,
"height": 88.00243019067032
},
{
"position": {
"x": -742698.470160441,
"y": -5463207.097956859,
"z": 3196033.444743967
},
"cartographic": {
"longitude": -1.705913523699257,
"latitude": 0.5282622952185787,
"height": 84.00153517112447
},
"lon": -97.74164512225796,
"lat": 30.26719999191849,
"height": 84.00153517112447
},
{
"position": {
"x": -742693.981212032,
"y": -5463211.194851964,
"z": 3196035.460877067
},
"cartographic": {
"longitude": -1.7059126168440648,
"latitude": 0.528262295208324,
"height": 88.00164678309102
},
"lon": -97.74159316328283,
"lat": 30.267199991330937,
"height": 88.00164678309102
},
{
"position": {
"x": -742689.492263623,
"y": -5463215.29174707,
"z": 3196037.4770101663
},
"cartographic": {
"longitude": -1.705911709990009,
"latitude": 0.5282622951977096,
"height": 92.0017623126523
},
"lon": -97.7415412043728,
"lat": 30.267199990722776,
"height": 92.0017623126523
},
{
"position": {
"x": -742685.0033152141,
"y": -5463219.388642175,
"z": 3196039.4931432656
},
"cartographic": {
"longitude": -1.7059108031370898,
"latitude": 0.5282622951867355,
"height": 96.00188175721505
},
"lon": -97.74148924552787,
"lat": 30.267199990094007,
"height": 96.00188175721505
},
{
"position": {
"x": -742680.5143668051,
"y": -5463223.485537281,
"z": 3196041.509276365
},
"cartographic": {
"longitude": -1.705909896285307,
"latitude": 0.5282622951754012,
"height": 100.00200511623785
},
"lon": -97.74143728674808,
"lat": 30.267199989444602,
"height": 100.00200511623785
},
{
"position": {
"x": -742675.0944939142,
"y": -5463220.735958191,
"z": 3196039.4931432656
},
"cartographic": {
"longitude": -1.7059089893971566,
"latitude": 0.5282622951637073,
"height": 96.0021323949121
},
"lon": -97.74138532588458,
"lat": 30.267199988774586,
"height": 96.0021323949121
},
{
"position": {
"x": -742669.6746210232,
"y": -5463217.986379101,
"z": 3196037.4770101663
},
"cartographic": {
"longitude": -1.7059080825078698,
"latitude": 0.5282622951516531,
"height": 92.0022635916176
},
"lon": -97.74133336495596,
"lat": 30.267199988083934,
"height": 92.0022635916176
},
{
"position": {
"x": -742664.2547481323,
"y": -5463215.236800011,
"z": 3196035.460877067
},
"cartographic": {
"longitude": -1.7059071756174464,
"latitude": 0.5282622951392388,
"height": 88.00239870366701
},
"lon": -97.74128140396222,
"lat": 30.26719998737265,
"height": 88.00239870366701
},
{
"position": {
"x": -742697.1119762337,
"y": -5463197.109205559,
"z": 3196050.718428668
},
"cartographic": {
"longitude": -1.7059135236528302,
"latitude": 0.5282654439720338,
"height": 84.00156665806156
},
"lon": -97.7416451195979,
"lat": 30.267380402202193,
"height": 84.00156665806156
},
{
"position": {
"x": -742692.6230278247,
"y": -5463201.206100664,
"z": 3196052.7345617674
},
"cartographic": {
"longitude": -1.70591261679598,
"latitude": 0.5282654439597962,
"height": 88.00167827087749
},
"lon": -97.74159316052776,
"lat": 30.267380401501033,
"height": 88.00167827087749
},
{
"position": {
"x": -742688.1340794157,
"y": -5463205.30299577,
"z": 3196054.7506948668
},
"cartographic": {
"longitude": -1.705911709940266,
"latitude": 0.5282654439471988,
"height": 92.00179379845281
},
"lon": -97.74154120152274,
"lat": 30.26738040077925,
"height": 92.00179379845281
},
{
"position": {
"x": -742683.6451310067,
"y": -5463209.399890875,
"z": 3196056.766827966
},
"cartographic": {
"longitude": -1.7059108030856887,
"latitude": 0.5282654439342416,
"height": 96.00191324280611
},
"lon": -97.74148924258282,
"lat": 30.267380400036856,
"height": 96.00191324280611
},
{
"position": {
"x": -742679.1561825977,
"y": -5463213.496785982,
"z": 3196058.7829610654
},
"cartographic": {
"longitude": -1.7059098962322479,
"latitude": 0.5282654439209244,
"height": 100.00203660378942
},
"lon": -97.74143728370802,
"lat": 30.267380399273843,
"height": 100.00203660378942
},
{
"position": {
"x": -742673.7363097068,
"y": -5463210.747206892,
"z": 3196056.766827966
},
"cartographic": {
"longitude": -1.7059089893424393,
"latitude": 0.5282654439112129,
"height": 96.00216388304962
},
"lon": -97.74138532274951,
"lat": 30.26738039871741,
"height": 96.00216388304962
},
{
"position": {
"x": -742668.3164368159,
"y": -5463207.997627802,
"z": 3196054.7506948668
},
"cartographic": {
"longitude": -1.7059080824514945,
"latitude": 0.5282654439011418,
"height": 92.00229507962341
},
"lon": -97.74133336172589,
"lat": 30.267380398140382,
"height": 92.00229507962341
},
{
"position": {
"x": -742662.896563925,
"y": -5463205.248048712,
"z": 3196052.7345617674
},
"cartographic": {
"longitude": -1.7059071755594128,
"latitude": 0.5282654438907107,
"height": 88.00243019027465
},
"lon": -97.74128140063713,
"lat": 30.267380397542723,
"height": 88.00243019027465
},
{
"position": {
"x": -742695.7537920263,
"y": -5463187.120454259,
"z": 3196067.9921133686
},
"cartographic": {
"longitude": -1.7059135236064031,
"latitude": 0.528268592725402,
"height": 84.00166112141882
},
"lon": -97.74164511693783,
"lat": 30.26756081248092,
"height": 84.00166112141882
},
{
"position": {
"x": -742691.2648436173,
"y": -5463191.217349365,
"z": 3196070.008246468
},
"cartographic": {
"longitude": -1.7059126167478948,
"latitude": 0.5282685927111814,
"height": 88.00177273414431
},
"lon": -97.74159315777268,
"lat": 30.26756081166614,
"height": 88.00177273414431
},
{
"position": {
"x": -742686.7758952084,
"y": -5463195.314244471,
"z": 3196072.0243795672
},
"cartographic": {
"longitude": -1.705911709890523,
"latitude": 0.5282685926966009,
"height": 92.0018882615363
},
"lon": -97.74154119867266,
"lat": 30.267560810830734,
"height": 92.0018882615363
},
{
"position": {
"x": -742682.2869467994,
"y": -5463199.411139577,
"z": 3196074.040512667
},
"cartographic": {
"longitude": -1.7059108030342875,
"latitude": 0.5282685926816608,
"height": 96.0020077065962
},
"lon": -97.74148923963774,
"lat": 30.267560809974732,
"height": 96.0020077065962
},
{
"position": {
"x": -742677.7979983904,
"y": -5463203.508034682,
"z": 3196076.0566457664
},
"cartographic": {
"longitude": -1.7059098961791888,
"latitude": 0.5282685926663608,
"height": 100.00213106635087
},
"lon": -97.74143728066795,
"lat": 30.267560809098107,
"height": 100.00213106635087
},
{
"position": {
"x": -742672.3781254995,
"y": -5463200.758455592,
"z": 3196074.040512667
},
"cartographic": {
"longitude": -1.705908989287722,
"latitude": 0.5282685926586324,
"height": 96.00225834543971
},
"lon": -97.74138531961444,
"lat": 30.267560808655297,
"height": 96.00225834543971
},
{
"position": {
"x": -742666.9582526085,
"y": -5463198.0088765025,
"z": 3196072.0243795672
},
"cartographic": {
"longitude": -1.705908082395119,
"latitude": 0.5282685926505436,
"height": 92.00238954127981
},
"lon": -97.74133335849581,
"lat": 30.267560808191845,
"height": 92.00238954127981
},
{
"position": {
"x": -742661.5383797176,
"y": -5463195.259297413,
"z": 3196070.008246468
},
"cartographic": {
"longitude": -1.7059071755013793,
"latitude": 0.5282685926420952,
"height": 88.0025246543197
},
"lon": -97.74128139731205,
"lat": 30.267560807707792,
"height": 88.0025246543197
}
]
}
}
+6 -2
View File
@@ -2286,7 +2286,9 @@
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/globe/contract.js"
"src/globe/contract.js",
"src/globe/cesium/qaTileset.js",
"src/maps/qaTileset.js"
],
"external": ["cesium"]
},
@@ -3593,7 +3595,9 @@
"src/globe/cesium/orbital.js",
"src/globe/cesium/worldTime.js",
"src/data/geoImportLimits.js",
"src/data/geoImportPlan.js"
"src/data/geoImportPlan.js",
"src/globe/cesium/qaTileset.js",
"src/maps/qaTileset.js"
],
"external": [
"hls.js",
+1
View File
@@ -352,6 +352,7 @@ const runs = [
],
['radio-engine-switch', 'radio-engine-switch', ['radio']],
['imagery-contract', 'imagery-contract', ['recent-imagery']],
['tiles-fixture', 'tiles-fixture', ['osm-places']],
['fire-camera-switch', 'fire-camera-switch', ['alpr-cameras']],
[
'fire-camera-switch--fires',
+1 -1
View File
@@ -3,7 +3,7 @@
"ownLaunch": 99,
"fixedSleeps": 86,
"loadingScreenReadiness": 1,
"cesiumOnlyReads": 75,
"cesiumOnlyReads": 76,
"ownWaitHelpers": 19
}
}
+39 -4
View File
@@ -117,10 +117,45 @@ export async function assertPreviewPortAvailable(href, request = fetch) {
/** Check the running page before any scenario touches its layers. */
export async function assertKeylessPreview(page) {
await page.evaluate(() => {
if (window.__godsEyeView.tileset || window.__GOOGLE_MAPS_API_KEY__)
throw new Error('Keyless preview required');
});
await page.evaluate(
(fixturePath) => {
const app = window.__godsEyeView;
const source = app.engine?.maps?.current()?.photoreal;
const fixture =
new URLSearchParams(location.search).get('qaTileset') === fixturePath &&
source?.tilesetUrl === fixturePath &&
(!source.tileset ||
new URL(source.tileset.resource?.url, location.href).href ===
location.origin + fixturePath);
if (
window.__GOOGLE_MAPS_API_KEY__ ||
(app.tileset && (!fixture || app.tileset !== source.tileset)) ||
((source || app.engine?.maps?.photorealActive()) && !fixture)
)
throw new Error('Keyless preview required');
},
TILES_FIXTURE_PATH,
);
}
export const TILES_FIXTURE_PATH = '/__qa/tiles3d/tileset.json';
// CDP serves these exact same-origin URLs, including requests from workers.
// Keeping them in scripts/ avoids shipping fixture assets in normal builds.
export function tilesFixtureRoutes(origin) {
return ['tileset.json', 'plate.glb', 'block50.glb', 'block200.glb', 'roof.glb'].map(
(name) => ({
pattern: new URL(`/__qa/tiles3d/${name}`, origin).href,
respond: () => ({
contentType: name.endsWith('.json')
? 'application/json'
: 'model/gltf-binary',
body: fs.readFileSync(
new URL(`./fixtures/tiles3d/${name}`, import.meta.url),
),
}),
}),
);
}
/** The URL with no remote IP: fulfilled by a fixture or a cache, or a data URL. */
+3
View File
@@ -812,10 +812,13 @@ test('picker readiness also settles the surviving engine after a rollback', asyn
observe() {}
disconnect() {}
};
// The keyless check reads the page address for the tiles fixture.
globalThis.location = new URL('http://localhost:4300/');
t.after(() => {
delete globalThis.window;
delete globalThis.document;
delete globalThis.MutationObserver;
delete globalThis.location;
});
engine.render.request = () => setImmediate(() => engine.fake.frame());
const page = {
+458
View File
@@ -0,0 +1,458 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import * as geo from '../src/geo/index.js';
import {
assertKeylessPreview,
createStrict,
localPreview,
openPage,
TILES_FIXTURE_PATH,
tilesFixtureRoutes,
waitForCameraAtRest,
waitForEngine,
} from './qa-strict-harness.mjs';
const base = localPreview(process.argv[2]);
const { extras: fixture } = JSON.parse(
readFileSync(new URL('./fixtures/tiles3d/tileset.json', import.meta.url)),
);
base.search = `?engine=navara&welcome=0&qaTileset=${encodeURIComponent(TILES_FIXTURE_PATH)}`;
base.hash = `v=2&lat=${fixture.origin.lat}&lon=${fixture.origin.lon}&alt=1100&heading=0&pitch=-90&roll=0&map=photoreal&l=`;
let releaseTiles;
let pendingTiles;
const holdTiles = () => {
pendingTiles = new Promise((resolve) => {
releaseTiles = resolve;
});
};
holdTiles();
const routes = tilesFixtureRoutes(base.origin).map((route) => ({
...route,
async respond(request) {
if (route.pattern.endsWith('.glb')) await pendingTiles;
return route.respond(request);
},
}));
routes.push({
pattern: `${base.origin}/api/overpass/status`,
respond: () => ({
contentType: 'application/json',
body: '{"configured":true}',
}),
});
const strict = createStrict({
origin: base.origin,
layers: ['osm-places'],
routes,
});
await strict.run({}, async (browser) => {
const { page, errors } = await openPage(browser, { shaderErrors: true });
await page.setCacheEnabled(false);
let readsBefore = { ...strict.ledger.routes };
await page.goto(base.href, { waitUntil: 'domcontentloaded' });
for (const kind of ['navara', 'cesium']) {
if (kind === 'cesium') {
readsBefore = { ...strict.ledger.routes };
holdTiles();
await strict.pick(page, kind, { settle: false });
}
try {
await page.waitForFunction(
(kind) => {
const engine = window.__godsEyeView?.engine;
return (
engine?.kind === kind &&
engine.maps.photorealActive() &&
!engine.surface.tilesLoaded()
);
},
{ timeout: 90000 },
kind,
);
await strict.passed(
page,
'osm-places',
'tilesLoaded is false while GLBs are held',
);
} finally {
releaseTiles();
}
await waitForEngine(page, kind);
await page.evaluate(
async (destination) => {
const { engine, styleManager } = window.__godsEyeView;
styleManager.orbitController.stop();
engine.camera.cancelFlight();
await engine.maps.setTerrain({ kind: 'ellipsoid' });
engine.camera.setView({
destination,
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
engine.render.request('qa-tiles-fixture');
},
geo.cartesian3.fromDegrees(fixture.origin.lon, fixture.origin.lat, 1100),
);
await loaded(page);
await waitForCameraAtRest(page);
for (const route of routes.slice(0, 5))
assert.ok(
strict.ledger.routes[route.pattern] > (readsBefore[route.pattern] ?? 0),
`${kind} intercepted ${new URL(route.pattern).pathname}`,
);
await strict.passed(
page,
'osm-places',
'tilesLoaded becomes true; all five files served by same-origin routes',
);
const heights = await page.evaluate(
({ probes, clampStart }) => {
const { surface } = window.__godsEyeView.engine;
return {
sampled: Object.fromEntries(
Object.entries(probes).map(([id, p]) => [
id,
surface.sampleHeight(p.cartographic),
]),
),
clamped: surface.clampToHeight(clampStart),
};
},
{
probes: fixture.probes,
clampStart: geo.cartesian3.fromDegrees(
fixture.probes.roof.lon,
fixture.probes.roof.lat,
fixture.probes.roof.height + 300,
),
},
);
for (const [id, p] of Object.entries(fixture.probes))
assert.ok(
Math.abs(heights.sampled[id] - p.height) <= 0.5,
`${kind} ${id}: ${JSON.stringify(heights)}`,
);
assert.ok(
Math.abs(heights.sampled.block50 - heights.sampled.plate - 50) <= 0.5,
);
assert.ok(
heights.clamped &&
geo.cartesian3.distance(
heights.clamped,
fixture.probes.roof.position,
) <= 0.5,
);
await strict.passed(
page,
'osm-places',
'known plate, 50 m, 200 m and sloping roof heights; clamp lands on roof',
heights,
);
const drapes = [];
for (const surface of ['tiles', 'terrain']) {
if (surface === 'terrain') {
await page.evaluate(async () => {
const { engine, mapStackController } = window.__godsEyeView;
await mapStackController.setStack('osm');
await engine.maps.setTerrain({ kind: 'ellipsoid' });
});
await loaded(page);
}
const p = fixture.probes.block50;
const position = geo.cartesian3.fromDegrees(
p.lon,
p.lat,
surface === 'tiles' ? p.height : 0,
);
const baseline = await pixels(page, position);
for (const drape of ['tiles', 'terrain']) {
await page.evaluate(
({ lon, lat, drape }) => {
const { engine } = window.__godsEyeView;
const set = engine.ground.createSet({
id: 'qa-drape',
kind: 'cell',
drape,
pickable: false,
});
set.replace([
{
west: lon - 0.0003,
east: lon + 0.0003,
south: lat - 0.0003,
north: lat + 0.0003,
fill: [1, 0, 1, 1],
},
]);
set.commit();
window.qaDrape = set;
},
{ ...p, drape },
);
try {
await page.waitForFunction(
() => {
window.__godsEyeView.engine.render.request('qa-drape');
return window.qaDrape.ready();
},
{ timeout: 30000 },
);
const rgb = await pixels(page, position);
const drawn = rgb[0] > 180 && rgb[1] < 80 && rgb[2] > 180;
assert.equal(
drawn,
surface === drape,
`${kind} ${drape} on ${surface}: ${rgb}`,
);
if (surface !== drape)
assert.ok(
rgb.every((v, i) => Math.abs(v - baseline[i]) < 15),
'other surface stays unchanged',
);
drapes.push({ surface, drape, baseline, rgb });
} finally {
await page.evaluate(() => {
window.qaDrape.destroy();
delete window.qaDrape;
});
}
}
}
await strict.passed(
page,
'osm-places',
'drape tiles and terrain draw only on their respective surface (pixels)',
drapes,
);
await page.evaluate(() =>
window.__godsEyeView.mapStackController.setStack('photoreal'),
);
await loaded(page);
await waitForCameraAtRest(page);
await checkPoints(page, kind);
await assertKeylessPreview(page);
console.log(
`PASS ${kind} fixture heights, clamp, drapes, tilesLoaded and surface-points budget`,
);
}
assert.deepEqual(errors, []);
assert.deepEqual(
Object.keys(strict.ledger.external).filter((host) =>
/google|gstatic|cesium\.com/i.test(host),
),
[],
);
});
async function loaded(page) {
await page.waitForFunction(
() => {
const { engine } = window.__godsEyeView;
engine.render.request('qa-tiles-fixture');
return engine.surface.tilesLoaded();
},
{ polling: 100, timeout: 90000 },
);
}
// Read actual canvas pixels during postRender; Cesium discards them afterwards.
async function pixels(page, position) {
return page.evaluate(async (position) => {
const { engine } = window.__godsEyeView;
const canvas = document.createElement('canvas');
canvas.width = canvas.height = 5;
const context = canvas.getContext('2d', { willReadFrequently: true });
for (let frame = 0; frame < 2; frame++)
await new Promise((resolve) => {
const off = engine.frames.postRender.addEventListener(() => {
off();
const point = engine.projector.worldToWindow(position);
const scale = engine.host.canvas.width / engine.host.size().width;
context.drawImage(
engine.host.canvas,
Math.round(point.x * scale) - 2,
Math.round(point.y * scale) - 2,
5,
5,
0,
0,
5,
5,
);
resolve();
});
engine.render.request('qa-drape-pixels');
});
const data = context.getImageData(0, 0, 5, 5).data;
return [0, 1, 2].map(
(channel) =>
data.reduce(
(sum, value, i) => sum + (i % 4 === channel ? value : 0),
0,
) / 25,
);
}, position);
}
async function checkPoints(page, kind) {
await page.evaluate(async (points) => {
const { engine, viewer, dataManager } = window.__godsEyeView;
const layer = dataManager.layers.get('osm-places').module;
layer.setResults([], null);
await dataManager.setEnabled('osm-places', true);
// Reattach this real layer once so its mover commits can be observed.
layer.detachRenderer();
const createSet = engine.movers.createSet;
const sampleHeight = engine.surface.sampleHeight;
const target =
engine.kind === 'cesium'
? viewer.scene._picking._pickOffscreenView.pickFramebuffer
: engine.extensions.navara.view._renderer;
const method = engine.kind === 'cesium' ? 'begin' : 'render';
const render = target[method];
if (typeof render !== 'function')
throw new Error('Missing pick-render counter hook');
const stats = (window.qaPoints = {
passes: [],
positions: [],
samples: 0,
renders: 0,
});
let pass = null,
passStart = 0,
sampling = false;
target[method] = function (...args) {
if (sampling) {
pass.renders++;
stats.renders++;
}
return render.apply(this, args);
};
engine.surface.sampleHeight = function (...args) {
if (!pass) {
passStart = performance.now();
pass = {
samples: 0,
renders: 0,
ms: 0,
startsMs: [],
cameraEstimate: !points.some(
(p) =>
Math.abs(p.cartographic.longitude - args[0].longitude) < 1e-10 &&
Math.abs(p.cartographic.latitude - args[0].latitude) < 1e-10,
),
};
stats.passes.push(pass);
// A placement callback samples synchronously, without yielding.
queueMicrotask(() => {
pass = null;
});
}
pass.samples++;
stats.samples++;
const start = performance.now();
pass.startsMs.push(start - passStart);
sampling = true;
try {
return sampleHeight.apply(this, args);
} finally {
sampling = false;
pass.ms += performance.now() - start;
}
};
engine.movers.createSet = function (options) {
const set = createSet.call(this, options);
if (options.id !== 'osm-places') return set;
const write = set.set,
commit = set.commit;
const positions = [];
set.set = function (index, fields) {
if (fields.position) positions[index] = { ...fields.position };
return write.call(this, index, fields);
};
set.commit = function () {
const result = commit.call(this);
stats.positions = positions.map((p) => ({ ...p }));
return result;
};
return set;
};
window.qaRestorePoints = () => {
engine.surface.sampleHeight = sampleHeight;
engine.movers.createSet = createSet;
target[method] = render;
};
layer.attachRenderer(viewer, { engine });
layer.setResults(
points.map((p, i) => ({ id: String(i), lon: p.lon, lat: p.lat })),
{ kind: 'fixture', label: 'Roof points', area: 'Austin' },
);
}, fixture.points);
try {
const expected = fixture.points.map((p) =>
geo.cartesian3.fromDegrees(p.lon, p.lat, p.height + 2),
);
await page.waitForFunction(
(expected) => {
const positions = window.qaPoints.positions;
return (
positions.length === expected.length &&
positions.every(
(p, i) =>
Math.hypot(
p.x - expected[i].x,
p.y - expected[i].y,
p.z - expected[i].z,
) <= 0.5,
)
);
},
{ polling: 100, timeout: 30000 },
expected,
);
const result = await page.evaluate(() => ({
...window.qaPoints,
drawn: window.__godsEyeView.engine.movers
.list()
.find((s) => s.id === 'osm-places'),
}));
assert.equal(result.drawn.count, fixture.points.length);
assert.equal(result.drawn.shown, true);
assert.ok(
result.samples >= fixture.points.length && result.passes.length > 1,
);
assert.ok(
result.passes.every((p) => p.samples <= 16 && p.renders <= 16),
JSON.stringify(result.passes),
);
// The 4 ms limit is checked between points: one expensive sample (and
// its initial camera estimate) may finish after the deadline.
assert.ok(
result.passes.every((p) =>
p.startsMs.slice(p.cameraEstimate ? 2 : 1).every((ms) => ms < 4),
),
JSON.stringify(result.passes),
);
if (kind === 'cesium')
assert.ok(
result.renders >= fixture.points.length,
'count actual offscreen pick renders',
);
else assert.equal(result.renders, 0, 'tile sampling uses CPU rays');
await strict.passed(
page,
'osm-places',
'40 roof points committed at surface + 2 m; 16 samples/pick renders and 4 ms soft budget per pass',
result,
);
} finally {
await page.evaluate(async () => {
window.qaRestorePoints();
const { dataManager } = window.__godsEyeView;
await dataManager.setEnabled('osm-places', false);
dataManager.layers.get('osm-places').module.setResults([], null);
});
}
}
+226
View File
@@ -0,0 +1,226 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { Cesium3DTileset } from 'cesium';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { Raycaster, Vector3 } from 'three';
import { buildTilesFixture } from './build-tiles3d-fixture.mjs';
import { localTilesetPath, qaTilesetUrl } from '../src/maps/qaTileset.js';
import { createDefaultMapSources } from '../src/maps/defaultSources.js';
import { MapSourceController } from '../src/maps/controller.js';
import { createFakeEngine } from '../src/globe/testing/fakeEngine.js';
import { createNavaraMaps } from '../src/globe/navara/maps.js';
import {
assertKeylessPreview,
TILES_FIXTURE_PATH,
tilesFixtureRoutes,
} from './qa-strict-harness.mjs';
function globals(t, values) {
for (const [name, value] of Object.entries(values)) {
const old = Object.getOwnPropertyDescriptor(globalThis, name);
Object.defineProperty(globalThis, name, {
configurable: true,
writable: true,
value,
});
t.after(() => {
if (old) Object.defineProperty(globalThis, name, old);
else delete globalThis[name];
});
}
}
test('fixture is reproducible, self-contained and under 200 KB', () => {
const files = buildTilesFixture();
assert.equal(files.size, 5);
assert.ok(
[...files.values()].reduce((sum, bytes) => sum + bytes.length, 0) < 200_000,
);
for (const [name, bytes] of files)
assert.deepEqual(
readFileSync(new URL(`./fixtures/tiles3d/${name}`, import.meta.url)),
bytes,
);
const tileset = JSON.parse(files.get('tileset.json'));
for (const child of tileset.root.children)
assert.ok(files.has(child.content.uri));
assert.equal(tileset.extras.points.length, 40);
});
test('independent glTF loader and triangle rays recover the authored heights', async () => {
const files = buildTilesFixture();
const probes = [
['plate', 0, -170, 0],
['block50', -160, 0, 50],
['block200', 0, 170, 200],
['roof', 180, 0, 84],
['roof', 140, 30, 84],
['roof', 160, 0, 100],
];
for (const [name, east, north, height] of probes) {
const bytes = files.get(`${name}.glb`);
const gltf = await new GLTFLoader().parseAsync(
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
'',
);
gltf.scene.updateMatrixWorld(true);
const ray = new Raycaster(
new Vector3(east, 1000, -north),
new Vector3(0, -1, 0),
);
const hits = ray.intersectObject(gltf.scene, true);
assert.ok(hits.length, name);
assert.ok(
Math.abs(hits[0].point.y - height) < 1e-5,
`${name}: ${hits[0].point.y}`,
);
}
});
test('QA URL switch is inert by default and rejects nonlocal or malformed paths', (t) => {
const location = new URL('http://localhost:4303/');
globals(t, { location });
assert.equal(qaTilesetUrl(), null);
assert.equal(createDefaultMapSources().defaultId, 'esri-imagery');
location.searchParams.set('qaTileset', TILES_FIXTURE_PATH);
assert.equal(qaTilesetUrl(), TILES_FIXTURE_PATH);
for (const value of [
'https://example.com/tileset.json',
'http://localhost:4303/tileset.json',
'//example.com/x',
'/\\example.com/x',
'relative.json',
'/x\n',
'/x?key=test',
'/x#fragment',
]) {
assert.throws(() => localTilesetPath(value), /page origin/);
for (const engineKind of ['cesium', 'navara'])
assert.throws(
() => createDefaultMapSources({ engineKind, tilesetUrl: value }),
/page origin/,
);
}
});
test('controller passes local URL to Navara and owns the Cesium factory result', async (t) => {
globals(t, { location: new URL('http://localhost:4303/') });
const tileset = { url: TILES_FIXTURE_PATH, show: true, destroy: t.mock.fn() };
const factory = t.mock.method(
Cesium3DTileset,
'fromUrl',
async () => tileset,
);
for (const engineKind of ['navara', 'cesium']) {
const registry = createDefaultMapSources({
engineKind,
tilesetUrl: TILES_FIXTURE_PATH,
});
const engine = createFakeEngine({ kind: engineKind });
const controller = new MapSourceController({ registry, engine });
await controller.setStack('photoreal');
const source = engine.maps.current().photoreal;
assert.equal(source.tilesetUrl, TILES_FIXTURE_PATH);
assert.equal(source.tileset, engineKind === 'cesium' ? tileset : undefined);
assert.equal(factory.mock.callCount(), engineKind === 'cesium' ? 1 : 0);
controller.destroy();
engine.destroy();
}
assert.equal(factory.mock.calls[0].arguments[0], TILES_FIXTURE_PATH);
assert.equal(tileset.destroy.mock.callCount(), 1);
});
test('Navara accepts the local 3D source without a key or Google credit, and rejects remote paths', async (t) => {
globals(t, { location: new URL('http://localhost:4303/') });
const sources = [],
credits = [];
const view = {
addSource: (desc) => {
sources.push(desc);
return { delete() {} };
},
addLayer: () => ({ id: 'fixture', delete() {} }),
forceUpdate() {},
attribution: {
add: (items) => credits.push(...items),
remove() {},
show: t.mock.fn(),
},
};
const owner = createNavaraMaps(view, {
setInterval: () => 1,
clearInterval() {},
});
t.after(owner.destroy);
await owner.maps.setPhotorealTiles({ tilesetUrl: TILES_FIXTURE_PATH });
assert.deepEqual(sources, [
{ type: '3d-tiles', url: `http://localhost:4303${TILES_FIXTURE_PATH}` },
]);
assert.equal(owner.maps.photorealActive(), true);
assert.deepEqual(credits, []);
assert.equal(view.attribution.show.mock.callCount(), 0);
await assert.rejects(
owner.maps.setPhotorealTiles({
tilesetUrl: '//example.com/tileset.json',
googleApiKey: 'unused',
}),
/page origin/,
);
assert.equal(sources.length, 1);
await owner.maps.setPhotorealTiles(null);
assert.equal(owner.maps.photorealActive(), false);
});
test('strict keyless assertion permits only the opted-in fixture and its real tileset URL', async (t) => {
const location = new URL(
`http://localhost:4303/?qaTileset=${encodeURIComponent(TILES_FIXTURE_PATH)}`,
);
const source = {
tilesetUrl: TILES_FIXTURE_PATH,
tileset: { resource: { url: TILES_FIXTURE_PATH } },
};
const app = {
engine: {
maps: {
current: () => ({ photoreal: source }),
photorealActive: () => true,
},
},
};
const window = { __godsEyeView: app };
globals(t, { window, location });
const page = { evaluate: async (fn, ...args) => fn(...args) };
await assertKeylessPreview(page);
source.tileset.resource.url = 'https://example.com/tileset.json';
await assert.rejects(assertKeylessPreview(page), /Keyless/);
source.tileset.resource.url = TILES_FIXTURE_PATH;
location.search = '';
await assert.rejects(assertKeylessPreview(page), /Keyless/);
location.searchParams.set('qaTileset', TILES_FIXTURE_PATH);
source.tilesetUrl = '/another/tileset.json';
await assert.rejects(assertKeylessPreview(page), /Keyless/);
source.tilesetUrl = TILES_FIXTURE_PATH;
window.__GOOGLE_MAPS_API_KEY__ = 'key';
await assert.rejects(assertKeylessPreview(page), /Keyless/);
delete window.__GOOGLE_MAPS_API_KEY__;
app.tileset = { url: '/another/tileset.json' };
await assert.rejects(assertKeylessPreview(page), /Keyless/);
});
test('harness exposes exactly the five binary-safe fixture routes on the preview origin', () => {
const routes = tilesFixtureRoutes('http://localhost:4303');
assert.equal(routes.length, 5);
const files = buildTilesFixture();
for (const route of routes) {
const url = new URL(route.pattern);
assert.equal(url.origin, 'http://localhost:4303');
assert.ok(!route.pattern.includes('*'));
const answer = route.respond();
assert.deepEqual(answer.body, files.get(url.pathname.split('/').at(-1)));
assert.equal(
answer.contentType,
url.pathname.endsWith('.glb') ? 'model/gltf-binary' : 'application/json',
);
}
});
+8
View File
@@ -0,0 +1,8 @@
import { Cesium3DTileset } from 'cesium';
import { localTilesetPath } from '../../maps/qaTileset.js';
/** The QA source uses the controller's normal tileset ownership path. */
export function createQaTileset(url, { signal } = {}) {
signal?.throwIfAborted();
return Cesium3DTileset.fromUrl(localTilesetPath(url));
}
+16 -10
View File
@@ -22,6 +22,7 @@ import {
SENTINEL2_MAX_ZOOM,
SENTINEL2_URL,
} from '../../maps/sentinel2.js';
import { localTilesetPath } from '../../maps/qaTileset.js';
const OSM_URL = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
const OSM_CREDIT = {
@@ -280,12 +281,15 @@ export function createNavaraMaps(
return;
}
const key = String(source.googleApiKey || '').trim();
if (!key)
const tilesetUrl = localTilesetPath(source.tilesetUrl);
if (!tilesetUrl && !key)
throw new Error(
'Google 3D tiles on Navara need a browser key (googleApiKey)',
);
if (destroyed || generation !== generations.display) return;
const url = `${GOOGLE_TILES_ROOT}?key=${encodeURIComponent(key)}`;
const url = tilesetUrl
? new URL(tilesetUrl, globalThis.location.origin).href
: `${GOOGLE_TILES_ROOT}?key=${encodeURIComponent(key)}`;
setImageryShown(false);
currentBase = null;
tilesSource = source;
@@ -296,17 +300,19 @@ export function createNavaraMaps(
url,
{ type: '3d-tiles', url },
{ type: '3d-tiles', model: { maxSse: PHOTOREAL_MAX_SSE } },
[
(layer) => ({
attribution: 'Google',
logo: GOOGLE_LOGO,
creditLayerId: layer.id,
}),
],
tilesetUrl
? []
: [
(layer) => ({
attribution: 'Google',
logo: GOOGLE_LOGO,
creditLayerId: layer.id,
}),
],
);
// Google's terms want its copyright lines on screen, as Cesium shows
// them; Navara lists them in its attribution card, collapsed by default.
view.attribution?.show();
if (!tilesetUrl) view.attribution?.show();
},
releasePhotorealTiles(source) {
if (!knownTiles.delete(source)) return;
+1
View File
@@ -408,6 +408,7 @@ export class MapSourceController {
descriptor: source.descriptor,
kind: source.descriptor.kind,
credit: source.credit ?? null,
...(source.tilesetUrl && { tilesetUrl: source.tilesetUrl }),
...(source.googleApiKey && { googleApiKey: source.googleApiKey }),
...(source.cesiumToken && { cesiumToken: source.cesiumToken }),
...(tileset && { tileset }),
+14 -2
View File
@@ -7,6 +7,7 @@ import {
} from '../globe/cesium/mapImagery.js';
import { terrainSourceFor } from '../globe/cesium/terrain.js';
import { t } from '../i18n/index.js';
import { localTilesetPath, qaTilesetUrl } from './qaTileset.js';
const navaraIonImageryReason = () =>
t('Bing imagery is not available on the experimental Navara engine');
@@ -24,10 +25,12 @@ export function createDefaultMapSources({
cesiumToken = '',
googleApiKey = '',
engineKind = 'cesium',
tilesetUrl = qaTilesetUrl(),
} = {}) {
if (engineKind !== 'cesium' && engineKind !== 'navara')
throw new TypeError(`Unknown globe engine: ${engineKind}`);
const navara = engineKind === 'navara';
tilesetUrl = localTilesetPath(tilesetUrl);
const ionToken = String(cesiumToken || '').trim();
const hasIon = Boolean(ionToken);
// Engine-neutral descriptions ride beside the Cesium factories: a terrain
@@ -40,7 +43,8 @@ export function createDefaultMapSources({
};
const googleKey = String(googleApiKey || '').trim();
const hasGoogle = Boolean(googleKey);
const photorealAvailable = navara ? hasGoogle : Boolean(googleTileset);
const photorealAvailable =
Boolean(tilesetUrl) || (navara ? hasGoogle : Boolean(googleTileset));
return {
defaultId: photorealAvailable ? 'photoreal' : 'esri-imagery',
unknownId: 'photoreal',
@@ -69,7 +73,15 @@ export function createDefaultMapSources({
photorealUnavailableReason(
navara ? hasGoogle : hasIon || hasGoogle,
),
tileset: googleTileset,
tileset: tilesetUrl ? null : googleTileset,
...(tilesetUrl && {
tilesetUrl,
createTileset: async (options) => {
const { createQaTileset } =
await import('../globe/cesium/qaTileset.js');
return createQaTileset(tilesetUrl, options);
},
}),
...(googleKey && { googleApiKey: googleKey }),
...(hasIon && { cesiumToken: ionToken }),
};
+22
View File
@@ -0,0 +1,22 @@
/** Only a root-relative path on this page may replace the photoreal source. */
export function localTilesetPath(value, location = globalThis.location) {
if (!value) return null;
if (
typeof value !== 'string' ||
!/^\/(?!\/)/.test(value) ||
/[\\\s\u0000-\u001f\u007f]/.test(value) ||
!location?.origin
)
throw new TypeError('tilesetUrl must be a path on the page origin');
const url = new URL(value, location.origin);
if (url.origin !== location.origin || url.search || url.hash)
throw new TypeError('tilesetUrl must be a path on the page origin');
return url.pathname;
}
// Like parkedMs, this explicit QA parameter is inert on an ordinary page.
export function qaTilesetUrl() {
return localTilesetPath(
new URLSearchParams(globalThis.location?.search).get('qaTileset'),
);
}