mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Check the photoreal paths in the browser on a synthetic 3D Tiles set
A generated tileset (a plate at ellipsoid level, 50 m and 200 m blocks and a pitched roof, 27 KB) replaces Google's tiles when a QA page passes ?qaTileset=/path; only a root-relative path on the page origin is accepted, and the Google credit is not shown for it. The strict harness serves the set from disk, so nothing reaches Google. qa-tiles-fixture checks on both engines that sampleHeight reads the block at 50 m, clampToHeight lands on the roof, tiles and terrain drapes draw where they should, tilesLoaded turns true, surface points land on the roof within their budget, and pick renders per pass stay within it.
This commit is contained in:
1 parent
a90407d547
commit
e9ba5e9859
17 files changed
+1855
-20
No files matched your search
@@ -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.
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"ownLaunch": 99,
|
||||
"fixedSleeps": 118,
|
||||
"loadingScreenReadiness": 9,
|
||||
"cesiumOnlyReads": 75,
|
||||
"cesiumOnlyReads": 76,
|
||||
"ownWaitHelpers": 19
|
||||
},
|
||||
"measuredAt": "Navara canaries run a bare Navara view outside the app, so they launch Chrome themselves"
|
||||
"measuredAt": "Tiles fixture measures Cesium offscreen pick renders and Navara renderer calls during surface sampling"
|
||||
}
|
||||
@@ -93,10 +93,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. */
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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 }),
|
||||
};
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user