Files
gods-eye-view/src/ui/flyToDataSource.js
T
kapturnerandClaude Opus 5.5 644a341b4c fix(import): address review on metadata, bounds, navigation, clock and GPX
- Imported metadata stays inert: descriptions, native labels, properties and
  KML extended data are dropped from every entity; names are kept as short
  plain text. GeoJSON loads with a describe that builds no HTML, and
  simplestyle (including marker-symbol) is dropped, so no icon asset is
  fetched for a file and GeoJSON draws in its row's color.
- Bound geometry before loading: features and vertices are counted in the
  parsed GeoJSON, the KML document and while reading GPX, and a file over
  20,000 features or 250,000 vertices is refused before Cesium builds it.
- Imports run one batch at a time, so concurrent picks or drops cannot pass
  the 12-file cap.
- Fly-to framing walks long tracks by index instead of spreading them into a
  call.
- Fly-to is an immediate navigation through the application shell; the fly
  after an import is a deferred navigation stamped when the batch starts and
  re-asserted just before flying.
- Dated imports never change the shared clock: imported sources expose no
  clock, entities are available for all time, and a KML gx:Track is drawn as
  its line.
- GPX is read into GeoJSON with one line per track segment, without times.
- KML/KMZ rows show a hatched swatch (they keep their own styles); keyboard
  focus is kept on the same control after hide/show and moves to the next
  row after remove.
- Unit regressions for each, and the rendered QA now imports hostile
  metadata, dated KML/GPX and a two-segment track, and fails on any request
  for a file's contents or icon assets, a clock change, or surviving
  metadata.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 08:50:35 -05:00

91 lines
3.1 KiB
JavaScript

/**
* Fly to everything in a data source (an imported file, a live feed) without
* trusting Cesium's own framing of ground-clamped points: before the
* destination's terrain has streamed in, a clamped billboard reports a
* bounding sphere kilometers below the surface, and `viewer.flyTo` follows it
* underground. Instead the footprint is framed from the features' longitudes
* and latitudes, and the camera ground guard lifts the eye once the real
* surface can be measured.
*/
import * as Cesium from 'cesium';
import { guardCameraAboveGround } from '../cameraGroundGuard.js';
const MAX_POINTS = 5000;
/**
* Longitude/latitude pairs of every entity's position, line and polygon,
* thinned to at most `MAX_POINTS`. Long tracks are walked by index, never
* spread into a call, so a file with hundreds of thousands of vertices cannot
* overflow the stack; only the kept points are converted.
*/
export function dataSourcePoints(dataSource, time = Cesium.JulianDate.now()) {
const parts = [];
let total = 0;
for (const entity of dataSource?.entities?.values ?? []) {
const at = entity.position?.getValue?.(time);
if (at) {
parts.push([at]);
total += 1;
}
const line = entity.polyline?.positions?.getValue?.(time);
if (Array.isArray(line) && line.length) {
parts.push(line);
total += line.length;
}
const ring = entity.polygon?.hierarchy?.getValue?.(time)?.positions;
if (Array.isArray(ring) && ring.length) {
parts.push(ring);
total += ring.length;
}
}
const stride = Math.max(1, Math.ceil(total / MAX_POINTS));
const points = [];
let index = 0;
for (const part of parts) {
// Keep every stride-th vertex across all parts, plus each part's ends so
// a short line is never skipped entirely.
for (let i = 0; i < part.length; i++, index++) {
if (index % stride !== 0 && i !== 0 && i !== part.length - 1) continue;
const carto = Cesium.Cartographic.fromCartesian(part[i]);
if (!carto) continue;
points.push([
Cesium.Math.toDegrees(carto.longitude),
Cesium.Math.toDegrees(carto.latitude),
]);
}
}
return points;
}
/**
* @param {Cesium.Viewer} viewer
* @param {Cesium.DataSource} dataSource
* @param {{duration?: number}} [options]
* @returns {boolean} Whether there was anything to fly to.
*/
export function flyToDataSource(viewer, dataSource, { duration = 1.5 } = {}) {
const points = dataSourcePoints(dataSource);
if (!points.length) {
viewer.flyTo?.(dataSource, { duration });
return false;
}
const sphere = Cesium.BoundingSphere.fromPoints(
points.map(([lon, lat]) => Cesium.Cartesian3.fromDegrees(lon, lat, 0)),
);
const center = Cesium.Cartographic.fromCartesian(sphere.center);
viewer.camera?.flyToBoundingSphere?.(sphere, {
offset: new Cesium.HeadingPitchRange(
viewer.camera.heading || 0,
Cesium.Math.toRadians(-45),
Math.max(800, sphere.radius * 2.8),
),
duration,
complete: () =>
guardCameraAboveGround(viewer, {
lat: Cesium.Math.toDegrees(center.latitude),
lon: Cesium.Math.toDegrees(center.longitude),
}),
});
return true;
}