mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Merge branch 'german/refactor-e6-tiles-fixture' into german/navara-refactor
# Conflicts: # scripts/qa-harness-baseline.json
This commit is contained in:
commit
1ac9f28c4a
18 files changed
+1857
-19
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,7 +3,7 @@
|
||||
"ownLaunch": 99,
|
||||
"fixedSleeps": 86,
|
||||
"loadingScreenReadiness": 1,
|
||||
"cesiumOnlyReads": 75,
|
||||
"cesiumOnlyReads": 76,
|
||||
"ownWaitHelpers": 19
|
||||
}
|
||||
}
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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