Files
gods-eye-view/src/data/boundaryLabels.test.mjs
T

163 lines
5.5 KiB
JavaScript

// Outline names are drawn by the DOM annotation overlay, never as Cesium
// text labels: the boundary data modules, the outline resolver and the
// annotation renderers must not import or construct Cesium label primitives,
// and drawing a bundled outline must create none at runtime.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import * as Cesium from 'cesium';
import { FEATURE_SOURCE_METHODS } from '../sources/featureSource.js';
import { createAnnotationResolver } from '../annotations/resolver.js';
import { createWorldAnnotationRenderer } from '../annotations/worldAnnotationRenderer.js';
const read = (path) => readFileSync(new URL(path, import.meta.url), 'utf8');
/** Pure data modules: no Cesium at all. */
const DATA_MODULES = [
'./adminBoundaries.js',
'./placeBoundaries.js',
'./neighborhoodPolygons.js',
'./local_data/us_census_places/files.js',
'./local_data/wof_neighborhoods/files.js',
];
/** Modules that use Cesium for outlines but must not draw text with it. */
const CESIUM_MODULES = [
'../annotations/resolver.js',
'../annotations/worldAnnotationRenderer.js',
'../annotations/hybridAnnotationRenderer.js',
];
/** The DOM overlay: callout elements, never Cesium label primitives. */
const OVERLAY_MODULES = ['../annotations/screenAnnotationRenderer.js'];
const LABEL_PRIMITIVES = [
/\bLabelCollection\b/,
/\bLabelGraphics\b/,
/\bLabelStyle\b/,
/\bLabelVisualizer\b/,
];
/** Entity label options, inline or assigned later. */
const ENTITY_LABELS = [
// `label: { text: … }` or `label: new …`
/\blabel\s*:\s*\{[^{}]*\btext\s*:/s,
/\blabel\s*:\s*new\b/,
// `label: labelOptions` (anything but `null`)
/\blabel\s*:\s*(?!null\b)[A-Za-z_$][\w$]*\s*[,}\n]/,
// `entity.label = …`
/\.label\s*=(?!=)/,
];
test('boundary data modules never import Cesium', () => {
for (const path of DATA_MODULES) {
const source = read(path);
assert.doesNotMatch(source, /from\s+['"]cesium['"]/, path);
assert.doesNotMatch(source, /import\(\s*['"]cesium['"]/, path);
}
});
test('outline modules and renderers construct no Cesium labels', () => {
for (const path of [...DATA_MODULES, ...CESIUM_MODULES, ...OVERLAY_MODULES])
for (const pattern of LABEL_PRIMITIVES)
assert.doesNotMatch(read(path), pattern, `${path}: ${pattern}`);
for (const path of [...DATA_MODULES, ...CESIUM_MODULES])
for (const pattern of ENTITY_LABELS)
assert.doesNotMatch(read(path), pattern, `${path}: ${pattern}`);
});
test('the static check catches a Cesium label however it is written', () => {
const offending = [
'viewer.scene.primitives.add(new Cesium.LabelCollection());',
"viewer.entities.add({ position, label: { text: 'Austin' } });",
'const graphics = new LabelGraphics({ text });',
'viewer.entities.add({ position, label: labelOptions });',
'entity.label = new Cesium.LabelGraphics(options);',
'entity.label = options;',
];
const all = [...LABEL_PRIMITIVES, ...ENTITY_LABELS];
for (const source of offending)
assert.ok(
all.some((pattern) => pattern.test(source)),
source,
);
// Data, not Cesium labels: a result's label point, a stripped proxy label.
for (const source of [
'label: geometry.label,',
'liveProxy(anno, { label: null });',
'if (entity.label === x) {}',
])
assert.ok(!ENTITY_LABELS.some((p) => p.test(source)), source);
});
test('drawing bundled place and neighborhood outlines creates no Cesium label', async (t) => {
// Cesium's material setup checks for these browser image types.
for (const name of [
'HTMLCanvasElement',
'HTMLImageElement',
'ImageBitmap',
'OffscreenCanvas',
]) {
if (name in globalThis) continue;
globalThis[name] = class {};
t.after(() => delete globalThis[name]);
}
const featureSource = {};
for (const method of FEATURE_SOURCE_METHODS)
featureSource[method] = async () => null;
const { resolveAnnotationTarget } = createAnnotationResolver({
featureSource,
});
const primitives = [];
const dataSources = [];
const viewer = {
camera: {
positionCartographic: Cesium.Cartographic.fromDegrees(
-97.7431,
30.2672,
3000,
),
},
scene: { primitives: { add: (p) => primitives.push(p) } },
dataSources: {
add: (ds) => dataSources.push(ds),
remove() {},
},
};
const renderer = createWorldAnnotationRenderer(viewer);
for (const [target, lon, lat] of [
['Austin', -97.7431, 30.2672],
['Notting Hill', -0.2, 51.511],
]) {
viewer.camera.positionCartographic = Cesium.Cartographic.fromDegrees(
lon,
lat,
3000,
);
const resolved = await resolveAnnotationTarget({
// The resolver reads only the camera (no scene to pick from).
viewer: { camera: viewer.camera },
target,
footprint: true,
deferFootprint: true,
});
assert.equal(resolved.source, 'bundled', target);
renderer.add({
id: target,
type: 'area',
label: resolved.label,
ring: resolved.ring,
polygons: resolved.polygons,
footprintKind: resolved.footprintKind,
synthesized: resolved.synthesized,
anchor: { lon: resolved.lon, lat: resolved.lat, height: 0 },
color: 'primary',
});
}
assert.equal(primitives.length, 0, 'no primitive collections');
const entities = dataSources.flatMap((ds) => ds.entities.values);
assert.ok(entities.length >= 2, 'the outlines were drawn');
for (const entity of entities) assert.equal(entity.label, undefined);
renderer.destroy();
});