import test from 'node:test';
import assert from 'node:assert/strict';
import { deflateRawSync } from 'node:zlib';
import { spawnSync } from 'node:child_process';
import * as Cesium from 'cesium';
import {
GeoImportError,
repairKmlRootNamespaces,
loadGeoFile,
MAX_IMPORT_POSITIONS,
MAX_IMPORT_ENTITIES,
countGeoJsonGeometry,
countKmlGeometry,
deduplicateKmlStyles,
detachImportedTime,
gpxToGeoJson,
importFeatureName,
scrubImportedEntities,
sanitizeKmlDocument,
detectGeoFileFormat,
importDisplayName,
kmzImageResolver,
kmzImageType,
kmzMainDocument,
parseGeoJsonText,
readZipEntries,
resolveKmzPath,
} from './geoFileImport.js';
import { createImportImageBudget, importImageSize } from './geoImportImages.js';
test('real loader grounds XYZ GeoJSON and strips loader-sensitive metadata before allocation', async () => {
const previous = Cesium.PinBuilder.prototype.fromColor;
Cesium.PinBuilder.prototype.fromColor = () => ({
toDataURL: () => 'data:image/png;base64,local',
tagName: 'CANVAS',
});
try {
const object = {
type: 'FeatureCollection',
crs: { type: 'name', properties: { name: 'unsupported' } },
features: [
{
type: 'Feature',
id: 'kartaview-owned',
properties: { name: 'Test', addProperty: 1, 'marker-symbol': 'rail' },
geometry: { type: 'Point', coordinates: [1, 2, 999] },
},
{
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [
[
[1, 2, 0],
[2, 2, 0],
[2, 3, 0],
[1, 2, 0],
],
],
},
},
],
};
const loaded = await loadGeoFile(
new File([JSON.stringify(object)], 'xyz.geojson'),
{
viewer: {
scene: {
frameState: { mode: Cesium.SceneMode.SCENE3D },
primitives: new Cesium.PrimitiveCollection(),
globe: { getHeight: () => 1234 },
},
},
},
);
const [point, polygon] =
loaded.dataSource.importedEntities || loaded.dataSource.entities.values;
assert.notEqual(point.id, 'kartaview-owned');
assert.equal(point.name, 'Test');
assert.equal(point.properties, undefined);
assert.equal(
point.billboard.heightReference.getValue(),
Cesium.HeightReference.NONE,
);
assert.equal(point.billboard.disableDepthTestDistance.getValue(), Infinity);
assert.ok(
Math.abs(
Cesium.Cartographic.fromCartesian(
point.position.getValue(Cesium.JulianDate.now()),
).height - 1234,
) < 1e-6,
);
assert.notEqual(polygon.polygon.perPositionHeight?.getValue(), true);
assert.equal(loaded.dataSource.clock, undefined);
assert.equal(
loaded.dataSource.entities.values.length,
1,
'only polygon geometry remains in the per-frame entity collection',
);
assert.equal(loaded.dataSource.importedBillboards.length, 1);
const update = Cesium.BillboardCollection.prototype.update;
let passes = 0;
Cesium.BillboardCollection.prototype.update = () => passes++;
try {
const collection = loaded.dataSource.importedBillboards;
collection.update({ passes: { pick: true, offscreen: true } });
assert.equal(passes, 0, 'height picks ignore imported markers');
collection.update({ passes: { pick: true, offscreen: false } });
collection.update({ passes: { render: true, offscreen: true } });
assert.equal(passes, 2, 'ordinary picks and screenshots include markers');
} finally {
Cesium.BillboardCollection.prototype.update = update;
}
loaded.mount();
loaded.dataSource.show = false;
assert.equal(loaded.dataSource.importedBillboards.show, false);
loaded.dataSource.show = true;
await loaded.refreshGround();
assert.equal(
point.billboard.heightReference.getValue(),
Cesium.HeightReference.NONE,
);
loaded.revoke();
assert.ok(loaded.dataSource.importedBillboards.isDestroyed());
} finally {
Cesium.PinBuilder.prototype.fromColor = previous;
}
});
test('real loader refuses empty files and malformed nested geometry before allocating entities', async () => {
for (const object of [
{ type: 'Feature', geometry: null },
{
type: 'FeatureCollection',
features: [{ geometry: { type: 'Point', coordinates: [1, 2] } }],
},
{ type: 'Feature', geometry: { type: 'Topology', arcs: [] } },
...[
[1, '2'],
[Infinity, 0],
[0, 91],
[0, 0, 1e100],
].map((coordinates) => ({ type: 'Point', coordinates })),
])
await assert.rejects(
loadGeoFile(new File([JSON.stringify(object)], 'bad.geojson'), {
viewer: {},
}),
GeoImportError,
);
});
test('KML tracks retain their explicit altitude and ground lines are tessellated', () => {
const track = xmlEl('Track', {}, [
xmlEl('altitudeMode', {}, 'absolute'),
xmlEl('coord', {}, '1 2 10000'),
xmlEl('coord', {}, '2 3 10000'),
]);
const line = xmlEl('LineString', {}, [xmlEl('coordinates', {}, '1,2 2,3')]);
const ground = xmlEl('Track', {}, [
xmlEl('coord', {}, '1 2 0'),
xmlEl('coord', {}, '2 3 0'),
]);
const root = xmlEl('kml', {}, [track, line, ground]);
sanitizeKmlDocument(xmlDoc(root));
assert.equal(
root.childNodes[0].childNodes.find((n) => n.localName === 'altitudeMode')
.textContent,
'absolute',
);
assert.equal(
root.childNodes[1].childNodes.find((n) => n.localName === 'tessellate')
.textContent,
'1',
);
assert.equal(
root.childNodes[2].childNodes.find((n) => n.localName === 'tessellate')
.textContent,
'1',
);
});
test('sanitizer normalizes styles, removes incomplete pairs and keeps colored overlays', () => {
const local = xmlEl('styleUrl', {}, ' #s ');
const pair = xmlEl('Pair', {}, [
xmlEl('key', {}, 'normal'),
xmlEl('styleUrl', {}, 'https://example.invalid/s'),
]);
const overlay = xmlEl('GroundOverlay', {}, [xmlEl('color', {}, 'ff00ff00')]);
const hidden = xmlEl('visibility', {}, '0');
const id = xmlEl('Placemark', { id: 'kartaview-123' });
const root = xmlEl('kml', {}, [
local,
xmlEl('StyleMap', { id: 's' }, [pair]),
overlay,
hidden,
id,
xmlEl('toString'),
]);
sanitizeKmlDocument(xmlDoc(root));
assert.equal(local.textContent, '#s');
assert.equal(pair.parentNode, null);
assert.ok(root.childNodes.includes(overlay));
assert.equal(hidden.parentNode, null);
assert.equal(id.getAttribute('id'), null);
assert.ok(!root.childNodes.some((n) => n.localName === 'toString'));
});
test('image budget rejects large decoded rasters before decoding and shares repeated images', () => {
const png = (w, h) => {
const b = new Uint8Array(24);
const v = new DataView(b.buffer);
v.setUint32(0, 0x89504e47);
b.set([73, 72, 68, 82], 12);
v.setUint32(16, w);
v.setUint32(20, h);
return b;
};
assert.deepEqual(importImageSize(png(4096, 4096)), [4096, 4096]);
const charge = createImportImageBudget();
charge('shared', png(4096, 4096));
charge('shared', png(4096, 4096));
assert.throws(() => charge('other', png(1, 1)), /limit/);
assert.throws(
() => createImportImageBudget()('huge', png(16384, 16384)),
/limit/,
);
});
test('pending file reads can be cancelled without waiting for the loader timeout', async () => {
const controller = new AbortController();
const file = {
name: 'waiting.geojson',
size: 1,
arrayBuffer: () => new Promise(() => {}),
};
const promise = loadGeoFile(file, { viewer: {}, signal: controller.signal });
controller.abort(new Error('cancelled by teardown'));
await assert.rejects(promise, /cancelled by teardown/);
});
test('a hidden coarse globe never seeds a mesh-clamp ray below the surface', async () => {
const previous = Cesium.PinBuilder.prototype.fromColor;
Cesium.PinBuilder.prototype.fromColor = () => ({
tagName: 'CANVAS',
toDataURL: () => 'data:image/png;base64,local',
});
const starts = [];
try {
for (const terrainProvider of [new Cesium.EllipsoidTerrainProvider(), {}]) {
const viewer = {
terrainProvider,
camera: {
positionCartographic: { height: 100000 },
computeViewRectangle: () => Cesium.Rectangle.MAX_VALUE,
},
scene: {
frameState: { mode: Cesium.SceneMode.SCENE3D },
globe: { show: false, getHeight: () => -12311 },
clampToHeightSupported: true,
clampToHeightMostDetailed: async (points) =>
points.map((p) => {
const c = Cesium.Cartographic.fromCartesian(p);
starts.push(c.height);
return c.height < -9000
? undefined
: Cesium.Cartesian3.fromRadians(c.longitude, c.latitude, 2000);
}),
},
};
const loaded = await loadGeoFile(
new File(
[JSON.stringify({ type: 'Point', coordinates: [1, 2] })],
'surface.geojson',
),
{ viewer },
);
assert.ok(
Math.abs(starts.at(-1)) < 1e-6,
`ray started at ${starts.at(-1)}`,
);
assert.ok(
Math.abs(
Cesium.Cartographic.fromCartesian(
loaded.dataSource.importedBillboards.get(0).position,
).height - 2000,
) < 1e-6,
);
const sampled = starts.length;
viewer.camera.computeViewRectangle = () => undefined;
await loaded.refreshGround();
assert.equal(
starts.length,
sampled,
'distant points do not start a detailed mesh sweep',
);
assert.ok(
Math.abs(
Cesium.Cartographic.fromCartesian(
loaded.dataSource.importedBillboards.get(0).position,
).height - 2000,
) < 1e-6,
'the last valid mesh height survives outside the camera footprint',
);
const point = loaded.dataSource.importedEntities[0];
const other = new Cesium.Entity({ id: 'unrelated-aircraft' });
viewer.trackedEntity = starts.length === 1 ? other : point;
viewer.selectedEntity = point;
loaded.revoke();
assert.equal(viewer.selectedEntity, undefined);
assert.equal(
viewer.trackedEntity,
starts.length === 1 ? other : undefined,
);
}
} finally {
Cesium.PinBuilder.prototype.fromColor = previous;
}
});
const bytes = (text) => new TextEncoder().encode(text);
/** A minimal zip writer: stored (0) or deflated (8) entries, optional flags. */
function zip(files) {
const locals = [];
const centrals = [];
let offset = 0;
for (const { name, data, method = 8, flags = 0, size } of files) {
const nameBytes = bytes(name);
const body = method === 8 ? new Uint8Array(deflateRawSync(data)) : data;
const local = new Uint8Array(30 + nameBytes.length);
const lv = new DataView(local.buffer);
lv.setUint32(0, 0x04034b50, true);
lv.setUint16(6, flags, true);
lv.setUint16(8, method, true);
lv.setUint32(18, body.length, true);
lv.setUint32(22, size ?? data.length, true);
lv.setUint16(26, nameBytes.length, true);
local.set(nameBytes, 30);
const central = new Uint8Array(46 + nameBytes.length);
const cv = new DataView(central.buffer);
cv.setUint32(0, 0x02014b50, true);
cv.setUint16(8, flags, true);
cv.setUint16(10, method, true);
cv.setUint32(20, body.length, true);
cv.setUint32(24, size ?? data.length, true);
cv.setUint16(28, nameBytes.length, true);
cv.setUint32(42, offset, true);
central.set(nameBytes, 46);
locals.push(local, body);
centrals.push(central);
offset += local.length + body.length;
}
const dirSize = centrals.reduce((n, c) => n + c.length, 0);
const eocd = new Uint8Array(22);
const ev = new DataView(eocd.buffer);
ev.setUint32(0, 0x06054b50, true);
ev.setUint16(8, files.length, true);
ev.setUint16(10, files.length, true);
ev.setUint32(12, dirSize, true);
ev.setUint32(16, offset, true);
const parts = [...locals, ...centrals, eocd];
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
let at = 0;
for (const p of parts) {
out.set(p, at);
at += p.length;
}
return out;
}
test('format comes from the extension, then from the first bytes', () => {
assert.equal(detectGeoFileFormat('trail.GPX'), 'gpx');
assert.equal(detectGeoFileFormat('farm.kml'), 'kml');
assert.equal(detectGeoFileFormat('farm.kmz'), 'kmz');
assert.equal(detectGeoFileFormat('parcels.geojson'), 'geojson');
assert.equal(detectGeoFileFormat('parcels.json'), 'geojson');
assert.equal(
detectGeoFileFormat('export', bytes(' {"type":"Feature"}')),
'geojson',
);
assert.equal(
detectGeoFileFormat(
'export',
bytes(''),
),
'kml',
);
assert.equal(
detectGeoFileFormat(
'export',
bytes('\n'),
),
'gpx',
);
assert.equal(
detectGeoFileFormat('export', new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0])),
'kmz',
);
assert.equal(detectGeoFileFormat('notes.txt', bytes('hello')), null);
assert.equal(
detectGeoFileFormat('image.png', new Uint8Array([0x89, 0x50, 0x4e, 0x47])),
null,
);
});
test('display names are printable and bounded', () => {
assert.equal(importDisplayName(' hike\u0000.gpx '), 'hike.gpx');
assert.equal(importDisplayName(''), 'Imported file');
const long = importDisplayName(`${'x'.repeat(80)}.kml`);
assert.equal(long.length, 58);
assert.ok(long.endsWith('…'));
});
test('GeoJSON text must be a GeoJSON object', () => {
assert.equal(
parseGeoJsonText('{"type":"FeatureCollection","features":[]}').type,
'FeatureCollection',
);
assert.equal(
parseGeoJsonText('{"type":"Point","coordinates":[0,0]}').type,
'Point',
);
assert.throws(() => parseGeoJsonText('{nope'), GeoImportError);
assert.throws(() => parseGeoJsonText('{"name":"not geo"}'), /not GeoJSON/);
assert.throws(() => parseGeoJsonText('[1,2]'), /not GeoJSON/);
assert.throws(
() => parseGeoJsonText('{"type":"FeatureCollection"}'),
/no features list/,
);
});
test('a KMZ is read with stored and deflated entries, directories skipped', async () => {
const kml = bytes('');
const png = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 1, 2, 3]);
const archive = zip([
{ name: 'files/', data: new Uint8Array(0), method: 0 },
{ name: 'doc.kml', data: kml },
{ name: 'files/icon.png', data: png, method: 0 },
]);
const entries = await readZipEntries(archive);
assert.deepEqual([...entries.keys()], ['doc.kml', 'files/icon.png']);
assert.deepEqual(entries.get('doc.kml'), kml);
assert.deepEqual(entries.get('files/icon.png'), png);
});
test('a KMZ that inflates past the limit is refused', async () => {
const archive = zip([{ name: 'doc.kml', data: new Uint8Array(200_000) }]);
await assert.rejects(
readZipEntries(archive, { maxInflatedBytes: 100_000 }),
/more than the import limit/,
);
});
test('encrypted, damaged and non-zip archives are refused with a reason', async () => {
await assert.rejects(
readZipEntries(
zip([{ name: 'doc.kml', data: bytes(''), flags: 1, method: 0 }]),
),
/encrypted/,
);
await assert.rejects(
readZipEntries(bytes('not a zip at all, just text')),
/not a readable zip/,
);
const truncated = zip([
{ name: 'doc.kml', data: bytes(''), method: 0 },
]);
const view = new DataView(truncated.buffer);
view.setUint32(truncated.length - 6, 9999, true); // directory offset past the end
await assert.rejects(readZipEntries(truncated), GeoImportError);
await assert.rejects(
readZipEntries(
zip([{ name: 'doc.kml', data: bytes(''), method: 12 }]),
),
/compression method/,
);
});
test('the main KML is doc.kml, else the shallowest .kml', () => {
const e = (...names) => new Map(names.map((n) => [n, new Uint8Array(0)]));
assert.equal(kmzMainDocument(e('a/b.kml', 'doc.kml')), 'doc.kml');
assert.equal(
kmzMainDocument(e('deep/x/z.kml', 'top.kml', 'images/a.png')),
'top.kml',
);
assert.equal(kmzMainDocument(e('images/a.png')), null);
});
test('KMZ hrefs resolve only to entries inside the archive', () => {
const entries = new Map([
['doc.kml', new Uint8Array(0)],
['files/icon one.png', new Uint8Array(0)],
['overlay.jpg', new Uint8Array(0)],
]);
assert.equal(
resolveKmzPath('files/icon%20one.png', 'doc.kml', entries),
'files/icon one.png',
);
assert.equal(
resolveKmzPath('./overlay.jpg', 'doc.kml', entries),
'overlay.jpg',
);
assert.equal(
resolveKmzPath('../overlay.jpg', 'files/sub.kml', entries),
'overlay.jpg',
);
assert.equal(
resolveKmzPath('https://example.com/overlay.jpg', 'doc.kml', entries),
null,
);
assert.equal(
resolveKmzPath('//example.com/overlay.jpg', 'doc.kml', entries),
null,
);
assert.equal(resolveKmzPath('file:///etc/passwd', 'doc.kml', entries), null);
assert.equal(resolveKmzPath('missing.png', 'doc.kml', entries), null);
assert.equal(resolveKmzPath('%E0%A4%A', 'doc.kml', entries), null);
assert.equal(kmzImageType('files/a.PNG'), 'image/png');
assert.equal(kmzImageType('files/a.svg'), null);
});
/* ── a tiny element tree with the DOM surface the readers use ─────────── */
function xmlEl(localName, attrs = {}, kids = []) {
const el = {
nodeType: 1,
localName,
namespaceURI: 'http://www.opengis.net/kml/2.2',
childNodes: [],
getAttribute: (n) => (n in attrs ? String(attrs[n]) : null),
get attributes() {
return Object.entries(attrs);
},
setAttribute: (n, value) => {
attrs[n] = value;
},
removeAttribute: (n) => {
delete attrs[n];
},
cloneNode() {
return xmlEl(
localName,
{ ...attrs },
text ?? el.childNodes.map((child) => child.cloneNode(true)),
);
},
getElementsByTagName(name) {
return xmlDoc(this).getElementsByTagName(name).slice(1);
},
appendChild(child) {
child.parentNode?.removeChild(child);
child.parentNode = this;
this.childNodes.push(child);
},
replaceChild(next, previous) {
const index = this.childNodes.indexOf(previous);
assert.ok(index >= 0);
next.parentNode?.removeChild(next);
this.childNodes[index] = next;
next.parentNode = this;
previous.parentNode = null;
},
removeChild(child) {
this.childNodes.splice(this.childNodes.indexOf(child), 1);
child.parentNode = null;
},
};
for (const k of typeof kids === 'string' ? [] : kids) {
k.parentNode = el;
el.childNodes.push(k);
}
let text = typeof kids === 'string' ? kids : null;
Object.defineProperty(el, 'firstChild', {
get: () => el.childNodes[0] || null,
});
Object.defineProperty(el, 'textContent', {
get: () => text ?? el.childNodes.map((c) => c.textContent).join(''),
set: (value) => {
text = value;
el.childNodes = [];
},
});
return el;
}
const xmlDoc = (root) => ({
documentElement: root,
createElementNS: (_ns, name) => xmlEl(name),
getElementsByTagName(name) {
assert.equal(name, '*');
const out = [];
const walk = (el) => {
out.push(el);
el.childNodes.forEach(walk);
};
walk(root);
return out;
},
});
const serializeStyle = (el) =>
JSON.stringify({
name: el.localName,
attributes: el.attributes,
text: el.childNodes.length ? null : el.textContent,
children: el.childNodes.map(serializeStyle),
});
test('repeated inline styles are shared without replacing their inherited styleUrl', () => {
const style = () =>
xmlEl('Style', {}, [xmlEl('IconStyle', {}, [xmlEl('scale', {}, '0.6')])]);
const placemarks = Array.from({ length: 2100 }, () =>
xmlEl('Placemark', {}, [xmlEl('styleUrl', {}, '#base'), style()]),
);
const doc = xmlDoc(
xmlEl('kml', {}, [
xmlEl('Document', {}, [
xmlEl('Style', { id: 'gev-import-style-1' }, [xmlEl('LineStyle')]),
...placemarks,
]),
]),
);
assert.equal(countKmlGeometry(doc).over, 'links');
assert.equal(deduplicateKmlStyles(doc, { serializeStyle }), 2100);
assert.equal(countKmlGeometry(doc).over, '');
const styles = doc
.getElementsByTagName('*')
.filter((el) => el.localName === 'Style');
assert.equal(styles.length, 2);
assert.equal(styles[1].getAttribute('id'), 'gev-import-style-2');
for (const placemark of placemarks) {
assert.equal(placemark.childNodes[0].textContent, '#base');
assert.equal(placemark.childNodes[1].localName, 'StyleMap');
assert.deepEqual(
placemark.childNodes[1].childNodes.map((pair) =>
pair.childNodes.map((el) => el.textContent),
),
[
['normal', '#gev-import-style-2'],
['highlight', '#gev-import-style-2'],
],
);
}
});
test('identical named styles redirect existing references and Pair styles remain valid', () => {
const style = (id) =>
xmlEl('Style', id ? { id } : {}, [
xmlEl('LineStyle', {}, [xmlEl('width', {}, '3')]),
]);
const reference = xmlEl('styleUrl', {}, '#b');
const pair = xmlEl('Pair', {}, [xmlEl('key', {}, 'normal'), style()]);
const doc = xmlDoc(
xmlEl('kml', {}, [
style('a'),
style('b'),
xmlEl('StyleMap', { id: 'map' }, [pair]),
xmlEl('Placemark', {}, [reference]),
]),
);
assert.equal(deduplicateKmlStyles(doc, { serializeStyle }), 3);
assert.equal(doc.documentElement.firstChild.localName, 'Document');
assert.equal(reference.textContent, '#gev-import-style-1');
assert.equal(pair.childNodes[1].localName, 'styleUrl');
assert.equal(pair.childNodes[1].textContent, '#gev-import-style-1');
assert.equal(
doc.getElementsByTagName('*').filter((el) => el.localName === 'Style')
.length,
1,
);
});
test('random colors and ambiguous style IDs retain independent evaluations and precedence', () => {
const random = () =>
xmlEl('Style', {}, [
xmlEl('IconStyle', {}, [xmlEl('colorMode', {}, 'random')]),
]);
const named = (width) =>
xmlEl('Style', { id: 'collision' }, [
xmlEl('LineStyle', {}, [xmlEl('width', {}, width)]),
]);
const original = [random(), random(), named('1'), named('1'), named('2')];
const doc = xmlDoc(xmlEl('kml', {}, original));
assert.equal(deduplicateKmlStyles(doc, { serializeStyle }), 0);
assert.deepEqual(doc.documentElement.childNodes, original);
});
test('distinct style definitions and named StyleMaps still have a pre-loader budget', () => {
const doc = xmlDoc(
xmlEl(
'kml',
{},
['1', '2', '3'].map((width) =>
xmlEl('Style', {}, [
xmlEl('LineStyle', {}, [xmlEl('width', {}, width)]),
]),
),
),
);
assert.throws(
() => deduplicateKmlStyles(doc, { serializeStyle, maxStyles: 2 }),
GeoImportError,
);
const maps = xmlDoc(
xmlEl('kml', {}, [
xmlEl('StyleMap', { id: 'a' }),
xmlEl('StyleMap', { id: 'b' }),
]),
);
assert.equal(countKmlGeometry(maps, { maxLinks: 1 }).over, 'links');
});
test('KML extended metadata is removed before the document reaches the loader', () => {
const point = xmlEl('Point', {}, [xmlEl('coordinates', {}, '1,2,0')]);
const metadata = xmlEl('ExtendedData', {}, [
xmlEl('Data', { name: 'note' }, [xmlEl('value', {}, 'ordinary metadata')]),
]);
const placemark = xmlEl('Placemark', {}, [metadata, point]);
const doc = xmlDoc(xmlEl('kml', {}, [placemark]));
assert.equal(sanitizeKmlDocument(doc).removed, 1);
assert.deepEqual(placemark.childNodes, [point]);
assert.equal(metadata.parentNode, null);
});
test('blocked icon sources fall back to local pins without changing intentional empty icons', () => {
const blocked = xmlEl('Icon', {}, [
xmlEl('href', {}, 'https://example.invalid/icon.png'),
]);
const blank = xmlEl('Icon');
const embedded = xmlEl('Icon', {}, [
xmlEl('href', {}, 'data:image/png;base64,AAAA'),
]);
const packed = xmlEl('Icon', {}, [xmlEl('href', {}, 'packed.png')]);
const iconStyle = xmlEl('IconStyle', {}, [
xmlEl('scale', {}, '0.3'),
blocked,
]);
const doc = xmlDoc(
xmlEl('kml', {}, [
xmlEl('Style', {}, [iconStyle]),
...[blank, embedded, packed].map((icon) =>
xmlEl('Style', {}, [xmlEl('IconStyle', {}, [icon])]),
),
]),
);
assert.equal(
sanitizeKmlDocument(doc, (href) =>
href === 'packed.png' ? 'blob:local-image' : null,
).removed,
1,
);
assert.equal(blocked.parentNode, null);
assert.equal(iconStyle.childNodes[0].textContent, '0.3');
assert.equal(blank.parentNode.localName, 'IconStyle');
assert.equal(
embedded.childNodes[0].textContent,
'data:image/png;base64,AAAA',
);
assert.equal(packed.childNodes[0].textContent, 'blob:local-image');
});
const trkpt = (lat, lon, time) =>
xmlEl('trkpt', { lat, lon }, time ? [xmlEl('time', {}, time)] : []);
test('GPX keeps each track segment as its own line, names as text, no times', () => {
const doc = xmlDoc(
xmlEl('gpx', {}, [
xmlEl('wpt', { lat: 37.77, lon: -122.49 }, [xmlEl('name', {}, 'Camp')]),
xmlEl('wpt', { lat: 99, lon: 0 }), // out of range: skipped
xmlEl('rte', {}, [
xmlEl('name', {}, 'Route'),
xmlEl('rtept', { lat: 1, lon: 2 }),
xmlEl('rtept', { lat: 3, lon: 4 }),
]),
xmlEl('trk', {}, [
xmlEl('name', {}, 'Morning walk'),
xmlEl('trkseg', {}, [
trkpt(10, 20, '2024-05-01T10:00:00Z'),
trkpt(10.1, 20.1, '2024-05-01T10:01:00Z'),
]),
// A pause in the recording: a new segment, not a straight jump.
xmlEl('trkseg', {}, [
trkpt(11, 21, '2024-05-01T11:00:00Z'),
trkpt(11.1, 21.1, '2024-05-01T11:01:00Z'),
trkpt(11.2, 21.2, '2024-05-01T11:02:00Z'),
]),
xmlEl('trkseg', {}, [trkpt(12, 22)]), // one point: not a line
]),
]),
);
const fc = gpxToGeoJson(doc);
assert.deepEqual(
fc.features.map((f) => [f.properties.name, f.geometry.type]),
[
['Camp', 'Point'],
['Route', 'LineString'],
['Morning walk', 'MultiLineString'],
],
);
assert.deepEqual(fc.features[2].geometry.coordinates, [
[
[20, 10],
[20.1, 10.1],
],
[
[21, 11],
[21.1, 11.1],
[21.2, 11.2],
],
]);
assert.doesNotMatch(JSON.stringify(fc), /2024|time/);
assert.throws(() => gpxToGeoJson(xmlDoc(xmlEl('kml'))), GeoImportError);
});
test('GPX past the point limit is refused while reading, before Cesium', () => {
const seg = xmlEl(
'trkseg',
{},
Array.from({ length: 30 }, (_, i) => trkpt(1 + i / 100, 2)),
);
const doc = xmlDoc(xmlEl('gpx', {}, [xmlEl('trk', {}, [seg])]));
assert.throws(
() => gpxToGeoJson(doc, { maxPositions: 20 }),
(e) => e instanceof GeoImportError && /more than .* points/.test(e.message),
);
assert.throws(
() =>
gpxToGeoJson(
xmlDoc(
xmlEl('gpx', {}, [
xmlEl('wpt', { lat: 1, lon: 1 }),
xmlEl('wpt', { lat: 2, lon: 2 }),
]),
),
{ maxFeatures: 1 },
),
/more than .* features/,
);
});
test('GeoJSON features and vertices are counted before loading, without recursion', () => {
const line = Array.from({ length: 50 }, (_, i) => [i / 10, 1]);
const fc = {
type: 'FeatureCollection',
features: [
{ type: 'Feature', geometry: { type: 'LineString', coordinates: line } },
{
type: 'Feature',
geometry: {
type: 'GeometryCollection',
geometries: [
{ type: 'Point', coordinates: [1, 2] },
{
type: 'Polygon',
coordinates: [
[
[0, 0],
[1, 0],
[1, 1],
[0, 0],
],
],
},
],
},
},
{ type: 'Feature', geometry: null },
],
};
assert.deepEqual(countGeoJsonGeometry(fc), {
features: 3,
positions: 55,
entities: 4,
over: '',
});
assert.equal(
countGeoJsonGeometry(fc, { maxPositions: 54 }).over,
'positions',
);
assert.equal(countGeoJsonGeometry(fc, { maxFeatures: 2 }).over, 'features');
assert.deepEqual(
countGeoJsonGeometry({ type: 'Point', coordinates: [1, 2] }),
{
features: 1,
positions: 1,
entities: 1,
over: '',
},
);
// A very long line is counted iteratively and stops at the limit.
const huge = {
type: 'LineString',
coordinates: Array.from({ length: MAX_IMPORT_POSITIONS + 5 }, () => [0, 0]),
};
assert.equal(countGeoJsonGeometry(huge).over, 'positions');
});
test('multipart GeoJSON is refused before loading when it expands past the entity cap', () => {
const parts = Array.from({ length: MAX_IMPORT_ENTITIES + 1 }, () => [1, 2]);
for (const geometry of [
{ type: 'MultiPoint', coordinates: parts },
{
type: 'GeometryCollection',
geometries: parts.map((coordinates) => ({ type: 'Point', coordinates })),
},
{ type: 'MultiLineString', coordinates: parts.map((p) => [p, [2, 3]]) },
]) {
const size = countGeoJsonGeometry({ type: 'Feature', geometry });
assert.equal(size.over, 'entities');
}
});
test('GPX points with missing or blank coordinates are skipped rather than placed at zero', () => {
const doc = xmlDoc(
xmlEl('gpx', {}, [
xmlEl('wpt', { lon: 2 }),
xmlEl('wpt', { lat: 1 }),
xmlEl('wpt', { lat: '', lon: 2 }),
xmlEl('wpt', { lat: 1, lon: ' ' }),
xmlEl('wpt', { lat: 0, lon: 0 }),
]),
);
assert.equal(gpxToGeoJson(doc).features.length, 1);
assert.deepEqual(gpxToGeoJson(doc).features[0].geometry.coordinates, [0, 0]);
});
test('KML placemarks and coordinate tuples are counted before loading', () => {
const coords = (n) =>
xmlEl(
'coordinates',
{},
Array.from({ length: n }, (_, i) => `${i},1,0`).join('\n '),
);
const doc = xmlDoc(
xmlEl('kml', {}, [
xmlEl('Placemark', {}, [xmlEl('LineString', {}, [coords(40)])]),
xmlEl('Placemark', {}, [
xmlEl('Track', {}, [
xmlEl('coord', {}, '1 2 0'),
xmlEl('coord', {}, '1 3 0'),
]),
]),
]),
);
assert.deepEqual(countKmlGeometry(doc), {
features: 2,
entities: 2,
positions: 42,
links: 0,
over: '',
});
assert.equal(countKmlGeometry(doc, { maxPositions: 41 }).over, 'positions');
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', () => {
const doc = xmlDoc(
xmlEl('kml', {}, [
xmlEl('Placemark', {}, [
xmlEl('MultiGeometry', {}, [
xmlEl('Point'),
xmlEl('MultiGeometry', {}, [xmlEl('Point'), xmlEl('LineString')]),
xmlEl('MultiTrack', {}, [xmlEl('Track'), xmlEl('Track')]),
]),
]),
xmlEl('GroundOverlay'),
]),
);
assert.equal(countKmlGeometry(doc).entities, 9);
assert.equal(countKmlGeometry(doc, { maxEntities: 8 }).over, 'entities');
const oversized = xmlDoc(
xmlEl('kml', {}, [
xmlEl('Placemark', {}, [
xmlEl(
'MultiGeometry',
{},
Array.from({ length: MAX_IMPORT_ENTITIES }, () => xmlEl('Point')),
),
]),
]),
);
assert.equal(countKmlGeometry(oversized).over, 'entities');
});
test('KML folders contribute to the pre-loader entity budget', () => {
const doc = xmlDoc(
xmlEl('kml', {}, [
xmlEl('Folder', {}, [xmlEl('Folder', {}, [xmlEl('Placemark')])]),
]),
);
assert.equal(countKmlGeometry(doc).entities, 3);
assert.equal(countKmlGeometry(doc, { maxEntities: 2 }).over, 'entities');
});
test('KML style definitions and distinct icon links have their own limit', () => {
const style = () =>
xmlEl('Style', {}, [
xmlEl('IconStyle', {}, [xmlEl('Icon', {}, [xmlEl('href', {}, 'a.png')])]),
]);
const doc = xmlDoc(xmlEl('kml', {}, [style(), style(), style()]));
assert.equal(countKmlGeometry(doc).links, 4);
assert.equal(countKmlGeometry(doc, { maxLinks: 3 }).over, 'links');
const distinct = xmlDoc(
xmlEl(
'kml',
{},
['a.png', 'b.png', 'c.png'].map((href) => xmlEl('href', {}, href)),
),
);
assert.equal(countKmlGeometry(distinct, { maxLinks: 2 }).over, 'links');
});
test('a KMZ image gets one blob URL however many hrefs name it', () => {
const entries = new Map([
['doc.kml', new Uint8Array(0)],
['a.png', new Uint8Array(8)],
['b.png', new Uint8Array(8)],
]);
const made = [];
const kept = [];
const resolve = kmzImageResolver(
entries,
'doc.kml',
(url) => kept.push(url),
(blob) => {
made.push(blob);
return `blob:test/${made.length}`;
},
);
for (let i = 0; i < 1000; i++) assert.equal(resolve('a.png'), 'blob:test/1');
assert.equal(resolve('./b.png'), 'blob:test/2');
assert.equal(resolve('a.png'), 'blob:test/1');
assert.equal(resolve('https://example.com/a.png'), null);
assert.equal(resolve('doc.kml'), null);
assert.equal(made.length, 2);
assert.deepEqual(kept, ['blob:test/1', 'blob:test/2']);
});
test('imported entities keep only a short plain-text name: no description, label or properties', () => {
const ds = new Cesium.CustomDataSource('t');
const e = ds.entities.add({
name: 'Trail\u0007 head\n' + 'x'.repeat(300),
description: '
',
label: { text: 'Trail head' },
properties: { note: '' },
position: Cesium.Cartesian3.fromDegrees(1, 2),
});
e.kml = { extendedData: { note: { value: ''));
assert.match(
repairKmlRootNamespaces(''),
/XMLSchema-instance"\/>$/,
);
for (const malformed of ['")}.repeat(100000) + ']>';
if (repairKmlRootNamespaces(wide).split('xmlns:gx="a"').length !== 2) process.exit(1);`;
const child = spawnSync(
process.execPath,
['--input-type=module', '-e', source],
{ timeout: 5000, encoding: 'utf8' },
);
assert.ifError(child.error);
assert.equal(child.status, 0, child.stderr);
});
test('KML root repair ignores incomplete tag text inside preamble comments', () => {
const input =
'\n';
assert.equal(
repairKmlRootNamespaces(input),
input.replace(' xmlns:gx="b"', ''),
);
});
test('KML namespace repair follows a doctype with a quoted root-like entity', () => {
const input =
'">]>';
assert.equal(
repairKmlRootNamespaces(input),
input.replace(' xmlns:gx="b"', ''),
);
});
test('KML namespace repair skips doctype comments and processing instructions whole', () => {
for (const prolog of [
"]>",
' here -->]>',
" fine?>]>",
'">]>',
]) {
const input = `${prolog}`;
assert.equal(
repairKmlRootNamespaces(input),
input.replace(
' xmlns:gx="b">',
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">',
),
prolog,
);
}
const unterminated = '';
assert.equal(repairKmlRootNamespaces(unterminated), unterminated);
});
test('KML namespace repair ignores declaration text inside quoted attribute values', () => {
const input =
' b xmlns:gx="fake"\' xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:gx="dup">';
const result = repairKmlRootNamespaces(input);
assert.equal(result, input.replace(' xmlns:gx="dup"', ''));
assert.match(result, /\sxmlns:gx="http:\/\/www.google.com\/kml\/ext\/2.2"/);
const xsi = '';
assert.match(
repairKmlRootNamespaces(xsi),
/note="xmlns:xsi='x'" xmlns:xsi="http:\/\/www.w3.org\/2001\/XMLSchema-instance">/,
);
});