mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
perf(import): reuse generated pin textures and trim unused processing
This commit is contained in:
1 parent
c58ff5c215
commit
767a7d5ae8
6 files changed
+169
-96
No files matched your search
+4
-1
@@ -176,7 +176,10 @@
|
|||||||
are shared before the style limit is applied, allowing large files with many
|
are shared before the style limit is applied, allowing large files with many
|
||||||
copies of the same style to load without changing their graphics. Blocked
|
copies of the same style to load without changing their graphics. Blocked
|
||||||
remote icons now fall back to local pins instead of leaving point markers
|
remote icons now fall back to local pins instead of leaving point markers
|
||||||
invisible, including USGS earthquake exports.
|
invisible, including USGS earthquake exports. Generated pins share one image
|
||||||
|
in the GPU atlas; unused time/view metadata is pruned before loading, and
|
||||||
|
obsolete track resampling code is removed. KML vertex counting follows
|
||||||
|
the loader's whitespace rules.
|
||||||
|
|
||||||
## [0.2.1] — 2026-10-02 — God's Eye View through Your Agent (with security fixes and other features)
|
## [0.2.1] — 2026-10-02 — God's Eye View through Your Agent (with security fixes and other features)
|
||||||
|
|
||||||
|
|||||||
@@ -4326,10 +4326,10 @@ are omitted rather than framing the wrong part of the globe.
|
|||||||
|
|
||||||
- DISPLAY ▸ **Import** puts GeoJSON (`.geojson`/`.json`), KML, KMZ and GPX files on the globe. Files come in through the Import button (a hidden `<input type=file multiple>`) or by dropping them onto the globe; the format is taken from the extension, then from the first bytes. Each file becomes its own Cesium data source, clamped to the ground, with a row to show/hide it, fly to it, or remove it. GeoJSON and GPX are drawn in the row's color from the whiteboard palette (simplestyle properties in GeoJSON are dropped); KML and KMZ keep the styles written in them, and their row shows a hatched swatch instead of a color. GPX is read by `gpxToGeoJson` and loaded as GeoJSON: waypoints are points, each route a line, and each track one `MultiLineString` with a line per `<trkseg>`, so a pause in a recording stays a gap. Imported files last for the page session: a reload starts empty, and nothing is uploaded or stored.
|
- DISPLAY ▸ **Import** puts GeoJSON (`.geojson`/`.json`), KML, KMZ and GPX files on the globe. Files come in through the Import button (a hidden `<input type=file multiple>`) or by dropping them onto the globe; the format is taken from the extension, then from the first bytes. Each file becomes its own Cesium data source, clamped to the ground, with a row to show/hide it, fly to it, or remove it. GeoJSON and GPX are drawn in the row's color from the whiteboard palette (simplestyle properties in GeoJSON are dropped); KML and KMZ keep the styles written in them, and their row shows a hatched swatch instead of a color. GPX is read by `gpxToGeoJson` and loaded as GeoJSON: waypoints are points, each route a line, and each track one `MultiLineString` with a line per `<trkseg>`, so a pause in a recording stays a gap. Imported files last for the page session: a reload starts empty, and nothing is uploaded or stored.
|
||||||
- **Navigation.** The row's Fly-to is an immediate navigation through the application shell (`runImmediateNavigation('file', …)`): Cockpit refuses it, and the current follow/track owner is released first. The fly after an import is a deferred navigation: `beginDeferredLocationNavigation()` stamps it when the pick or drop is submitted, before queued batches wait and `reassertDeferredLocationNavigation()` re-checks it just before flying, so a camera gesture, search, Cockpit or any newer navigation while files load wins and the import does not fly. Fly-to frames the features' longitudes and latitudes at the surface (thinned to 5,000 points by index, never spread into a call) and lets the camera ground guard lift the eye once the terrain has loaded (`src/ui/flyToDataSource.js`); `viewer.flyTo` is not used, because before terrain streams in it frames ground-clamped points kilometers below the surface.
|
- **Navigation.** The row's Fly-to is an immediate navigation through the application shell (`runImmediateNavigation('file', …)`): Cockpit refuses it, and the current follow/track owner is released first. The fly after an import is a deferred navigation: `beginDeferredLocationNavigation()` stamps it when the pick or drop is submitted, before queued batches wait and `reassertDeferredLocationNavigation()` re-checks it just before flying, so a camera gesture, search, Cockpit or any newer navigation while files load wins and the import does not fly. Fly-to frames the features' longitudes and latitudes at the surface (thinned to 5,000 points by index, never spread into a call) and lets the camera ground guard lift the eye once the terrain has loaded (`src/ui/flyToDataSource.js`); `viewer.flyTo` is not used, because before terrain streams in it frames ground-clamped points kilometers below the surface.
|
||||||
- **Clock.** An import never changes the app's shared clock. The viewer adopts the clock of every data source added to it, so each imported data source reports no clock (`detachImportedTime`); every entity is available for all time, and KML `gx:Track` / `gx:MultiTrack` geometry is converted to static lines before Cesium loads it, preserving the recorded vertices and segment gaps without expanding them through time sampling. GPX times are not read at all.
|
- **Clock.** An import never changes the app's shared clock. The viewer adopts the clock of every data source added to it, so each imported data source reports no clock (`detachImportedTime`); every entity is available for all time. Unused KML `TimeSpan`/`TimeStamp` and `LookAt`/`Camera` metadata is removed before loader construction, and KML `gx:Track` / `gx:MultiTrack` geometry is converted to static lines before Cesium loads it, preserving the recorded vertices and segment gaps without expanding them through time sampling. GPX times are not read at all.
|
||||||
- Runtime entry: `createApplicationTools` (`src/app/tools.js`) calls `initGeoFileImport({ viewer, navigation: styleManager })` (`src/ui/geoFileImport.js`) after the draw tool and defers its `destroy()`, which removes every imported data source, revokes every blob URL, and gives back the drop listeners on the viewer container and the `window.__gevGeoImport` handle (`importFiles`, `list`, `setVisible`, `flyTo`, `remove`, `diagnostics`). Parsing lives in `src/data/geoFileImport.js`.
|
- Runtime entry: `createApplicationTools` (`src/app/tools.js`) calls `initGeoFileImport({ viewer, navigation: styleManager })` (`src/ui/geoFileImport.js`) after the draw tool and defers its `destroy()`, which removes every imported data source, revokes every blob URL, and gives back the drop listeners on the viewer container and the `window.__gevGeoImport` handle (`importFiles`, `list`, `setVisible`, `flyTo`, `remove`, `diagnostics`). Parsing lives in `src/data/geoFileImport.js`.
|
||||||
- **An imported file cannot make the page fetch anything.** Before `KmlDataSource` reads a KML/KMZ document, `sanitizeKmlDocument` removes `NetworkLink`, `NetworkLinkControl`, `ScreenOverlay`, `PhotoOverlay`, `Model` and `Tour`, every `href` that is not an image packed in the same KMZ or a `data:image/` URL (a blocked `IconStyle` icon definition is removed so the marker falls back to Cesium's default pin, drawn locally; an intentionally empty icon remains empty, and a `GroundOverlay` left without an image is dropped), every `styleUrl` that is not a local `#id`, and placemark `description`/`BalloonStyle`/`Snippet` HTML and `ExtendedData` before metadata processing. GeoJSON `marker-symbol` (a maki icon asset) is dropped. KMZ images are served from blob URLs, one per packed image however many hrefs name it (`kmzImageResolver`). The hint says when something was left out. `scripts/qa-geo-import.mjs` imports files that name a trap host in each of those places and fails on any request that leaves the app's own origin.
|
- **An imported file cannot make the page fetch anything.** Before `KmlDataSource` reads a KML/KMZ document, `sanitizeKmlDocument` removes `NetworkLink`, `NetworkLinkControl`, `ScreenOverlay`, `PhotoOverlay`, `Model` and `Tour`, every `href` that is not an image packed in the same KMZ or a `data:image/` URL (a blocked `IconStyle` icon definition is removed so the marker falls back to Cesium's default pin, drawn locally; an intentionally empty icon remains empty, and a `GroundOverlay` left without an image is dropped), every `styleUrl` that is not a local `#id`, and placemark `description`/`BalloonStyle`/`Snippet` HTML and `ExtendedData` before metadata processing. GeoJSON `marker-symbol` (a maki icon asset) is dropped. KMZ images are served from blob URLs, one per packed image however many hrefs name it (`kmzImageResolver`). The hint says when something was left out. `scripts/qa-geo-import.mjs` imports files that name a trap host in each of those places and fails on any request that leaves the app's own origin.
|
||||||
- **Imported metadata stays inert.** After loading, `scrubImportedEntities` removes every entity's description, native label, properties and KML extended data, and keeps the name only as plain text without control characters, at most 120 characters. GeoJSON is loaded with a `describe` that builds no HTML. The file name is shown with `textContent` only.
|
- **Imported metadata stays inert.** After loading, `scrubImportedEntities` removes every entity's description, native label, properties and KML extended data, and keeps the name only as plain text without control characters, at most 120 characters. GeoJSON is loaded with a `describe` that builds no HTML. The file name is shown with `textContent` only. Entity normalization batches collection notifications and converts constant generated pin canvases to one shared data URL per image, avoiding a duplicate GPU texture entry per marker while preserving authored image URLs and dynamic image properties.
|
||||||
- **KMZ** is read by a small zip reader (`readZipEntries`: stored and deflated entries through the platform `DecompressionStream`; no encryption, no ZIP64). The main document is `doc.kml`, else the shallowest `.kml`; relative hrefs resolve only to entries inside the archive.
|
- **KMZ** is read by a small zip reader (`readZipEntries`: stored and deflated entries through the platform `DecompressionStream`; no encryption, no ZIP64). The main document is `doc.kml`, else the shallowest `.kml`; relative hrefs resolve only to entries inside the archive.
|
||||||
- **Bounds.** A file may be at most 50 MiB (`MAX_IMPORT_BYTES`), a KMZ may inflate to at most 120 MiB in total (`MAX_KMZ_INFLATED_BYTES`, the zip-bomb stop), one file may create at most 20,000 Cesium entities, including multipart geometries and KML parent containers (`MAX_IMPORT_ENTITIES`) and 250,000 vertices (`MAX_IMPORT_POSITIONS`), and a KML/KMZ at most 2,000 normalized style definitions (including named StyleMaps) and distinct surviving image links (`MAX_KML_LINKS`). Identical KML styles are shared after metadata and remote-reference cleanup, preserving inline overrides and local style references; repeated copies do not exhaust the style budget. Folder containers count toward the entity budget. Files of 20–30 MB fit within the byte limit; drawing complexity is bounded separately. Geometry is counted before Cesium builds anything (`countGeoJsonGeometry`, `countKmlGeometry`, and while reading GPX), and the entity count is checked again after loading. At most 12 files are on the globe at once (`MAX_IMPORTED_FILES`): imports run one batch at a time, so two picks or drops in quick succession cannot pass the cap; files over it are not read. A refused file names its reason in the hint and does not stop the rest of the batch; a file with nothing to draw is refused.
|
- **Bounds.** A file may be at most 50 MiB (`MAX_IMPORT_BYTES`), a KMZ may inflate to at most 120 MiB in total (`MAX_KMZ_INFLATED_BYTES`, the zip-bomb stop), one file may create at most 20,000 Cesium entities, including multipart geometries and KML parent containers (`MAX_IMPORT_ENTITIES`) and 250,000 vertices (`MAX_IMPORT_POSITIONS`), and a KML/KMZ at most 2,000 normalized style definitions (including named StyleMaps) and distinct surviving image links (`MAX_KML_LINKS`). Identical KML styles are shared after metadata and remote-reference cleanup, preserving inline overrides and local style references; repeated copies do not exhaust the style budget. Folder containers count toward the entity budget. Files of 20–30 MB fit within the byte limit; drawing complexity is bounded separately. Geometry is counted before Cesium builds anything (`countGeoJsonGeometry`, `countKmlGeometry`, and while reading GPX), and the entity count is checked again after loading. At most 12 files are on the globe at once (`MAX_IMPORTED_FILES`): imports run one batch at a time, so two picks or drops in quick succession cannot pass the cap; files over it are not read. A refused file names its reason in the hint and does not stop the rest of the batch; a file with nothing to draw is refused.
|
||||||
- **Keyboard.** Hide, show and remove rebuild the list without losing focus: the same control on the same row keeps it, a removed row hands it to the next row (else the Import button).
|
- **Keyboard.** Hide, show and remove rebuild the list without losing focus: the same control on the same row keeps it, a removed row hands it to the next row (else the Import button).
|
||||||
|
|||||||
@@ -102,6 +102,13 @@ try {
|
|||||||
});
|
});
|
||||||
window.__qaStyleSnapshot = (source) => {
|
window.__qaStyleSnapshot = (source) => {
|
||||||
const now = viewer.clock.currentTime;
|
const now = viewer.clock.currentTime;
|
||||||
|
const images = new Map();
|
||||||
|
const imageValue = (p) => {
|
||||||
|
const value = p?.getValue(now);
|
||||||
|
if (value?.tagName !== 'CANVAS') return value;
|
||||||
|
if (!images.has(value)) images.set(value, value.toDataURL());
|
||||||
|
return images.get(value);
|
||||||
|
};
|
||||||
const color = (p) => p?.getValue(now)?.toCssColorString();
|
const color = (p) => p?.getValue(now)?.toCssColorString();
|
||||||
const positions = (p) => p?.getValue(now)?.map((v) => [v.x, v.y, v.z]);
|
const positions = (p) => p?.getValue(now)?.map((v) => [v.x, v.y, v.z]);
|
||||||
return source.entities.values
|
return source.entities.values
|
||||||
@@ -110,7 +117,7 @@ try {
|
|||||||
point: e.position?.getValue(now),
|
point: e.position?.getValue(now),
|
||||||
scale: e.billboard?.scale?.getValue(now),
|
scale: e.billboard?.scale?.getValue(now),
|
||||||
rotation: e.billboard?.rotation?.getValue(now),
|
rotation: e.billboard?.rotation?.getValue(now),
|
||||||
image: e.billboard?.image?.getValue(now),
|
image: imageValue(e.billboard?.image),
|
||||||
color: color(e.billboard?.color),
|
color: color(e.billboard?.color),
|
||||||
width: e.polyline?.width?.getValue(now),
|
width: e.polyline?.width?.getValue(now),
|
||||||
lineColor: color(e.polyline?.material?.color),
|
lineColor: color(e.polyline?.material?.color),
|
||||||
|
|||||||
+61
-73
@@ -68,14 +68,22 @@ const KMZ_IMAGE_TYPES = Object.freeze({
|
|||||||
webp: 'image/webp',
|
webp: 'image/webp',
|
||||||
bmp: 'image/bmp',
|
bmp: 'image/bmp',
|
||||||
});
|
});
|
||||||
// KML elements that fetch, embed or execute something outside the file.
|
// External content and metadata the session-local import does not use.
|
||||||
const KML_REMOVED_ELEMENTS = Object.freeze([
|
const KML_REMOVED_ELEMENTS = new Set([
|
||||||
'NetworkLink',
|
'NetworkLink',
|
||||||
'NetworkLinkControl',
|
'NetworkLinkControl',
|
||||||
'ScreenOverlay',
|
'ScreenOverlay',
|
||||||
'PhotoOverlay',
|
'PhotoOverlay',
|
||||||
'Model',
|
'Model',
|
||||||
'Tour',
|
'Tour',
|
||||||
|
'description',
|
||||||
|
'BalloonStyle',
|
||||||
|
'Snippet',
|
||||||
|
'ExtendedData',
|
||||||
|
'TimeSpan',
|
||||||
|
'TimeStamp',
|
||||||
|
'LookAt',
|
||||||
|
'Camera',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** An error whose message is meant for the person who chose the file. */
|
/** An error whose message is meant for the person who chose the file. */
|
||||||
@@ -222,7 +230,7 @@ export function countGeoJsonGeometry(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The refusal for a file over a size limit, in the person's terms. */
|
/** The refusal for a file over a size limit, in the person's terms. */
|
||||||
function overLimitError({ over, features, positions }) {
|
function overLimitError({ over, features }) {
|
||||||
if (over === 'entities')
|
if (over === 'entities')
|
||||||
return new GeoImportError(
|
return new GeoImportError(
|
||||||
`That file creates more than ${MAX_IMPORT_ENTITIES.toLocaleString()} map objects (the limit).`,
|
`That file creates more than ${MAX_IMPORT_ENTITIES.toLocaleString()} map objects (the limit).`,
|
||||||
@@ -486,8 +494,11 @@ export function sanitizeKmlDocument(doc, resolveLocal = () => null) {
|
|||||||
removed += 1;
|
removed += 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
for (const name of KML_REMOVED_ELEMENTS)
|
// One snapshot and pass, rather than rescanning the whole XML tree for
|
||||||
elementsNamed(doc, name).forEach(drop);
|
// every discarded element type. Strip unused metadata before the loader
|
||||||
|
// constructs descriptions, availability intervals and camera objects.
|
||||||
|
for (const el of [...doc.getElementsByTagName('*')])
|
||||||
|
if (KML_REMOVED_ELEMENTS.has(el.localName)) drop(el);
|
||||||
for (const href of elementsNamed(doc, 'href')) {
|
for (const href of elementsNamed(doc, 'href')) {
|
||||||
const value = (href.textContent || '').trim();
|
const value = (href.textContent || '').trim();
|
||||||
const local = resolveLocal(value);
|
const local = resolveLocal(value);
|
||||||
@@ -511,10 +522,6 @@ export function sanitizeKmlDocument(doc, resolveLocal = () => null) {
|
|||||||
for (const styleUrl of elementsNamed(doc, 'styleUrl')) {
|
for (const styleUrl of elementsNamed(doc, 'styleUrl')) {
|
||||||
if (!(styleUrl.textContent || '').trim().startsWith('#')) drop(styleUrl);
|
if (!(styleUrl.textContent || '').trim().startsWith('#')) drop(styleUrl);
|
||||||
}
|
}
|
||||||
// Placemark balloons are HTML; the app shows no info box, and keeping their
|
|
||||||
// <img>/<iframe> sources out means no future surface can load them either.
|
|
||||||
for (const name of ['description', 'BalloonStyle', 'Snippet', 'ExtendedData'])
|
|
||||||
elementsNamed(doc, name).forEach(drop);
|
|
||||||
// Draw recorded KML paths as their original static vertices. Letting the
|
// Draw recorded KML paths as their original static vertices. Letting the
|
||||||
// loader build time-dynamic tracks would expand each into thousands of
|
// loader build time-dynamic tracks would expand each into thousands of
|
||||||
// interpolated points later and exceed the checked geometry budget.
|
// interpolated points later and exceed the checked geometry budget.
|
||||||
@@ -735,7 +742,10 @@ export function countKmlGeometry(
|
|||||||
// a large string into a large array.
|
// a large string into a large array.
|
||||||
let inTuple = false;
|
let inTuple = false;
|
||||||
for (let i = 0; i < text.length; i++) {
|
for (let i = 0; i < text.length; i++) {
|
||||||
const space = text.charCodeAt(i) <= 32;
|
const code = text.charCodeAt(i);
|
||||||
|
// Match Cesium's whitespace splitting, keeping the common ASCII
|
||||||
|
// path cheap and avoiding a large token array during preflight.
|
||||||
|
const space = code <= 32 || (code > 127 && /\s/.test(text[i]));
|
||||||
if (!space && !inTuple) {
|
if (!space && !inTuple) {
|
||||||
positions += 1;
|
positions += 1;
|
||||||
if (positions > maxPositions) return result('positions');
|
if (positions > maxPositions) return result('positions');
|
||||||
@@ -854,47 +864,61 @@ export function importFeatureName(value) {
|
|||||||
/**
|
/**
|
||||||
* Make every entity of an imported data source inert: no description,
|
* Make every entity of an imported data source inert: no description,
|
||||||
* balloon, extended data or properties, no native label, and a name that is
|
* balloon, extended data or properties, no native label, and a name that is
|
||||||
* short plain text. Returns how many entities were touched.
|
* short plain text. Generated pin canvases share a data URL so Cesium uploads
|
||||||
|
* each image once, rather than allocating a texture entry for every entity.
|
||||||
|
* Returns how many entities were touched.
|
||||||
* @param {{entities: {values: object[]}}} dataSource
|
* @param {{entities: {values: object[]}}} dataSource
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
export function scrubImportedEntities(dataSource) {
|
export function scrubImportedEntities(dataSource) {
|
||||||
const list = dataSource?.entities?.values || [];
|
const collection = dataSource?.entities;
|
||||||
for (let i = 0; i < list.length; i++) {
|
const list = collection?.values || [];
|
||||||
const entity = list[i];
|
const images = new Map();
|
||||||
let name;
|
collection?.suspendEvents?.();
|
||||||
try {
|
try {
|
||||||
name = importFeatureName(
|
for (let i = 0; i < list.length; i++) {
|
||||||
typeof entity.name === 'string' ? entity.name : undefined,
|
const entity = list[i];
|
||||||
);
|
let name;
|
||||||
} catch {
|
try {
|
||||||
name = undefined;
|
name = importFeatureName(
|
||||||
|
typeof entity.name === 'string' ? entity.name : undefined,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
name = undefined;
|
||||||
|
}
|
||||||
|
entity.name = name;
|
||||||
|
entity.description = undefined;
|
||||||
|
entity.label = undefined;
|
||||||
|
entity.properties = undefined;
|
||||||
|
if (entity.kml && typeof entity.kml === 'object') entity.kml = undefined;
|
||||||
|
const property = entity.billboard?.image;
|
||||||
|
if (property?.isConstant === false) continue;
|
||||||
|
const image = property?.getValue?.();
|
||||||
|
if (image?.tagName === 'CANVAS') {
|
||||||
|
let url = images.get(image);
|
||||||
|
if (!url) {
|
||||||
|
url = image.toDataURL();
|
||||||
|
images.set(image, url);
|
||||||
|
}
|
||||||
|
entity.billboard.image = url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entity.name = name;
|
} finally {
|
||||||
entity.description = undefined;
|
collection?.resumeEvents?.();
|
||||||
entity.label = undefined;
|
|
||||||
entity.properties = undefined;
|
|
||||||
if (entity.kml && typeof entity.kml === 'object') entity.kml = undefined;
|
|
||||||
}
|
}
|
||||||
return list.length;
|
return list.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRACK_SAMPLES = 2000;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Take time out of an imported data source so adding it cannot move the app's
|
* Take time out of an imported data source so adding it cannot move the app's
|
||||||
* shared clock: the data source reports no clock (the viewer adopts the clock
|
* shared clock: the data source reports no clock (the viewer adopts the clock
|
||||||
* of every added source that has one), every entity is available for all
|
* of every added source that has one), every entity is available for all
|
||||||
* time, and a time-dynamic position (a KML gx:Track) is drawn as the line it
|
* time. KML tracks have already become their original static vertices in
|
||||||
* travels instead of a moving marker.
|
* `sanitizeKmlDocument`, so no sampled-position fallback is needed here.
|
||||||
* @param {object} dataSource
|
* @param {object} dataSource
|
||||||
* @param {{Cesium?: object, color?: object}} [options]
|
|
||||||
* @returns {object} The same data source.
|
* @returns {object} The same data source.
|
||||||
*/
|
*/
|
||||||
export function detachImportedTime(
|
export function detachImportedTime(dataSource) {
|
||||||
dataSource,
|
|
||||||
{ Cesium: C = Cesium, color } = {},
|
|
||||||
) {
|
|
||||||
Object.defineProperty(dataSource, 'clock', {
|
Object.defineProperty(dataSource, 'clock', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
enumerable: false,
|
enumerable: false,
|
||||||
@@ -904,42 +928,6 @@ export function detachImportedTime(
|
|||||||
const list = dataSource?.entities?.values || [];
|
const list = dataSource?.entities?.values || [];
|
||||||
for (let i = 0; i < list.length; i++) {
|
for (let i = 0; i < list.length; i++) {
|
||||||
const entity = list[i];
|
const entity = list[i];
|
||||||
const span = entity.availability;
|
|
||||||
const position = entity.position;
|
|
||||||
if (position && position.isConstant === false) {
|
|
||||||
const start = span?.start;
|
|
||||||
const stop = span?.stop;
|
|
||||||
const line = [];
|
|
||||||
if (start && stop && C.JulianDate.lessThan(start, stop)) {
|
|
||||||
const seconds = C.JulianDate.secondsDifference(stop, start);
|
|
||||||
for (let n = 0; n <= TRACK_SAMPLES; n++) {
|
|
||||||
const t = C.JulianDate.addSeconds(
|
|
||||||
start,
|
|
||||||
(seconds * n) / TRACK_SAMPLES,
|
|
||||||
new C.JulianDate(),
|
|
||||||
);
|
|
||||||
const at = position.getValue(t);
|
|
||||||
if (at) line.push(at);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entity.path = undefined;
|
|
||||||
entity.model = undefined;
|
|
||||||
if (line.length >= 2 && !entity.polyline) {
|
|
||||||
entity.billboard = undefined;
|
|
||||||
entity.point = undefined;
|
|
||||||
entity.position = undefined;
|
|
||||||
entity.polyline = new C.PolylineGraphics({
|
|
||||||
positions: line,
|
|
||||||
clampToGround: true,
|
|
||||||
width: 3,
|
|
||||||
material: color || C.Color.WHITE,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
entity.position = line.length
|
|
||||||
? new C.ConstantPositionProperty(line[0])
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entity.availability = undefined;
|
entity.availability = undefined;
|
||||||
}
|
}
|
||||||
return dataSource;
|
return dataSource;
|
||||||
@@ -1050,8 +1038,8 @@ export async function loadGeoFile(file, { viewer, color = '#39d0ff' }) {
|
|||||||
throw new GeoImportError('That file has nothing to draw.');
|
throw new GeoImportError('That file has nothing to draw.');
|
||||||
}
|
}
|
||||||
scrubImportedEntities(dataSource);
|
scrubImportedEntities(dataSource);
|
||||||
// Only KML has time-dynamic positions (GPX is read without times); its
|
// Keep the clock invariant explicit even though unused KML dates have
|
||||||
// track line is drawn in KML's default white, like its other unstyled lines.
|
// already been stripped and all supported geometries are static.
|
||||||
detachImportedTime(dataSource);
|
detachImportedTime(dataSource);
|
||||||
// The display name travels beside the data source (the file name, not
|
// The display name travels beside the data source (the file name, not
|
||||||
// anything the document says about itself).
|
// anything the document says about itself).
|
||||||
|
|||||||
@@ -657,6 +657,22 @@ test('KML placemarks and coordinate tuples are counted before loading', () => {
|
|||||||
assert.equal(countKmlGeometry(doc, { maxFeatures: 1 }).over, 'features');
|
assert.equal(countKmlGeometry(doc, { maxFeatures: 1 }).over, 'features');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('KML vertex counting recognizes the Unicode whitespace Cesium accepts', () => {
|
||||||
|
for (const separator of ['\u00a0', '\u2003', '\ufeff']) {
|
||||||
|
const doc = xmlDoc(
|
||||||
|
xmlEl('kml', {}, [
|
||||||
|
xmlEl('Placemark', {}, [
|
||||||
|
xmlEl('LineString', {}, [
|
||||||
|
xmlEl('coordinates', {}, ['1,2', '3,4', '5,6'].join(separator)),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
assert.equal(countKmlGeometry(doc).positions, 3);
|
||||||
|
assert.equal(countKmlGeometry(doc, { maxPositions: 2 }).over, 'positions');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('KML multipart geometries count every Cesium entity before loading', () => {
|
test('KML multipart geometries count every Cesium entity before loading', () => {
|
||||||
const doc = xmlDoc(
|
const doc = xmlDoc(
|
||||||
xmlEl('kml', {}, [
|
xmlEl('kml', {}, [
|
||||||
@@ -786,6 +802,67 @@ test('imported entities keep only a short plain-text name: no description, label
|
|||||||
assert.equal(importFeatureName(' '), undefined);
|
assert.equal(importFeatureName(' '), undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('generated pin canvases are encoded once and shared without changing authored image URLs', () => {
|
||||||
|
const ds = new Cesium.CustomDataSource('pins');
|
||||||
|
let encoded = 0;
|
||||||
|
const canvas = {
|
||||||
|
tagName: 'CANVAS',
|
||||||
|
toDataURL: () => {
|
||||||
|
encoded++;
|
||||||
|
return 'data:image/png;base64,shared-pin';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
for (let i = 0; i < 1000; i++)
|
||||||
|
ds.entities.add({
|
||||||
|
billboard: { image: canvas },
|
||||||
|
label: { text: 'discard' },
|
||||||
|
properties: { discard: true },
|
||||||
|
});
|
||||||
|
const authored = ds.entities.add({
|
||||||
|
billboard: { image: 'blob:packed-image' },
|
||||||
|
});
|
||||||
|
const dynamicImage = new Cesium.CallbackProperty(() => canvas, false);
|
||||||
|
const dynamic = ds.entities.add({ billboard: { image: dynamicImage } });
|
||||||
|
let notifications = 0;
|
||||||
|
ds.entities.collectionChanged.addEventListener(() => notifications++);
|
||||||
|
scrubImportedEntities(ds);
|
||||||
|
assert.equal(encoded, 1);
|
||||||
|
assert.equal(
|
||||||
|
notifications,
|
||||||
|
1,
|
||||||
|
'normalization publishes one complete collection change',
|
||||||
|
);
|
||||||
|
for (const e of ds.entities.values.slice(0, 1000))
|
||||||
|
assert.equal(
|
||||||
|
e.billboard.image.getValue(),
|
||||||
|
'data:image/png;base64,shared-pin',
|
||||||
|
);
|
||||||
|
assert.equal(authored.billboard.image.getValue(), 'blob:packed-image');
|
||||||
|
assert.equal(
|
||||||
|
dynamic.billboard.image,
|
||||||
|
dynamicImage,
|
||||||
|
'time-dependent image properties stay intact',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unused KML time and camera metadata is removed before loading without changing geometry', () => {
|
||||||
|
const coordinates = xmlEl('coordinates', {}, '1,2,0 3,4,0');
|
||||||
|
const placemark = xmlEl('Placemark', {}, [
|
||||||
|
xmlEl('TimeSpan'),
|
||||||
|
xmlEl('TimeStamp'),
|
||||||
|
xmlEl('LookAt'),
|
||||||
|
xmlEl('Camera'),
|
||||||
|
xmlEl('LineString', {}, [coordinates]),
|
||||||
|
]);
|
||||||
|
const doc = xmlDoc(xmlEl('kml', {}, [placemark]));
|
||||||
|
sanitizeKmlDocument(doc);
|
||||||
|
assert.deepEqual(
|
||||||
|
placemark.childNodes.map((el) => el.localName),
|
||||||
|
['LineString'],
|
||||||
|
);
|
||||||
|
assert.equal(coordinates.textContent, '1,2,0 3,4,0');
|
||||||
|
});
|
||||||
|
|
||||||
test('a dated import exposes no clock and draws for all time', () => {
|
test('a dated import exposes no clock and draws for all time', () => {
|
||||||
// A data source with its own clock, as KmlDataSource has for a dated file.
|
// A data source with its own clock, as KmlDataSource has for a dated file.
|
||||||
class DatedSource extends Cesium.CustomDataSource {
|
class DatedSource extends Cesium.CustomDataSource {
|
||||||
@@ -800,15 +877,15 @@ test('a dated import exposes no clock and draws for all time', () => {
|
|||||||
const span = new Cesium.TimeIntervalCollection([
|
const span = new Cesium.TimeIntervalCollection([
|
||||||
new Cesium.TimeInterval({ start, stop }),
|
new Cesium.TimeInterval({ start, stop }),
|
||||||
]);
|
]);
|
||||||
const track = new Cesium.SampledPositionProperty();
|
// KML tracks become their original static vertices before Cesium loading.
|
||||||
track.addSample(start, Cesium.Cartesian3.fromDegrees(10, 10));
|
const track = ds.entities.add({
|
||||||
track.addSample(stop, Cesium.Cartesian3.fromDegrees(10.1, 10.1));
|
|
||||||
const mover = ds.entities.add({
|
|
||||||
availability: span,
|
availability: span,
|
||||||
position: track,
|
polyline: {
|
||||||
path: {},
|
positions: Cesium.Cartesian3.fromDegreesArray([10, 10, 10.1, 10.1]),
|
||||||
billboard: {},
|
clampToGround: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
const recorded = track.polyline.positions;
|
||||||
const pin = ds.entities.add({
|
const pin = ds.entities.add({
|
||||||
availability: span,
|
availability: span,
|
||||||
position: Cesium.Cartesian3.fromDegrees(1, 1),
|
position: Cesium.Cartesian3.fromDegrees(1, 1),
|
||||||
@@ -821,12 +898,8 @@ test('a dated import exposes no clock and draws for all time', () => {
|
|||||||
assert.equal(ds.clock, undefined);
|
assert.equal(ds.clock, undefined);
|
||||||
assert.equal(pin.availability, undefined);
|
assert.equal(pin.availability, undefined);
|
||||||
assert.ok(pin.isAvailable(Cesium.JulianDate.now()));
|
assert.ok(pin.isAvailable(Cesium.JulianDate.now()));
|
||||||
// The moving marker became the line it travels, visible now.
|
assert.equal(track.availability, undefined);
|
||||||
assert.equal(mover.availability, undefined);
|
assert.equal(track.polyline.positions, recorded);
|
||||||
assert.equal(mover.position, undefined);
|
assert.equal(recorded.getValue(Cesium.JulianDate.now()).length, 2);
|
||||||
assert.equal(mover.path, undefined);
|
assert.ok(track.polyline.clampToGround.getValue());
|
||||||
assert.equal(mover.billboard, undefined);
|
|
||||||
const line = mover.polyline.positions.getValue(Cesium.JulianDate.now());
|
|
||||||
assert.ok(line.length > 100);
|
|
||||||
assert.ok(mover.polyline.clampToGround.getValue());
|
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Fly to everything in a data source (an imported file, a live feed) without
|
* Fly to everything in an imported file without
|
||||||
* trusting Cesium's own framing of ground-clamped points: before the
|
* trusting Cesium's own framing of ground-clamped points: before the
|
||||||
* destination's terrain has streamed in, a clamped billboard reports a
|
* destination's terrain has streamed in, a clamped billboard reports a
|
||||||
* bounding sphere kilometers below the surface, and `viewer.flyTo` follows it
|
* bounding sphere kilometers below the surface, and `viewer.flyTo` follows it
|
||||||
@@ -10,11 +10,13 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { guardCameraAboveGround } from '../cameraGroundGuard.js';
|
import { guardCameraAboveGround } from '../cameraGroundGuard.js';
|
||||||
|
|
||||||
const MAX_POINTS = 5000;
|
const SAMPLE_TARGET_POINTS = 5000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Longitude/latitude pairs of every entity's position, line and polygon,
|
* Longitude/latitude pairs of every entity's position, line and polygon,
|
||||||
* thinned to at most `MAX_POINTS`. Long tracks are walked by index, never
|
* sampling long paths toward `SAMPLE_TARGET_POINTS` while retaining each
|
||||||
|
* geometry's endpoints. Separate point features stay in the footprint so
|
||||||
|
* distant outliers are not lost. Long tracks are walked by index, never
|
||||||
* spread into a call, so a file with hundreds of thousands of vertices cannot
|
* spread into a call, so a file with hundreds of thousands of vertices cannot
|
||||||
* overflow the stack; only the kept points are converted.
|
* overflow the stack; only the kept points are converted.
|
||||||
*/
|
*/
|
||||||
@@ -38,7 +40,7 @@ export function dataSourcePoints(dataSource, time = Cesium.JulianDate.now()) {
|
|||||||
total += ring.length;
|
total += ring.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const stride = Math.max(1, Math.ceil(total / MAX_POINTS));
|
const stride = Math.max(1, Math.ceil(total / SAMPLE_TARGET_POINTS));
|
||||||
const points = [];
|
const points = [];
|
||||||
let index = 0;
|
let index = 0;
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
|
|||||||
Reference in new issue
Block a user