mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Plan KML and KMZ drawing in one pure function, checked against Cesium
src/data/geoImportPlan.js decides what an imported file draws and with what: raised or clamped geometry, rings and holes, rotation near the poles and across 360 degrees, absolute and relative altitudes, draw order, canvas budgets and the vertex and ring limits, with no engine and no DOM. drawImportedFile draws that plan through the globe contract and keeps pin placement, yielding and cancellation. Every limit of the import lives in src/data/geoImportLimits.js with what it protects. scripts/fixtures/geo-import holds 13 files covering those cases, and reference.json what Cesium's own KML and GeoJSON data sources make of them; scripts/qa-geo-import-reference.mjs records it and compares the plan with it. Property tests on seeded random polygons check area, bounds, ring closure and winding. Writing the plan fixed three cases: overlays below the ellipsoid kept a negative height, raised plain-colour overlays now draw, and the vertex limit no longer depends on whether a ring repeats its first point.
This commit is contained in:
1 parent
9c2a2d6951
commit
9ae7dea918
32 files changed
+103263
-458
No files matched your search
@@ -0,0 +1,45 @@
|
||||
# Import reference corpus
|
||||
|
||||
`reference.json` records Cesium 1.138.0 DataSources in Node with a DOM shim,
|
||||
canvas pins stubbed and KMZ images resolved locally. It is independent of
|
||||
`planImport` and checks entity geometry and styles, without rendering pixels.
|
||||
|
||||
Record and compare offline from the repository root; no server or browser is
|
||||
needed. Recording uses the installed Cesium version, saved in the reference:
|
||||
|
||||
```sh
|
||||
node scripts/qa-geo-import-reference.mjs --record
|
||||
node scripts/qa-geo-import-reference.mjs
|
||||
```
|
||||
|
||||
Recording uses Cesium's `KmlDataSource` and `GeoJsonDataSource` after the import's
|
||||
sanitization and GeoJSON grounding policy. Files are read from this directory;
|
||||
packed images use local blob URLs. Comparison uses 0.01 metres for heights,
|
||||
1e-7 degrees for coordinates and one 8-bit channel (1/255) for colours.
|
||||
Repeated ring endpoints and ignored short holes are normalized; input winding
|
||||
is retained. Image overlay order is compared separately, including ties in
|
||||
document order. Pixel composition and ordering between primitive families
|
||||
still need browser acceptance.
|
||||
|
||||
| File | Cases |
|
||||
| -------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `absolute.kml` | Absolute pins, lines and polygons; positive and negative image heights; raised rotated colour overlay |
|
||||
| `relative.kml` | Relative pins, lines, polygons and image overlay |
|
||||
| `clamped.kml` | Clamped heights and tessellation |
|
||||
| `rotations.kml` | Zero, positive, negative and full turns; a tinted 360-degree span |
|
||||
| `pole.kml` | Rotation near and past a pole; polar polygon |
|
||||
| `antimeridian.kml` | Dateline polygon and rotated image |
|
||||
| `holes.kml` | Ring closure and opposite windings with holes |
|
||||
| `short-holes.kml` | Short and duplicate-corner holes |
|
||||
| `draw-order.kml` | Negative, positive and equal image orders; plain colour |
|
||||
| `styles.kml` | KML colours, line width, icon scale, heading and hot spot |
|
||||
| `geometries.geojson` | Grounded point, line and polygon |
|
||||
| `large-polygon.kml` | A raised polygon with 20,000 vertices |
|
||||
| `packed.kmz` | An icon and a tinted, rotated overlay using a packed PNG |
|
||||
|
||||
The corpus checks negative absolute image heights and raised plain-colour
|
||||
overlays against Cesium. Short holes are skipped on direct record input without
|
||||
rejecting the whole ground polygon. Raised fills have a 32,768-vertex ceiling
|
||||
and raised polygons a 128-ring ceiling in `src/data/geoImportLimits.js`;
|
||||
a repeated closing vertex does not consume triangulation budget. The large
|
||||
polygon fixture exercises a 20,000-vertex fill within that budget.
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Document>
|
||||
<Placemark><name>absolute-pin</name><Point><altitudeMode>absolute</altitudeMode><coordinates>10,20,350</coordinates></Point></Placemark>
|
||||
<Placemark><name>absolute-line-0</name><LineString><altitudeMode>absolute</altitudeMode><tessellate>0</tessellate><coordinates>10,20,150 11,20,300</coordinates></LineString></Placemark>
|
||||
<Placemark><name>absolute-line-1</name><LineString><altitudeMode>absolute</altitudeMode><tessellate>1</tessellate><coordinates>10,20,150 11,20,300</coordinates></LineString></Placemark>
|
||||
<Placemark><name>absolute-polygon</name><Polygon><altitudeMode>absolute</altitudeMode><outerBoundaryIs><LinearRing><coordinates>0,0,100 2,0,100 2,2,100 0,2,100 0,0,100</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>0.5,0.5,100 0.5,1.5,100 1.5,1.5,100 1.5,0.5,100 0.5,0.5,100</coordinates></LinearRing></innerBoundaryIs></Polygon></Placemark>
|
||||
<GroundOverlay><name>absolute-image</name><drawOrder>0</drawOrder><color>ffffffff</color><altitudeMode>absolute</altitudeMode><altitude>200</altitude><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>10</west><south>20</south><east>12</east><north>22</north><rotation>0</rotation></LatLonBox></GroundOverlay>
|
||||
<GroundOverlay><name>negative-image</name><drawOrder>0</drawOrder><color>ffffffff</color><altitudeMode>absolute</altitudeMode><altitude>-100</altitude><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>10</west><south>20</south><east>12</east><north>22</north><rotation>0</rotation></LatLonBox></GroundOverlay>
|
||||
<GroundOverlay><name>raised-color</name><drawOrder>0</drawOrder><color>ffffffff</color><altitudeMode>absolute</altitudeMode><altitude>200</altitude><LatLonBox><west>10</west><south>20</south><east>12</east><north>22</north><rotation>30</rotation></LatLonBox></GroundOverlay>
|
||||
|
||||
</Document></kml>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Document>
|
||||
<GroundOverlay><name>crossing-image</name><drawOrder>0</drawOrder><color>ffffffff</color><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>179</west><south>-2</south><east>-179</east><north>2</north><rotation>30</rotation></LatLonBox></GroundOverlay>
|
||||
<Placemark><name>crossing-ring</name><Polygon><altitudeMode>absolute</altitudeMode><outerBoundaryIs><LinearRing><coordinates>179,-1,100 -179,-1,100 -179,1,100 179,1,100</coordinates></LinearRing></outerBoundaryIs></Polygon></Placemark>
|
||||
|
||||
</Document></kml>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Document>
|
||||
<Placemark><name>clamped-pin</name><Point><altitudeMode>clampToGround</altitudeMode><coordinates>10,20,350</coordinates></Point></Placemark>
|
||||
<Placemark><name>clamped-line-0</name><LineString><altitudeMode>clampToGround</altitudeMode><tessellate>0</tessellate><coordinates>10,20,150 11,20,300</coordinates></LineString></Placemark>
|
||||
<Placemark><name>clamped-line-1</name><LineString><altitudeMode>clampToGround</altitudeMode><tessellate>1</tessellate><coordinates>10,20,150 11,20,300</coordinates></LineString></Placemark>
|
||||
<Placemark><name>clamped-polygon</name><Polygon><altitudeMode>clampToGround</altitudeMode><outerBoundaryIs><LinearRing><coordinates>0,0,100 2,0,100 2,2,100 0,2,100 0,0,100</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>0.5,0.5,100 0.5,1.5,100 1.5,1.5,100 1.5,0.5,100 0.5,0.5,100</coordinates></LinearRing></innerBoundaryIs></Polygon></Placemark>
|
||||
<GroundOverlay><name>clamped-image</name><drawOrder>0</drawOrder><color>ffffffff</color><altitudeMode>clampToGround</altitudeMode><altitude>200</altitude><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>10</west><south>20</south><east>12</east><north>22</north><rotation>0</rotation></LatLonBox></GroundOverlay>
|
||||
|
||||
</Document></kml>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Document>
|
||||
<GroundOverlay><name>top</name><drawOrder>9</drawOrder><color>804020ff</color><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>-1</west><south>-1</south><east>1</east><north>1</north><rotation>0</rotation></LatLonBox></GroundOverlay>
|
||||
<GroundOverlay><name>first</name><drawOrder>-2</drawOrder><color>ff00ff00</color><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>-1</west><south>-1</south><east>1</east><north>1</north><rotation>0</rotation></LatLonBox></GroundOverlay>
|
||||
<GroundOverlay><name>equal-a</name><drawOrder>3</drawOrder><color>ff0000ff</color><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>-1</west><south>-1</south><east>1</east><north>1</north><rotation>0</rotation></LatLonBox></GroundOverlay>
|
||||
<GroundOverlay><name>equal-b</name><drawOrder>3</drawOrder><color>ffff0000</color><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>-1</west><south>-1</south><east>1</east><north>1</north><rotation>0</rotation></LatLonBox></GroundOverlay>
|
||||
<GroundOverlay><name>plain-color</name><drawOrder>0</drawOrder><color>804020ff</color><LatLonBox><west>-1</west><south>-1</south><east>1</east><north>1</north><rotation>25</rotation></LatLonBox></GroundOverlay>
|
||||
|
||||
</Document></kml>
|
||||
@@ -0,0 +1 @@
|
||||
{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"name":"json-point"},"geometry":{"type":"Point","coordinates":[10,20,99]}},{"type":"Feature","properties":{"name":"json-line"},"geometry":{"type":"LineString","coordinates":[[10,20,4],[11,21,8]]}},{"type":"Feature","properties":{"name":"json-polygon"},"geometry":{"type":"Polygon","coordinates":[[[0,0,100],[2,0,100],[2,2,100],[0,2,100],[0,0,100]],[[0.5,0.5,100],[0.5,1.5,100],[1.5,1.5,100],[1.5,0.5,100],[0.5,0.5,100]]]}}]}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Document>
|
||||
<Placemark><name>open</name><Polygon><altitudeMode>absolute</altitudeMode><outerBoundaryIs><LinearRing><coordinates>0,0,100 2,0,100 2,2,100 0,2,100</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>0.5,0.5,100 0.5,1.5,100 1.5,1.5,100 1.5,0.5,100</coordinates></LinearRing></innerBoundaryIs></Polygon></Placemark>
|
||||
<Placemark><name>closed</name><Polygon><altitudeMode>absolute</altitudeMode><outerBoundaryIs><LinearRing><coordinates>0,0,100 2,0,100 2,2,100 0,2,100 0,0,100</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>0.5,0.5,100 0.5,1.5,100 1.5,1.5,100 1.5,0.5,100 0.5,0.5,100</coordinates></LinearRing></innerBoundaryIs></Polygon></Placemark>
|
||||
<Placemark><name>reversed</name><Polygon><altitudeMode>absolute</altitudeMode><outerBoundaryIs><LinearRing><coordinates>0,0,100 0,2,100 2,2,100 2,0,100 0,0,100</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>0.5,0.5,100 1.5,0.5,100 1.5,1.5,100 0.5,1.5,100 0.5,0.5,100</coordinates></LinearRing></innerBoundaryIs></Polygon></Placemark>
|
||||
<Placemark><name>ground-holes</name><Polygon><altitudeMode>clampToGround</altitudeMode><outerBoundaryIs><LinearRing><coordinates>0,0,100 2,0,100 2,2,100 0,2,100 0,0,100</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>0.5,0.5,100 0.5,1.5,100 1.5,1.5,100 1.5,0.5,100 0.5,0.5,100</coordinates></LinearRing></innerBoundaryIs></Polygon></Placemark>
|
||||
|
||||
</Document></kml>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Document>
|
||||
<Placemark><name>large</name><Polygon><altitudeMode>absolute</altitudeMode><outerBoundaryIs><LinearRing><coordinates>10.1,20.0,500 10.099999995,20.000031416,500 10.09999998,20.000062832,500 10.099999956,20.000094248,500 10.099999921,20.000125664,500 10.099999877,20.00015708,500 10.099999822,20.000188495,500 10.099999758,20.000219911,500 10.099999684,20.000251327,500 10.0999996,20.000282743,500 10.099999507,20.000314159,500 10.099999403,20.000345575,500 10.099999289,20.00037699,500 10.099999166,20.000408406,500 10.099999033,20.000439822,500 10.09999889,20.000471237,500 10.099998737,20.000502653,500 10.099998574,20.000534068,500 10.099998401,20.000565484,500 10.099998219,20.000596899,500 10.099998026,20.000628314,500 10.099997824,20.00065973,500 10.099997612,20.000691145,500 10.09999739,20.00072256,500 10.099997158,20.000753975,500 10.099996916,20.00078539,500 10.099996664,20.000816805,500 10.099996403,20.00084822,500 10.099996131,20.000879635,500 10.09999585,20.000911049,500 10.099995559,20.000942464,500 10.099995258,20.000973878,500 10.099994947,20.001005293,500 10.099994626,20.001036707,500 10.099994295,20.001068121,500 10.099993955,20.001099535,500 10.099993605,20.001130949,500 10.099993244,20.001162363,500 10.099992874,20.001193777,500 10.099992494,20.00122519,500 10.099992104,20.001256604,500 10.099991705,20.001288017,500 10.099991295,20.001319431,500 10.099990876,20.001350844,500 10.099990446,20.001382257,500 10.099990007,20.00141367,500 10.099989558,20.001445082,500 10.099989099,20.001476495,500 10.09998863,20.001507907,500 10.099988152,20.00153932,500 10.099987663,20.001570732,500 10.099987165,20.001602144,500 10.099986657,20.001633556,500 10.099986138,20.001664967,500 10.09998561,20.001696379,500 10.099985073,20.00172779,500 10.099984525,20.001759201,500 10.099983967,20.001790612,500 10.0999834,20.001822023,500 10.099982822,20.001853434,500 10.099982235,20.001884844,500 10.099981638,20.001916254,500 10.099981031,20.001947664,500 10.099980414,20.001979074,500 10.099979788,20.002010484,500 10.099979151,20.002041893,500 10.099978505,20.002073303,500 10.099977848,20.002104712,500 10.099977182,20.002136121,500 10.099976506,20.002167529,500 10.09997582,20.002198938,500 10.099975125,20.002230346,500 10.099974419,20.002261754,500 10.099973704,20.002293162,500 10.099972978,20.002324569,500 10.099972243,20.002355976,500 10.099971498,20.002387384,500 10.099970743,20.00241879,500 10.099969978,20.002450197,500 10.099969203,20.002481603,500 10.099968419,20.00251301,500 10.099967625,20.002544415,500 10.09996682,20.002575821,500 10.099966006,20.002607226,500 10.099965182,20.002638632,500 10.099964348,20.002670036,500 10.099963504,20.002701441,500 10.099962651,20.002732845,500 10.099961787,20.002764249,500 10.099960914,20.002795653,500 10.099960031,20.002827057,500 10.099959138,20.00285846,500 10.099958235,20.002889863,500 10.099957322,20.002921266,500 10.099956399,20.002952668,500 10.099955467,20.00298407,500 10.099954524,20.003015472,500 10.099953572,20.003046873,500 10.09995261,20.003078274,500 10.099951638,20.003109675,500 10.099950656,20.003141076,500 10.099949664,20.003172476,500 10.099948663,20.003203876,500 10.099947651,20.003235276,500 10.09994663,20.003266675,500 10.099945599,20.003298074,500 10.099944558,20.003329473,500 10.099943507,20.003360871,500 10.099942446,20.003392269,500 10.099941375,20.003423667,500 10.099940295,20.003455064,500 10.099939204,20.003486461,500 10.099938104,20.003517858,500 10.099936994,20.003549254,500 10.099935874,20.00358065,500 10.099934744,20.003612046,500 10.099933605,20.003643441,500 10.099932455,20.003674836,500 10.099931296,20.00370623,500 10.099930126,20.003737624,500 10.099928947,20.003769018,500 10.099927758,20.003800412,500 10.099926559,20.003831805,500 10.099925351,20.003863197,500 10.099924132,20.00389459,500 10.099922904,20.003925982,500 10.099921665,20.003957373,500 10.099920417,20.003988764,500 10.099919159,20.004020155,500 10.099917891,20.004051545,500 10.099916613,20.004082935,500 10.099915326,20.004114325,500 10.099914028,20.004145714,500 10.099912721,20.004177103,500 10.099911404,20.004208491,500 10.099910077,20.004239879,500 10.09990874,20.004271266,500 10.099907393,20.004302653,500 10.099906036,20.00433404,500 10.09990467,20.004365426,500 10.099903293,20.004396812,500 10.099901907,20.004428197,500 10.099900511,20.004459582,500 10.099899105,20.004490967,500 10.099897689,20.004522351,500 10.099896264,20.004553734,500 10.099894828,20.004585117,500 10.099893383,20.0046165,500 10.099891928,20.004647882,500 10.099890462,20.004679264,500 10.099888987,20.004710645,500 10.099887503,20.004742026,500 10.099886008,20.004773406,500 10.099884503,20.004804786,500 10.099882989,20.004836166,500 10.099881465,20.004867544,500 10.099879931,20.004898923,500 10.099878387,20.004930301,500 10.099876833,20.004961678,500 10.099875269,20.004993055,500 10.099873696,20.005024432,500 10.099872112,20.005055808,500 10.099870519,20.005087183,500 10.099868916,20.005118558,500 10.099867303,2Line truncated
|
||||
|
||||
</Document></kml>
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Document>
|
||||
<GroundOverlay><name>near-pole</name><drawOrder>0</drawOrder><color>ffffffff</color><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>-2</west><south>84</south><east>2</east><north>86</north><rotation>30</rotation></LatLonBox></GroundOverlay>
|
||||
<GroundOverlay><name>past-pole</name><drawOrder>0</drawOrder><color>ffffffff</color><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>-10</west><south>86</south><east>10</east><north>89</north><rotation>45</rotation></LatLonBox></GroundOverlay>
|
||||
<Placemark><name>polar-ring</name><Polygon><altitudeMode>absolute</altitudeMode><outerBoundaryIs><LinearRing><coordinates>10,88,200 14,88,200 14,88.2,200 10,88.2,200</coordinates></LinearRing></outerBoundaryIs></Polygon></Placemark>
|
||||
|
||||
</Document></kml>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Document>
|
||||
<Placemark><name>relative-pin</name><Point><altitudeMode>relativeToGround</altitudeMode><coordinates>10,20,350</coordinates></Point></Placemark>
|
||||
<Placemark><name>relative-line-0</name><LineString><altitudeMode>relativeToGround</altitudeMode><tessellate>0</tessellate><coordinates>10,20,150 11,20,300</coordinates></LineString></Placemark>
|
||||
<Placemark><name>relative-line-1</name><LineString><altitudeMode>relativeToGround</altitudeMode><tessellate>1</tessellate><coordinates>10,20,150 11,20,300</coordinates></LineString></Placemark>
|
||||
<Placemark><name>relative-polygon</name><Polygon><altitudeMode>relativeToGround</altitudeMode><outerBoundaryIs><LinearRing><coordinates>0,0,100 2,0,100 2,2,100 0,2,100 0,0,100</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>0.5,0.5,100 0.5,1.5,100 1.5,1.5,100 1.5,0.5,100 0.5,0.5,100</coordinates></LinearRing></innerBoundaryIs></Polygon></Placemark>
|
||||
<GroundOverlay><name>relative-image</name><drawOrder>0</drawOrder><color>ffffffff</color><altitudeMode>relativeToGround</altitudeMode><altitude>200</altitude><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>10</west><south>20</south><east>12</east><north>22</north><rotation>0</rotation></LatLonBox></GroundOverlay>
|
||||
|
||||
</Document></kml>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Document>
|
||||
<GroundOverlay><name>rotation-0</name><drawOrder>0</drawOrder><color>ffffffff</color><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>-1</west><south>-1</south><east>1</east><north>1</north><rotation>0</rotation></LatLonBox></GroundOverlay>
|
||||
<GroundOverlay><name>rotation-45</name><drawOrder>0</drawOrder><color>ffffffff</color><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>-1</west><south>-1</south><east>1</east><north>1</north><rotation>45</rotation></LatLonBox></GroundOverlay>
|
||||
<GroundOverlay><name>rotation--30</name><drawOrder>0</drawOrder><color>ffffffff</color><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>-1</west><south>-1</south><east>1</east><north>1</north><rotation>-30</rotation></LatLonBox></GroundOverlay>
|
||||
<GroundOverlay><name>rotation-360</name><drawOrder>0</drawOrder><color>ffffffff</color><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>-1</west><south>-1</south><east>1</east><north>1</north><rotation>360</rotation></LatLonBox></GroundOverlay>
|
||||
<GroundOverlay><name>rotation--360</name><drawOrder>0</drawOrder><color>ffffffff</color><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>-1</west><south>-1</south><east>1</east><north>1</north><rotation>-360</rotation></LatLonBox></GroundOverlay>
|
||||
<GroundOverlay><name>all-longitudes</name><drawOrder>0</drawOrder><color>ff80ffff</color><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><LatLonBox><west>-180</west><south>-1</south><east>180</east><north>1</north><rotation>0</rotation></LatLonBox></GroundOverlay>
|
||||
|
||||
</Document></kml>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Document>
|
||||
<Placemark><name>two-point-hole</name><Polygon><altitudeMode>absolute</altitudeMode><outerBoundaryIs><LinearRing><coordinates>0,0,100 2,0,100 2,2,100 0,2,100 0,0,100</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>0.4,0.4,100 0.8,0.4,100</coordinates></LinearRing></innerBoundaryIs></Polygon></Placemark>
|
||||
<Placemark><name>repeated-hole</name><Polygon><altitudeMode>absolute</altitudeMode><outerBoundaryIs><LinearRing><coordinates>0,0,100 2,0,100 2,2,100 0,2,100 0,0,100</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing><coordinates>0.4,0.4,100 0.8,0.4,100 0.4,0.4,100</coordinates></LinearRing></innerBoundaryIs></Polygon></Placemark>
|
||||
|
||||
</Document></kml>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"><Document>
|
||||
<Style id="shared"><LineStyle><color>804020ff</color><width>5</width></LineStyle><PolyStyle><color>7f00ffff</color><outline>1</outline></PolyStyle><IconStyle><color>cc804020</color><scale>1.5</scale><heading>45</heading><Icon><href>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==</href></Icon><hotSpot x="0.25" y="0.75" xunits="fraction" yunits="fraction"/></IconStyle></Style><StyleMap id="mapped"><Pair><key>normal</key><styleUrl>#shared</styleUrl></Pair></StyleMap><Placemark><name>styled-polygon</name><styleUrl>#mapped</styleUrl><Polygon><altitudeMode>absolute</altitudeMode><outerBoundaryIs><LinearRing><coordinates>0,0,100 2,0,100 2,2,100 0,2,100 0,0,100</coordinates></LinearRing></outerBoundaryIs></Polygon></Placemark>
|
||||
<Placemark><name>styled-pin</name><styleUrl>#mapped</styleUrl><Point><coordinates>10,20</coordinates></Point></Placemark><Placemark><name>styled-line</name><styleUrl>#shared</styleUrl><LineString><coordinates>10,20 11,21</coordinates></LineString></Placemark>
|
||||
</Document></kml>
|
||||
@@ -3590,7 +3590,9 @@
|
||||
"src/data/imagingSensors.js",
|
||||
"src/data/planespottersPhoto.js",
|
||||
"src/globe/cesium/orbital.js",
|
||||
"src/globe/cesium/worldTime.js"
|
||||
"src/globe/cesium/worldTime.js",
|
||||
"src/data/geoImportLimits.js",
|
||||
"src/data/geoImportPlan.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
@@ -4302,7 +4304,8 @@
|
||||
"src/layers/satellites/sensors.js",
|
||||
"src/data/ucsSatellites.js",
|
||||
"src/data/imagingSensors.js",
|
||||
"src/data/planespottersPhoto.js"
|
||||
"src/data/planespottersPhoto.js",
|
||||
"src/data/geoImportLimits.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
@@ -5254,7 +5257,8 @@
|
||||
"src/data/geoImportImages.js",
|
||||
"src/data/geoImportRecords.js",
|
||||
"src/data/geoImportKml.js",
|
||||
"src/overlays/bindOverlayHost.js"
|
||||
"src/overlays/bindOverlayHost.js",
|
||||
"src/data/geoImportLimits.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
#!/usr/bin/env node
|
||||
// Record offline: node scripts/qa-geo-import-reference.mjs --record
|
||||
// Compare offline: node scripts/qa-geo-import-reference.mjs
|
||||
// Positions: 1 cm height and 1e-7 degrees; colours: one 8-bit channel.
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { planImport } from '../src/data/geoImportPlan.js';
|
||||
import * as geo from '../src/geo/index.js';
|
||||
import { iconBox } from '../src/data/geoImportPins.js';
|
||||
|
||||
const directory = fileURLToPath(
|
||||
new URL('./fixtures/geo-import/', import.meta.url),
|
||||
);
|
||||
const referencePath = path.join(directory, 'reference.json');
|
||||
const files = (await fs.readdir(directory))
|
||||
.filter((name) => /\.(kml|kmz|geojson)$/.test(name))
|
||||
.sort();
|
||||
const tolerances = { degrees: 1e-7, metres: 0.01, color: 1 / 255 };
|
||||
const openRing = (ring) => {
|
||||
const last = ring.at(-1);
|
||||
return ring.length > 1 &&
|
||||
Math.abs(ring[0][0] - last[0]) < 1e-7 &&
|
||||
Math.abs(ring[0][1] - last[1]) < 1e-7
|
||||
? ring.slice(0, -1)
|
||||
: ring;
|
||||
};
|
||||
const worldPosition = (p) => {
|
||||
const c = geo.cartographic.fromCartesian(p);
|
||||
return [
|
||||
geo.math.toDegrees(c.longitude),
|
||||
geo.math.toDegrees(c.latitude),
|
||||
c.height,
|
||||
];
|
||||
};
|
||||
function meshPositions(mesh) {
|
||||
const result = [];
|
||||
for (let i = 0; i < mesh.positions.length; i += 3)
|
||||
result.push(
|
||||
worldPosition({
|
||||
x: mesh.positions[i],
|
||||
y: mesh.positions[i + 1],
|
||||
z: mesh.positions[i + 2],
|
||||
}),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
function area(positions, indices) {
|
||||
let total = 0;
|
||||
for (let i = 0; i < indices.length; i += 3) {
|
||||
const [a, b, c] = Array.from(indices.slice(i, i + 3), (n) =>
|
||||
positions.slice(n * 3, n * 3 + 3),
|
||||
);
|
||||
const u = b.map((v, j) => v - a[j]),
|
||||
v = c.map((v, j) => v - a[j]);
|
||||
total +=
|
||||
Math.hypot(
|
||||
u[1] * v[2] - u[2] * v[1],
|
||||
u[2] * v[0] - u[0] * v[2],
|
||||
u[0] * v[1] - u[1] * v[0],
|
||||
) / 2;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
async function record() {
|
||||
const { Window } = await import('happy-dom');
|
||||
const dom = new Window({ url: 'http://localhost/' });
|
||||
globalThis.document = dom.document;
|
||||
globalThis.DOMParser = dom.DOMParser;
|
||||
globalThis.XMLSerializer = dom.XMLSerializer;
|
||||
// happy-dom's XML namespace query misses KML elements Cesium reads.
|
||||
function byNamespace(namespace, localName) {
|
||||
return [...this.getElementsByTagName('*')].filter(
|
||||
(el) =>
|
||||
(localName === '*' || el.localName === localName) &&
|
||||
(namespace === '*' || el.namespaceURI === namespace),
|
||||
);
|
||||
}
|
||||
const probe = new DOMParser().parseFromString(
|
||||
'<a><b/></a>',
|
||||
'application/xml',
|
||||
);
|
||||
for (const node of [probe, probe.documentElement])
|
||||
for (let p = Object.getPrototypeOf(node); p; p = Object.getPrototypeOf(p))
|
||||
if (Object.hasOwn(p, 'getElementsByTagNameNS')) {
|
||||
p.getElementsByTagNameNS = byNamespace;
|
||||
break;
|
||||
}
|
||||
|
||||
// Cesium captures the document and XML parser when its modules load.
|
||||
const C = await import('cesium');
|
||||
const reader = await import('../src/data/geoFileImport.js');
|
||||
const fromColor = C.PinBuilder.prototype.fromColor;
|
||||
// Only entity properties are recorded; no pin pixels are rendered.
|
||||
C.PinBuilder.prototype.fromColor = (_color, size) => ({
|
||||
width: size,
|
||||
height: size,
|
||||
});
|
||||
const result = {
|
||||
version: 1,
|
||||
recorder: `Cesium ${C.VERSION} DataSources in Node; canvas pins stubbed, KMZ images resolved locally. Re-record with node scripts/qa-geo-import-reference.mjs --record.`,
|
||||
tolerances,
|
||||
cesiumVersion: C.VERSION,
|
||||
files: {},
|
||||
};
|
||||
console.log(result.recorder);
|
||||
const now = C.JulianDate.fromIso8601('2020-01-01T12:00:00Z');
|
||||
const v = (property) => property?.getValue?.(now);
|
||||
const rgba = (color) =>
|
||||
color ? [color.red, color.green, color.blue, color.alpha] : [1, 1, 1, 1];
|
||||
const position = (p) => {
|
||||
const c = C.Cartographic.fromCartesian(p);
|
||||
return [
|
||||
C.Math.toDegrees(c.longitude),
|
||||
C.Math.toDegrees(c.latitude),
|
||||
c.height,
|
||||
];
|
||||
};
|
||||
const ring = (positions, raised) => {
|
||||
const out = positions
|
||||
.map(position)
|
||||
.map(([x, y, z]) => [x, y, raised ? z : 0]);
|
||||
if (
|
||||
out.length > 1 &&
|
||||
C.Cartesian3.equalsEpsilon(positions[0], positions.at(-1), 1e-12, 1e-7)
|
||||
)
|
||||
out.pop();
|
||||
return out;
|
||||
};
|
||||
try {
|
||||
for (const file of files) {
|
||||
const bytes = await fs.readFile(path.join(directory, file));
|
||||
const urls = [];
|
||||
try {
|
||||
let ds;
|
||||
if (file.endsWith('.geojson')) {
|
||||
const json = JSON.parse(bytes.toString('utf8'));
|
||||
// The import's GeoJSON policy keeps names and grounds every coordinate.
|
||||
const stack = [json];
|
||||
while (stack.length) {
|
||||
const node = stack.pop();
|
||||
if (node.type === 'FeatureCollection') stack.push(...node.features);
|
||||
else if (node.type === 'Feature') {
|
||||
node.properties = { name: node.properties?.name };
|
||||
stack.push(node.geometry);
|
||||
} else if (node.type === 'GeometryCollection')
|
||||
stack.push(...node.geometries);
|
||||
else {
|
||||
const coords = [node.coordinates];
|
||||
while (coords.length) {
|
||||
const c = coords.pop();
|
||||
if (typeof c[0] === 'number') c.length = 2;
|
||||
else coords.push(...c);
|
||||
}
|
||||
}
|
||||
}
|
||||
const color = C.Color.fromCssColorString('#39d0ff');
|
||||
ds = await C.GeoJsonDataSource.load(json, {
|
||||
clampToGround: true,
|
||||
markerColor: color,
|
||||
markerSize: 48,
|
||||
stroke: color,
|
||||
strokeWidth: 3,
|
||||
fill: color.withAlpha(0.25),
|
||||
});
|
||||
} else {
|
||||
let text = reader.decodeXml(bytes),
|
||||
resolveLocal = () => null;
|
||||
if (file.endsWith('.kmz')) {
|
||||
const entries = await reader.readZipEntries(bytes);
|
||||
const main = reader.kmzMainDocument(entries);
|
||||
text = reader.decodeXml(entries.get(main));
|
||||
resolveLocal = reader.kmzImageResolver(entries, main, (url) =>
|
||||
urls.push(url),
|
||||
);
|
||||
}
|
||||
const doc = reader.parseXml(text, 'KML');
|
||||
// Keep the import's sanitization and tessellation policy.
|
||||
reader.sanitizeKmlDocument(doc, resolveLocal);
|
||||
reader.deduplicateKmlStyles(doc);
|
||||
ds = await C.KmlDataSource.load(doc, {
|
||||
clampToGround: true,
|
||||
});
|
||||
}
|
||||
const entities = [];
|
||||
for (const entity of ds.entities.values) {
|
||||
const name = entity.name;
|
||||
if (entity.billboard && entity.position) {
|
||||
const b = entity.billboard;
|
||||
const offset = v(b.pixelOffset),
|
||||
distance = v(b.scaleByDistance);
|
||||
const heightReference =
|
||||
v(b.heightReference) ?? C.HeightReference.NONE;
|
||||
const pos = position(v(entity.position));
|
||||
if (heightReference === C.HeightReference.CLAMP_TO_GROUND)
|
||||
pos[2] = 0;
|
||||
entities.push({
|
||||
kind: 'pin',
|
||||
name,
|
||||
positions: [pos],
|
||||
altitude:
|
||||
heightReference === C.HeightReference.RELATIVE_TO_GROUND
|
||||
? 'relative'
|
||||
: heightReference === C.HeightReference.CLAMP_TO_GROUND
|
||||
? 'ground'
|
||||
: 'absolute',
|
||||
color: rgba(v(b.color)),
|
||||
width: v(b.width) ?? 48,
|
||||
height: v(b.height) ?? 48,
|
||||
scale: v(b.scale) ?? 1,
|
||||
rotation: v(b.rotation) ?? 0,
|
||||
origin:
|
||||
v(b.verticalOrigin) === C.VerticalOrigin.BOTTOM
|
||||
? 'bottom'
|
||||
: 'center',
|
||||
offset: offset ? [offset.x, offset.y] : [0, 0],
|
||||
distance: distance
|
||||
? [
|
||||
distance.near,
|
||||
distance.nearValue,
|
||||
distance.far,
|
||||
distance.farValue,
|
||||
]
|
||||
: null,
|
||||
});
|
||||
}
|
||||
if (entity.polyline) {
|
||||
const line = entity.polyline;
|
||||
const ground = v(line.clampToGround) === true;
|
||||
entities.push({
|
||||
kind: 'line',
|
||||
name,
|
||||
ground,
|
||||
positions: v(line.positions)
|
||||
.map(position)
|
||||
.map(([x, y, z]) => [x, y, ground ? 0 : z]),
|
||||
color: rgba(v(line.material)?.color),
|
||||
width: v(line.width) ?? 1,
|
||||
});
|
||||
}
|
||||
if (entity.polygon) {
|
||||
const p = entity.polygon,
|
||||
hierarchy = v(p.hierarchy);
|
||||
const raised = v(p.perPositionHeight) === true;
|
||||
const rings = [
|
||||
hierarchy.positions,
|
||||
...hierarchy.holes.map((h) => h.positions),
|
||||
]
|
||||
.map((r) => ring(r, raised))
|
||||
.filter((r) => r.length >= 3);
|
||||
const fill = v(p.fill) !== false;
|
||||
entities.push({
|
||||
kind: 'polygon',
|
||||
name,
|
||||
raised,
|
||||
rings,
|
||||
color: fill ? rgba(v(p.material)?.color) : null,
|
||||
outline: raised && v(p.outline) ? rgba(v(p.outlineColor)) : null,
|
||||
});
|
||||
}
|
||||
if (entity.rectangle) {
|
||||
const r = entity.rectangle,
|
||||
rectangle = v(r.coordinates),
|
||||
rotation = v(r.rotation) ?? 0;
|
||||
let bounds,
|
||||
drawable = true;
|
||||
try {
|
||||
const b = C.RectangleGeometry.computeRectangle({
|
||||
rectangle,
|
||||
rotation,
|
||||
});
|
||||
bounds = [b.west, b.south, b.east, b.north].map(C.Math.toDegrees);
|
||||
} catch {
|
||||
drawable = false;
|
||||
bounds = null;
|
||||
}
|
||||
entities.push({
|
||||
kind: 'overlay',
|
||||
name,
|
||||
drawable,
|
||||
bounds,
|
||||
height: v(r.height) ?? 0,
|
||||
rotation: C.Math.toDegrees(rotation),
|
||||
drawOrder: v(r.zIndex) ?? 0,
|
||||
image: Boolean(r.material?.image),
|
||||
color: rgba(v(r.material)?.color),
|
||||
});
|
||||
}
|
||||
}
|
||||
result.files[file] = entities;
|
||||
console.log(`Recorded ${file}: ${entities.length} entities`);
|
||||
} finally {
|
||||
urls.forEach((url) => URL.revokeObjectURL(url));
|
||||
}
|
||||
}
|
||||
await fs.writeFile(referencePath, `${JSON.stringify(result, null, 2)}\n`);
|
||||
} finally {
|
||||
C.PinBuilder.prototype.fromColor = fromColor;
|
||||
await dom.happyDOM.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function planned(file) {
|
||||
// Only the file reader needs XML; the planner remains independent of the DOM.
|
||||
const { Window } = await import('happy-dom');
|
||||
const dom = new Window();
|
||||
globalThis.DOMParser ??= dom.DOMParser;
|
||||
globalThis.XMLSerializer ??= dom.XMLSerializer;
|
||||
const { loadGeoFile } = await import('../src/data/geoFileImport.js');
|
||||
const { importImageSize } = await import('../src/data/geoImportImages.js');
|
||||
const bytes = await fs.readFile(path.join(directory, file));
|
||||
const loaded = await loadGeoFile(new File([bytes], file));
|
||||
try {
|
||||
const metadata = new Map();
|
||||
const pending = planImport(loaded.records);
|
||||
for (const url of pending.images) {
|
||||
const size = importImageSize(
|
||||
new Uint8Array(await (await fetch(url)).arrayBuffer()),
|
||||
);
|
||||
metadata.set(url, size ? { width: size[0], height: size[1] } : null);
|
||||
}
|
||||
const plan = planImport(loaded.records, undefined, { images: metadata });
|
||||
const entities = [];
|
||||
for (const look of plan.pins)
|
||||
for (const pin of look.draw ? look.pins : []) {
|
||||
const icon = look.icon,
|
||||
box = iconBox(icon, metadata.get(icon.image?.url));
|
||||
entities.push({
|
||||
kind: 'pin',
|
||||
name: pin.record.name,
|
||||
positions: [[pin.lon, pin.lat, pin.height ?? pin.offset]],
|
||||
altitude:
|
||||
pin.height !== null
|
||||
? 'absolute'
|
||||
: pin.offset
|
||||
? 'relative'
|
||||
: pin.record.altitude,
|
||||
color: pin.color,
|
||||
width: box.width / (icon.scale ?? 1),
|
||||
height: box.height / (icon.scale ?? 1),
|
||||
scale: icon.scale ?? 1,
|
||||
rotation: pin.rotation,
|
||||
origin: icon.origin ?? 'center',
|
||||
offset: icon.offsetPx ?? [0, 0],
|
||||
distance: icon.scaleByDistance ?? null,
|
||||
});
|
||||
}
|
||||
for (const line of loaded.records.lines) {
|
||||
const ground = plan.ground.find((entry) => entry.record === line);
|
||||
const raised = plan.lines.find((entry) => entry.record === line);
|
||||
if (!ground && !raised) continue;
|
||||
entities.push({
|
||||
kind: 'line',
|
||||
name: line.name,
|
||||
ground: Boolean(ground),
|
||||
positions: ground
|
||||
? ground.item.positions.map(([x, y]) => [x, y, 0])
|
||||
: raised.positions.map(worldPosition),
|
||||
color: (ground?.item ?? raised).color,
|
||||
width: (ground?.item ?? raised).widthPx,
|
||||
});
|
||||
}
|
||||
for (const polygon of loaded.records.polygons) {
|
||||
const ground = plan.ground.find((entry) => entry.record === polygon);
|
||||
const mesh = plan.meshes.find((entry) => entry.record === polygon);
|
||||
const outline = plan.lines.find((entry) => entry.record === polygon);
|
||||
let rings = [];
|
||||
if (ground)
|
||||
rings = ground.item.rings.map((r) =>
|
||||
openRing(r).map(([x, y]) => [x, y, 0]),
|
||||
);
|
||||
if (mesh) {
|
||||
const positions = meshPositions(mesh);
|
||||
let start = 0;
|
||||
rings = polygon.rings
|
||||
.map(openRing)
|
||||
.filter((r) => r.length >= 3)
|
||||
.map((r) => {
|
||||
const result = positions.slice(start, start + r.length);
|
||||
start += r.length;
|
||||
return result;
|
||||
});
|
||||
if (!(area(mesh.positions, mesh.indices) > 0))
|
||||
throw new Error(`${file}: empty raised fill`);
|
||||
}
|
||||
if (ground || mesh || outline)
|
||||
entities.push({
|
||||
kind: 'polygon',
|
||||
name: polygon.name,
|
||||
raised: Boolean(mesh || outline),
|
||||
rings,
|
||||
color: ground?.item.fill ?? mesh?.color ?? null,
|
||||
outline: outline?.color ?? null,
|
||||
});
|
||||
}
|
||||
for (const overlay of loaded.records.overlays) {
|
||||
const entry = [...plan.overlays, ...plan.meshes, ...plan.ground].find(
|
||||
(e) => e.record === overlay,
|
||||
);
|
||||
let bounds = null;
|
||||
if (entry?.rectangle) {
|
||||
const r = entry.rectangle;
|
||||
bounds = [r.west, r.south, r.east, r.north];
|
||||
} else if (entry?.item) {
|
||||
const points = entry.item.rings[0];
|
||||
bounds = [
|
||||
Math.min(...points.map((p) => p[0])),
|
||||
Math.min(...points.map((p) => p[1])),
|
||||
Math.max(...points.map((p) => p[0])),
|
||||
Math.max(...points.map((p) => p[1])),
|
||||
];
|
||||
}
|
||||
if (!bounds && entry?.positions) {
|
||||
const points = meshPositions(entry);
|
||||
bounds = [
|
||||
Math.min(...points.map((p) => p[0])),
|
||||
Math.min(...points.map((p) => p[1])),
|
||||
Math.max(...points.map((p) => p[0])),
|
||||
Math.max(...points.map((p) => p[1])),
|
||||
];
|
||||
}
|
||||
entities.push({
|
||||
kind: 'overlay',
|
||||
name: overlay.name,
|
||||
drawable: Boolean(entry),
|
||||
bounds,
|
||||
height: entry?.positions ? meshPositions(entry)[0][2] : 0,
|
||||
rotation: entry?.canvas
|
||||
? (entry.canvas.angle * 180) / Math.PI
|
||||
: overlay.rotation,
|
||||
drawOrder: entry?.positions ? 0 : overlay.drawOrder,
|
||||
image: Boolean(entry?.url),
|
||||
color:
|
||||
entry?.item?.fill ??
|
||||
(!entry?.url && entry?.color
|
||||
? entry.color
|
||||
: entry
|
||||
? [...(entry.canvas?.tint ?? [1, 1, 1]), entry.alpha]
|
||||
: overlay.color),
|
||||
});
|
||||
}
|
||||
const actualOrder = [
|
||||
...plan.overlays,
|
||||
...plan.meshes.filter((entry) => entry.url),
|
||||
]
|
||||
.filter((entry) => !entry.positions)
|
||||
.map((entry) => entry.record.name);
|
||||
return { entities, actualOrder, report: plan.report };
|
||||
} finally {
|
||||
loaded.revoke();
|
||||
}
|
||||
}
|
||||
|
||||
function differences(actual, expected, location, output) {
|
||||
if (typeof actual === 'number' && typeof expected === 'number') {
|
||||
const tolerance = /color|outline/.test(location)
|
||||
? tolerances.color
|
||||
: /positions.*\[2\]$|rings.*\[2\]$|\.height$/.test(location)
|
||||
? tolerances.metres
|
||||
: tolerances.degrees;
|
||||
if (!Number.isFinite(actual) || Math.abs(actual - expected) > tolerance)
|
||||
output.push(
|
||||
`${location}: expected ${expected}, got ${actual} (tolerance ${tolerance})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(expected)) {
|
||||
if (!Array.isArray(actual) || actual.length !== expected.length) {
|
||||
output.push(
|
||||
`${location}: expected ${expected.length} entries, got ${actual?.length ?? 'none'}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
expected.forEach((value, i) =>
|
||||
differences(actual[i], value, `${location}[${i}]`, output),
|
||||
);
|
||||
} else if (expected && typeof expected === 'object') {
|
||||
for (const key of Object.keys(expected))
|
||||
differences(actual?.[key], expected[key], `${location}.${key}`, output);
|
||||
} else if (actual !== expected)
|
||||
output.push(
|
||||
`${location}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (process.argv.includes('--record')) await record();
|
||||
else {
|
||||
const reference = JSON.parse(await fs.readFile(referencePath, 'utf8'));
|
||||
console.log(reference.recorder);
|
||||
const output = [];
|
||||
differences(files, Object.keys(reference.files).sort(), 'corpus', output);
|
||||
for (const file of files) {
|
||||
const { entities, actualOrder } = await planned(file);
|
||||
const expected = reference.files[file];
|
||||
// A rectangle rejected at the pole has no drawable style or height.
|
||||
const visible = (e) =>
|
||||
e.kind === 'overlay' && !e.drawable
|
||||
? { kind: e.kind, name: e.name, drawable: false, bounds: null }
|
||||
: e;
|
||||
const sort = (a, b) =>
|
||||
`${a.kind}:${a.name}`.localeCompare(`${b.kind}:${b.name}`);
|
||||
differences(
|
||||
entities.map(visible).sort(sort),
|
||||
expected.map(visible).sort(sort),
|
||||
file,
|
||||
output,
|
||||
);
|
||||
const expectedOrder = expected
|
||||
.filter((e) => e.kind === 'overlay' && e.image && e.drawable && !e.height)
|
||||
.sort((a, b) => a.drawOrder - b.drawOrder)
|
||||
.map((e) => e.name);
|
||||
differences(actualOrder, expectedOrder, `${file}.drawOrder`, output);
|
||||
}
|
||||
output.forEach((line) => console.error(line));
|
||||
console.log(`${files.length} files: ${output.length} differences`);
|
||||
process.exitCode = output.length ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user