@honua/app-platform 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/control-plane/dataset-import/index.d.ts +2 -1
- package/control-plane/dataset-import/index.d.ts.map +1 -1
- package/control-plane/dataset-import/index.js +1 -0
- package/control-plane/dataset-import/server-job-source-client.d.ts +16 -0
- package/control-plane/dataset-import/server-job-source-client.d.ts.map +1 -0
- package/control-plane/dataset-import/server-job-source-client.js +143 -0
- package/control-plane/dataset-import/types.d.ts +65 -3
- package/control-plane/dataset-import/types.d.ts.map +1 -1
- package/control-plane/dataset-import/upload-client.d.ts +7 -0
- package/control-plane/dataset-import/upload-client.d.ts.map +1 -1
- package/control-plane/dataset-import/upload-client.js +14 -0
- package/core/error-envelope.js +1 -0
- package/core/stac-conformance.d.ts +66 -0
- package/core/stac-conformance.d.ts.map +1 -0
- package/core/stac-conformance.js +177 -0
- package/core/stac.d.ts +20 -2
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +71 -7
- package/core/types.d.ts +10 -0
- package/core/types.d.ts.map +1 -1
- package/map/feature-service-adapter.d.ts +3 -3
- package/map/feature-service-adapter.d.ts.map +1 -1
- package/map/feature-service-adapter.js +99 -69
- package/package.json +2 -2
|
@@ -22,8 +22,46 @@
|
|
|
22
22
|
*
|
|
23
23
|
* @module
|
|
24
24
|
*/
|
|
25
|
+
import { esriGeometryToGeoJSON } from "../core/esri-geojson.js";
|
|
25
26
|
import { HonuaFeatureLayer } from "../core/surfaces.js";
|
|
26
27
|
import { parseFeatureLayerUrl } from "../esri-compat/url.js";
|
|
28
|
+
/** Validate an unambiguous shape when the service cannot declare its geometry type. */
|
|
29
|
+
function isValidGenericGeometry(geometry) {
|
|
30
|
+
const shapes = [
|
|
31
|
+
"x" in geometry || "y" in geometry,
|
|
32
|
+
"points" in geometry,
|
|
33
|
+
"paths" in geometry,
|
|
34
|
+
"rings" in geometry,
|
|
35
|
+
["xmin", "ymin", "xmax", "ymax"].some((key) => key in geometry),
|
|
36
|
+
];
|
|
37
|
+
if (shapes.filter(Boolean).length !== 1)
|
|
38
|
+
return false;
|
|
39
|
+
if (shapes[0])
|
|
40
|
+
return asFiniteNumber(geometry.x) !== undefined && asFiniteNumber(geometry.y) !== undefined;
|
|
41
|
+
if (shapes[1])
|
|
42
|
+
return (Array.isArray(geometry.points) &&
|
|
43
|
+
geometry.points.length > 0 &&
|
|
44
|
+
geometry.points.every((point) => asPosition(point) !== undefined));
|
|
45
|
+
if (shapes[2] || shapes[3]) {
|
|
46
|
+
const parts = shapes[2] ? geometry.paths : geometry.rings;
|
|
47
|
+
const polygon = shapes[3];
|
|
48
|
+
return (Array.isArray(parts) &&
|
|
49
|
+
parts.length > 0 &&
|
|
50
|
+
parts.every((part) => {
|
|
51
|
+
if (!Array.isArray(part) ||
|
|
52
|
+
part.length < (polygon ? 4 : 2) ||
|
|
53
|
+
!part.every((point) => asPosition(point) !== undefined))
|
|
54
|
+
return false;
|
|
55
|
+
if (!polygon)
|
|
56
|
+
return true;
|
|
57
|
+
const first = asPosition(part[0]);
|
|
58
|
+
const last = asPosition(part[part.length - 1]);
|
|
59
|
+
return first[0] === last[0] && first[1] === last[1];
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
const bounds = [geometry.xmin, geometry.ymin, geometry.xmax, geometry.ymax].map(asFiniteNumber);
|
|
63
|
+
return bounds.every((value) => value !== undefined) && bounds[0] <= bounds[2] && bounds[1] <= bounds[3];
|
|
64
|
+
}
|
|
27
65
|
/**
|
|
28
66
|
* Convert a single Esri geometry into a GeoJSON geometry.
|
|
29
67
|
*
|
|
@@ -31,75 +69,68 @@ import { parseFeatureLayerUrl } from "../esri-compat/url.js";
|
|
|
31
69
|
* multipoint, polyline, polygon, envelope). Returns `null` when the geometry is
|
|
32
70
|
* absent or unrecognised so the feature still renders attribute-only.
|
|
33
71
|
*/
|
|
34
|
-
export function esriGeometryToGeoJson(geometry, geometryType) {
|
|
35
|
-
if (!geometry)
|
|
72
|
+
export function esriGeometryToGeoJson(geometry, geometryType, dimensions) {
|
|
73
|
+
if (!geometry || typeof geometry !== "object" || Array.isArray(geometry))
|
|
36
74
|
return null;
|
|
75
|
+
let shape;
|
|
76
|
+
if (geometryType === undefined || geometryType === null || geometryType === "esriGeometryNull") {
|
|
77
|
+
shape = geometry;
|
|
37
78
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (geometryType === "esriGeometryPoint" || (geometry.x !== undefined && geometry.y !== undefined)) {
|
|
41
|
-
const x = asFiniteNumber(geometry.x);
|
|
42
|
-
const y = asFiniteNumber(geometry.y);
|
|
43
|
-
if (x === undefined || y === undefined)
|
|
44
|
-
return null;
|
|
45
|
-
return { type: "Point", coordinates: [x, y] };
|
|
79
|
+
else if (geometryType === "esriGeometryPoint" || (geometry.x !== undefined && geometry.y !== undefined)) {
|
|
80
|
+
shape = { x: geometry.x, y: geometry.y, z: geometry.z };
|
|
46
81
|
}
|
|
47
|
-
if (geometryType === "esriGeometryMultipoint") {
|
|
48
|
-
|
|
49
|
-
.map(asCoordinatePair)
|
|
50
|
-
.filter((value) => value !== undefined);
|
|
51
|
-
return { type: "MultiPoint", coordinates: points };
|
|
82
|
+
else if (geometryType === "esriGeometryMultipoint") {
|
|
83
|
+
shape = { points: geometry.points };
|
|
52
84
|
}
|
|
53
|
-
if (geometryType === "esriGeometryPolyline") {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return { type: "LineString", coordinates: paths[0] };
|
|
59
|
-
}
|
|
60
|
-
return { type: "MultiLineString", coordinates: paths };
|
|
85
|
+
else if (geometryType === "esriGeometryPolyline") {
|
|
86
|
+
shape = { paths: geometry.paths };
|
|
87
|
+
}
|
|
88
|
+
else if (geometryType === "esriGeometryPolygon") {
|
|
89
|
+
shape = { rings: geometry.rings };
|
|
61
90
|
}
|
|
62
|
-
if (geometryType === "
|
|
63
|
-
|
|
64
|
-
.map(asCoordinateArray)
|
|
65
|
-
.filter((ring) => ring.length > 0);
|
|
66
|
-
// MapLibre does not enforce GeoJSON ring winding for rendering, so the
|
|
67
|
-
// adapter passes Esri rings through as a Polygon without re-nesting.
|
|
68
|
-
return { type: "Polygon", coordinates: rings };
|
|
91
|
+
else if (geometryType === "esriGeometryEnvelope") {
|
|
92
|
+
shape = { xmin: geometry.xmin, ymin: geometry.ymin, xmax: geometry.xmax, ymax: geometry.ymax };
|
|
69
93
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
else
|
|
95
|
+
return null;
|
|
96
|
+
// One converter owns exterior/hole grouping, winding and coordinate ordinates
|
|
97
|
+
// for both metadata-declared and structurally inferred feature geometry.
|
|
98
|
+
if (!isValidGenericGeometry(shape))
|
|
99
|
+
return null;
|
|
100
|
+
// Optional Esri Z/M can be null. Keep finite spatial coordinates, omit
|
|
101
|
+
// absent Z, and never treat a measure as GeoJSON altitude.
|
|
102
|
+
const converted = esriGeometryToGeoJSON({ ...shape, z: asFiniteNumber(shape.z) });
|
|
103
|
+
if (!converted || !("coordinates" in converted))
|
|
104
|
+
return null;
|
|
105
|
+
const hasZ = asFiniteNumber(shape.z) !== undefined || (geometry.hasZ ?? dimensions?.hasZ) === true;
|
|
106
|
+
const coordinates = projectCoordinates(converted.coordinates, hasZ);
|
|
107
|
+
const polygons = converted.type === "Polygon"
|
|
108
|
+
? [coordinates]
|
|
109
|
+
: converted.type === "MultiPolygon"
|
|
110
|
+
? coordinates
|
|
111
|
+
: [];
|
|
112
|
+
for (const polygon of polygons) {
|
|
113
|
+
for (const ring of polygon) {
|
|
114
|
+
const first = ring[0];
|
|
115
|
+
const last = ring[ring.length - 1];
|
|
116
|
+
// Close after dimension projection, comparing every retained ordinate.
|
|
117
|
+
// Append the first position so a distinct final Z is not discarded.
|
|
118
|
+
if (first && last && (first.length !== last.length || first.some((value, index) => value !== last[index])))
|
|
119
|
+
ring.push([...first]);
|
|
77
120
|
}
|
|
78
|
-
return {
|
|
79
|
-
type: "Polygon",
|
|
80
|
-
coordinates: [
|
|
81
|
-
[
|
|
82
|
-
[xmin, ymin],
|
|
83
|
-
[xmax, ymin],
|
|
84
|
-
[xmax, ymax],
|
|
85
|
-
[xmin, ymax],
|
|
86
|
-
[xmin, ymin],
|
|
87
|
-
],
|
|
88
|
-
],
|
|
89
|
-
};
|
|
90
121
|
}
|
|
91
|
-
return
|
|
122
|
+
return { type: converted.type, coordinates };
|
|
92
123
|
}
|
|
93
124
|
/**
|
|
94
125
|
* Convert an array of Esri features (attributes + geometry) into a GeoJSON
|
|
95
126
|
* `FeatureCollection`.
|
|
96
127
|
*/
|
|
97
|
-
export function esriFeaturesToGeoJson(features, geometryType) {
|
|
128
|
+
export function esriFeaturesToGeoJson(features, geometryType, dimensions) {
|
|
98
129
|
return {
|
|
99
130
|
type: "FeatureCollection",
|
|
100
131
|
features: features.map((feature) => ({
|
|
101
132
|
type: "Feature",
|
|
102
|
-
geometry: esriGeometryToGeoJson(feature.geometry ?? null, geometryType),
|
|
133
|
+
geometry: esriGeometryToGeoJson(feature.geometry ?? null, geometryType, dimensions),
|
|
103
134
|
properties: { ...(feature.attributes ?? {}) },
|
|
104
135
|
})),
|
|
105
136
|
};
|
|
@@ -136,8 +167,8 @@ export async function loadHonuaFeatureServiceGeoJson(client, url, options = {})
|
|
|
136
167
|
};
|
|
137
168
|
const metadataPromise = layer.metadata(options.signal ? { signal: options.signal } : {}).catch(() => undefined);
|
|
138
169
|
const features = await layer.queryFeaturesAll(request);
|
|
139
|
-
const
|
|
140
|
-
const data = esriFeaturesToGeoJson(features, geometryType);
|
|
170
|
+
const metadata = await metadataPromise;
|
|
171
|
+
const data = esriFeaturesToGeoJson(features, metadata?.geometryType, metadata);
|
|
141
172
|
return {
|
|
142
173
|
type: "geojson",
|
|
143
174
|
data,
|
|
@@ -260,20 +291,19 @@ async function allSettledLimited(values, concurrency, worker) {
|
|
|
260
291
|
function asFiniteNumber(value) {
|
|
261
292
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
262
293
|
}
|
|
263
|
-
function
|
|
264
|
-
return Array.isArray(value)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const y = asFiniteNumber(value[1]);
|
|
271
|
-
if (x === undefined || y === undefined)
|
|
272
|
-
return undefined;
|
|
273
|
-
return [x, y];
|
|
294
|
+
function asPosition(value) {
|
|
295
|
+
return Array.isArray(value) &&
|
|
296
|
+
value.length >= 2 &&
|
|
297
|
+
asFiniteNumber(value[0]) !== undefined &&
|
|
298
|
+
asFiniteNumber(value[1]) !== undefined
|
|
299
|
+
? value
|
|
300
|
+
: undefined;
|
|
274
301
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
302
|
+
/** GeoJSON has spatial coordinates, not Esri measures; absent optional Z stays 2D. */
|
|
303
|
+
function projectCoordinates(coordinates, hasZ) {
|
|
304
|
+
if (!Array.isArray(coordinates))
|
|
305
|
+
return coordinates;
|
|
306
|
+
if (Array.isArray(coordinates[0]))
|
|
307
|
+
return coordinates.map((part) => projectCoordinates(part, hasZ));
|
|
308
|
+
return coordinates.slice(0, hasZ && asFiniteNumber(coordinates[2]) !== undefined ? 3 : 2);
|
|
279
309
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@honua/app-platform",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Honua application-platform surfaces: app-shell, workspace, scene, operator, studio, and hosted-product clients",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gis",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"proj4": "^2.20.9"
|
|
122
122
|
},
|
|
123
123
|
"peerDependencies": {
|
|
124
|
-
"@honua/sdk": "0.2.
|
|
124
|
+
"@honua/sdk": "0.2.7",
|
|
125
125
|
"@bufbuild/protobuf": "^2.0.0",
|
|
126
126
|
"@connectrpc/connect": "^2.0.0",
|
|
127
127
|
"@connectrpc/connect-web": "^2.0.0",
|