landxml 0.0.1 → 0.2.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # landxml
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ae3bcf7: Swapped xml reader package from `xml2json` to `xml-js`
8
+ - ae3bcf7: Added `glb` download example into README.md
9
+
10
+ ### Patch Changes
11
+
12
+ - ae3bcf7: Fixed dependency list
13
+
14
+ ## 0.1.0
15
+
16
+ ### Minor Changes
17
+
18
+ - e0fa5a8: Added tests
19
+ - e0fa5a8: Added contour lines geojson export
20
+ - e0fa5a8: Added GLB file export
21
+ - e0fa5a8: Exposed functions for extenal use
22
+ - e0fa5a8: Added README.md
23
+ - e0fa5a8: Added LandXML parser
24
+
25
+ ### Patch Changes
26
+
27
+ - 4e2c14f: Fixed test imports
28
+ - 3c53b08: Fixed types on toGeojsonContours to reflect that surface may have a wktString
29
+ - 3c53b08: Updated README GeoJSON Contours code example
package/README.md CHANGED
@@ -1 +1,48 @@
1
- Just a landxml parser for the web.
1
+ # LandXML Parser for Contour Geojson and 3D Models
2
+
3
+ Easily transform LandXML surfaces into stunning GeoJSON contours or immersive GLB models to explore within threeJS or any popular 3D software.
4
+
5
+ ### Features
6
+
7
+ - **toGlb:** Generate GLB models from LandXML surfaces.
8
+ - **toGeojsonContours:** Convert LandXML data into detailed contour GeoJSON representations.
9
+ - **reprojectGeojson:** Effortlessly reproject GeoJSON data to desired projection using `proj4`
10
+
11
+ ### Example: Retrieve GeoJSON Contours
12
+
13
+ ```typescript
14
+ import { toGeojsonContours, reprojectGeoJson } from "landxml";
15
+
16
+ const loadGeojson = async () => {
17
+ const landXmlString = "<?xml version="1.0"?>...</LandXML>";
18
+ const contourInterval = 2;
19
+ const geojsonSurfaces = await toGeojsonContours(landXmlString, contourInterval);
20
+
21
+ // Retrieve geojson with raw LandXML coordinates
22
+ let { geojson, wktString } = geojsonSurfaces[0];
23
+
24
+ // Reproject GeoJSON to a desired coordinate system (e.g., WGS84)
25
+ const targetCoordinateSystem = wktString || "WGS84";
26
+ const keepOriginalGeometryAsFeatureProperties = false;
27
+ return reprojectGeoJson(geojson, wktString, targetCoordinateSystem, keepOriginalGeometryAsFeatureProperties);
28
+ }
29
+ ```
30
+
31
+ ### Example: Convert to GLB 3D model
32
+
33
+ ```typescript
34
+ import { toGlb } from "landxml";
35
+
36
+ const LandXml2GlbModel = async () => {
37
+ const landXmlString = "<?xml version="1.0"?>...</LandXML>";
38
+
39
+ // Define how the model's origin should be positioned (options: "origin" | "auto" | [x: number, y: number])
40
+ const center = "auto";
41
+ const glbSurfaces = await toGlb(landXmlString, center);
42
+
43
+ let { download, glb } = glbSurfaces[0];
44
+ download()
45
+
46
+ // Alternatively, process the raw binary data from the 'glb' variable (Uint8Array)
47
+ }
48
+ ```
package/dist/index.d.mts CHANGED
@@ -1,3 +1,62 @@
1
- declare const _default: () => void;
1
+ import * as geojson from 'geojson';
2
+ import { FeatureCollection, LineString } from 'geojson';
2
3
 
3
- export { _default as default };
4
+ /**
5
+ * @param landXmlString
6
+ * @param center 3D models don't work well when they're far from origin (coordinate `[0,0,0]`), therefore by default your center is moved to the median of x and y axis of your surface
7
+ * @param surfaceId Surface name or index if your LandXML contains multiple surfaces. By default all surfaces are converted and returns an array of glb Uint8Array
8
+ * @returns {Object[]} glbs - Array of processed glbs (will also return an array when just one surface has been processed)
9
+ * @returns {string} glbs[].name - Name of surface as defined in LandXML
10
+ * @returns {string} glbs[].description - Description of surface as defined in LandXML
11
+ * @returns {string} glbs[].sourceFile - Source file of where the LandXML was generated from.
12
+ * @returns {string} glbs[].timeStamp - Timestamp of when the surface was exported into LandXML
13
+ * @returns {Uint8Array} glbs[].glb - Uint8Array binary data of glb
14
+ * @returns {[number, number]} glbs[].center - Offset center of the processed glb
15
+ * @returns {function(): void} glbs[].download - Convenient way to download the GLB, within the filename the new center will be appended.
16
+ */
17
+ declare const toGlb: (landXmlString: string, center?: "auto" | "origin" | [
18
+ x: number,
19
+ y: number
20
+ ], surfaceId?: string | number) => Promise<{
21
+ name: string;
22
+ description: string;
23
+ sourceFile: string;
24
+ timeStamp: string;
25
+ glb: Uint8Array;
26
+ center: [x: number, y: number];
27
+ download: () => void;
28
+ }[]>;
29
+
30
+ /**
31
+ * @param landXmlString
32
+ * @param contourInterval Interval at which you would like to generate contour lines
33
+ * @param surfaceId Surface name or index if your LandXML contains multiple surfaces. By default all surfaces are converted and returns an array of glb Uint8Array
34
+ * @returns {Object[]} surfaceContours - Array of processed surface contours (will also return an array when just one surface has been processed)
35
+ * @returns {string} surfaceContours[].name - Name of surface as defined in LandXML
36
+ * @returns {string} surfaceContours[].description - Description of surface as defined in LandXML
37
+ * @returns {string} surfaceContours[].sourceFile - Source file of where the LandXML was generated from.
38
+ * @returns {string} surfaceContours[].timeStamp - Timestamp of when the surface was exported into LandXML
39
+ * @returns {string} surfaceContours[].wktString - WKT string of LandXML coordinate system projection
40
+ * @returns {Object} surfaceContours[].geojson - Geojson feature collection of contour lines
41
+ */
42
+ declare const toGeojsonContours: (landXmlString: string, contourInterval?: number, surfaceId?: string | number) => Promise<{
43
+ name: string;
44
+ description: string;
45
+ sourceFile: string;
46
+ timeStamp: string;
47
+ wktString?: string;
48
+ geojson: FeatureCollection<LineString, {
49
+ z: number;
50
+ }>;
51
+ }[]>;
52
+
53
+ /**
54
+ * @param geojson
55
+ * @param sourceProjection can be a proj4 string or WKT string, you will likely have a wkt string available with your LandXML if you used Civil 3D exporter and had your drawing geo-referenced
56
+ * @param targetProjection you will most likely want to use WGS84 for online viewing, however any other projection you might need can be used as long as it's valid
57
+ * @param keepOriginalGeometryAsFeatureProperty if you intend to repurpose the original geometry, it can be added to geojson feature properties
58
+ * @returns {FeatureCollection} Geojson FeatureCollection with updated geometry coordinates
59
+ */
60
+ declare const reprojectGeoJson: (geojson: FeatureCollection, sourceProjection: string, targetProjection?: string, keepOriginalGeometryAsFeatureProperty?: boolean) => FeatureCollection<geojson.Geometry, geojson.GeoJsonProperties>;
61
+
62
+ export { reprojectGeoJson, toGeojsonContours, toGlb };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,62 @@
1
- declare const _default: () => void;
1
+ import * as geojson from 'geojson';
2
+ import { FeatureCollection, LineString } from 'geojson';
2
3
 
3
- export { _default as default };
4
+ /**
5
+ * @param landXmlString
6
+ * @param center 3D models don't work well when they're far from origin (coordinate `[0,0,0]`), therefore by default your center is moved to the median of x and y axis of your surface
7
+ * @param surfaceId Surface name or index if your LandXML contains multiple surfaces. By default all surfaces are converted and returns an array of glb Uint8Array
8
+ * @returns {Object[]} glbs - Array of processed glbs (will also return an array when just one surface has been processed)
9
+ * @returns {string} glbs[].name - Name of surface as defined in LandXML
10
+ * @returns {string} glbs[].description - Description of surface as defined in LandXML
11
+ * @returns {string} glbs[].sourceFile - Source file of where the LandXML was generated from.
12
+ * @returns {string} glbs[].timeStamp - Timestamp of when the surface was exported into LandXML
13
+ * @returns {Uint8Array} glbs[].glb - Uint8Array binary data of glb
14
+ * @returns {[number, number]} glbs[].center - Offset center of the processed glb
15
+ * @returns {function(): void} glbs[].download - Convenient way to download the GLB, within the filename the new center will be appended.
16
+ */
17
+ declare const toGlb: (landXmlString: string, center?: "auto" | "origin" | [
18
+ x: number,
19
+ y: number
20
+ ], surfaceId?: string | number) => Promise<{
21
+ name: string;
22
+ description: string;
23
+ sourceFile: string;
24
+ timeStamp: string;
25
+ glb: Uint8Array;
26
+ center: [x: number, y: number];
27
+ download: () => void;
28
+ }[]>;
29
+
30
+ /**
31
+ * @param landXmlString
32
+ * @param contourInterval Interval at which you would like to generate contour lines
33
+ * @param surfaceId Surface name or index if your LandXML contains multiple surfaces. By default all surfaces are converted and returns an array of glb Uint8Array
34
+ * @returns {Object[]} surfaceContours - Array of processed surface contours (will also return an array when just one surface has been processed)
35
+ * @returns {string} surfaceContours[].name - Name of surface as defined in LandXML
36
+ * @returns {string} surfaceContours[].description - Description of surface as defined in LandXML
37
+ * @returns {string} surfaceContours[].sourceFile - Source file of where the LandXML was generated from.
38
+ * @returns {string} surfaceContours[].timeStamp - Timestamp of when the surface was exported into LandXML
39
+ * @returns {string} surfaceContours[].wktString - WKT string of LandXML coordinate system projection
40
+ * @returns {Object} surfaceContours[].geojson - Geojson feature collection of contour lines
41
+ */
42
+ declare const toGeojsonContours: (landXmlString: string, contourInterval?: number, surfaceId?: string | number) => Promise<{
43
+ name: string;
44
+ description: string;
45
+ sourceFile: string;
46
+ timeStamp: string;
47
+ wktString?: string;
48
+ geojson: FeatureCollection<LineString, {
49
+ z: number;
50
+ }>;
51
+ }[]>;
52
+
53
+ /**
54
+ * @param geojson
55
+ * @param sourceProjection can be a proj4 string or WKT string, you will likely have a wkt string available with your LandXML if you used Civil 3D exporter and had your drawing geo-referenced
56
+ * @param targetProjection you will most likely want to use WGS84 for online viewing, however any other projection you might need can be used as long as it's valid
57
+ * @param keepOriginalGeometryAsFeatureProperty if you intend to repurpose the original geometry, it can be added to geojson feature properties
58
+ * @returns {FeatureCollection} Geojson FeatureCollection with updated geometry coordinates
59
+ */
60
+ declare const reprojectGeoJson: (geojson: FeatureCollection, sourceProjection: string, targetProjection?: string, keepOriginalGeometryAsFeatureProperty?: boolean) => FeatureCollection<geojson.Geometry, geojson.GeoJsonProperties>;
61
+
62
+ export { reprojectGeoJson, toGeojsonContours, toGlb };
package/dist/index.js CHANGED
@@ -1,8 +1,39 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
4
+ var __defProps = Object.defineProperties;
3
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
7
  var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
9
+ var __getProtoOf = Object.getPrototypeOf;
5
10
  var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
12
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
13
+ var __spreadValues = (a, b) => {
14
+ for (var prop in b || (b = {}))
15
+ if (__hasOwnProp.call(b, prop))
16
+ __defNormalProp(a, prop, b[prop]);
17
+ if (__getOwnPropSymbols)
18
+ for (var prop of __getOwnPropSymbols(b)) {
19
+ if (__propIsEnum.call(b, prop))
20
+ __defNormalProp(a, prop, b[prop]);
21
+ }
22
+ return a;
23
+ };
24
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
25
+ var __objRest = (source, exclude) => {
26
+ var target = {};
27
+ for (var prop in source)
28
+ if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
29
+ target[prop] = source[prop];
30
+ if (source != null && __getOwnPropSymbols)
31
+ for (var prop of __getOwnPropSymbols(source)) {
32
+ if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
33
+ target[prop] = source[prop];
34
+ }
35
+ return target;
36
+ };
6
37
  var __export = (target, all) => {
7
38
  for (var name in all)
8
39
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -15,14 +46,350 @@ var __copyProps = (to, from, except, desc) => {
15
46
  }
16
47
  return to;
17
48
  };
49
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
50
+ // If the importer is in node compatibility mode or this is not an ESM
51
+ // file that has been converted to a CommonJS file using a Babel-
52
+ // compatible transform (i.e. "__esModule" has not been set), then set
53
+ // "default" to the CommonJS "module.exports" for node compatibility.
54
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
55
+ mod
56
+ ));
18
57
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
58
+ var __async = (__this, __arguments, generator) => {
59
+ return new Promise((resolve, reject) => {
60
+ var fulfilled = (value) => {
61
+ try {
62
+ step(generator.next(value));
63
+ } catch (e) {
64
+ reject(e);
65
+ }
66
+ };
67
+ var rejected = (value) => {
68
+ try {
69
+ step(generator.throw(value));
70
+ } catch (e) {
71
+ reject(e);
72
+ }
73
+ };
74
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
75
+ step((generator = generator.apply(__this, __arguments)).next());
76
+ });
77
+ };
19
78
 
20
79
  // src/index.ts
21
80
  var src_exports = {};
22
81
  __export(src_exports, {
23
- default: () => src_default
82
+ reprojectGeoJson: () => reproject_geojson_default,
83
+ toGeojsonContours: () => to_geojson_contours_default,
84
+ toGlb: () => to_glb_default
24
85
  });
25
86
  module.exports = __toCommonJS(src_exports);
26
- var src_default = () => {
27
- console.log("Hello world");
87
+
88
+ // src/private/filter-by-surfaceId.ts
89
+ var filterBySurfaceId = (parsedSurfaces, surfaceId) => {
90
+ let filtered = [...parsedSurfaces];
91
+ if (typeof surfaceId === "string") {
92
+ filtered = filtered.filter((s) => s.name === surfaceId);
93
+ if (filtered.length === 0)
94
+ throw "Provided SurfaceId doesn't exist within provided LandXML";
95
+ }
96
+ if (typeof surfaceId === "number" && surfaceId > 0) {
97
+ if (!filtered[surfaceId])
98
+ throw `Provided SurfaceId index is out of range. Provided LandXML has ${filtered.length} surfaces.`;
99
+ filtered = [filtered[surfaceId]];
100
+ }
101
+ return filtered;
102
+ };
103
+ var filter_by_surfaceId_default = filterBySurfaceId;
104
+
105
+ // src/private/get-glb.ts
106
+ var import_core = require("@gltf-transform/core");
107
+ var findXYAxisMedians = (vertices) => {
108
+ var _a, _b;
109
+ vertices = vertices.slice().filter(Boolean);
110
+ const middleIndex = Math.floor(vertices.length / 2);
111
+ const medianX = (_a = vertices.slice().sort((a, b) => a[0] - b[0])[middleIndex]) == null ? void 0 : _a[0];
112
+ const medianY = (_b = vertices.slice().sort((a, b) => a[1] - b[1])[middleIndex]) == null ? void 0 : _b[1];
113
+ return [medianX, medianY];
28
114
  };
115
+ var getGlb = (data, customCenter) => __async(void 0, null, function* () {
116
+ const center = customCenter || findXYAxisMedians(data.surfaceDefinition.points);
117
+ const vertices = [...data.surfaceDefinition.points].map((p) => p.slice()).map(([x, y, z]) => {
118
+ return [x - center[0], z, -(y - center[1])];
119
+ }).reduce((prev, curr) => prev.concat(curr), []);
120
+ const triangles = data.surfaceDefinition.faces.reduce((prev, curr) => prev.concat(curr), []);
121
+ const doc = new import_core.Document();
122
+ const buffer = doc.createBuffer();
123
+ const position = doc.createAccessor().setType("VEC3").setArray(new Float32Array(vertices)).setBuffer(buffer);
124
+ const indices = doc.createAccessor().setType("SCALAR").setArray(new Uint32Array(triangles)).setBuffer(buffer);
125
+ const prim = doc.createPrimitive().setAttribute("POSITION", position).setIndices(indices);
126
+ const mesh = doc.createMesh().addPrimitive(prim);
127
+ const node = doc.createNode().setMesh(mesh);
128
+ const scene = doc.createScene().addChild(node);
129
+ const glb = yield new import_core.WebIO().writeBinary(doc);
130
+ return { glb, center };
131
+ });
132
+ var get_glb_default = getGlb;
133
+
134
+ // src/private/download-glb.ts
135
+ var downloadGlb = (glbData, fileName) => {
136
+ const blobUrl = URL.createObjectURL(new Blob([glbData], { type: "model/gltf-binary" }));
137
+ const link = document.createElement("a");
138
+ link.href = blobUrl;
139
+ link.download = fileName;
140
+ document.body.appendChild(link);
141
+ link.click();
142
+ document.body.removeChild(link);
143
+ URL.revokeObjectURL(blobUrl);
144
+ };
145
+ var download_glb_default = downloadGlb;
146
+
147
+ // src/private/parse-xml.ts
148
+ var import_xml_js = __toESM(require("xml-js"));
149
+ var parseXML = (xmlString) => __async(void 0, null, function* () {
150
+ const { LandXML } = import_xml_js.default.xml2js(xmlString, {
151
+ compact: true,
152
+ attributesKey: "attr",
153
+ textKey: "content"
154
+ });
155
+ const landXML_surfaces = Array.isArray(LandXML.Surfaces.Surface) ? LandXML.Surfaces.Surface : [LandXML.Surfaces.Surface];
156
+ return landXML_surfaces.map((surface) => {
157
+ var _a, _b;
158
+ let points = [];
159
+ let faces = [];
160
+ let pointIdMap = {};
161
+ surface.Definition.Pnts.P.forEach((pt) => {
162
+ const { attr, content } = pt;
163
+ const [y, x, z] = content.split(" ").map((v) => parseFloat(v));
164
+ points.push([x, y, z]);
165
+ pointIdMap[attr.id] = points.length - 1;
166
+ }, []);
167
+ faces = surface.Definition.Faces.F.map((face) => {
168
+ const { content } = face;
169
+ const [a, b, c] = content.split(" ").map((v) => pointIdMap[v]);
170
+ if ([a, b, c].filter((v) => typeof v === "undefined").length > 0) {
171
+ throw `Invalid LandXML. A face is referencing a point that doesn't exist. Face is referencing points: ${content}`;
172
+ }
173
+ return [a, b, c];
174
+ });
175
+ return {
176
+ sourceFile: LandXML.Project.attr.name,
177
+ timeStamp: LandXML.Application.attr.timeStamp,
178
+ name: surface.attr.name,
179
+ description: surface.attr.desc,
180
+ wktString: (_b = (_a = LandXML.CoordinateSystem) == null ? void 0 : _a.attr) == null ? void 0 : _b.ogcWktCode,
181
+ surfaceDefinition: {
182
+ points,
183
+ faces
184
+ }
185
+ };
186
+ });
187
+ });
188
+ var parse_xml_default = parseXML;
189
+
190
+ // src/public/to-glb.ts
191
+ var toGlb = (landXmlString, center = "auto", surfaceId = -1) => __async(void 0, null, function* () {
192
+ const requestedCenter = center == "origin" ? [0, 0] : center === "auto" ? void 0 : center;
193
+ let requestedParsedSurfaces = filter_by_surfaceId_default(yield parse_xml_default(landXmlString), surfaceId);
194
+ const glbs = yield Promise.all(
195
+ requestedParsedSurfaces.map(
196
+ (surface) => new Promise((resolve, reject) => __async(void 0, null, function* () {
197
+ try {
198
+ const { glb, center: center2 } = yield get_glb_default(surface, requestedCenter);
199
+ const _a = surface, { surfaceDefinition } = _a, rest = __objRest(_a, ["surfaceDefinition"]);
200
+ resolve(__spreadProps(__spreadValues({}, rest), {
201
+ glb,
202
+ center: center2,
203
+ download: () => {
204
+ download_glb_default(glb, surface.name.replace(/\.xml$/, `${JSON.stringify(center2)}.glb`));
205
+ }
206
+ }));
207
+ } catch (e) {
208
+ reject(e);
209
+ }
210
+ }))
211
+ )
212
+ );
213
+ return glbs;
214
+ });
215
+ var to_glb_default = toGlb;
216
+
217
+ // src/private/get-contours.ts
218
+ var contourLineOnFace = (face, z) => {
219
+ let line = [];
220
+ for (let i = 0; i < face.length; i++) {
221
+ let vertex1 = face[i];
222
+ let vertex2 = face[(i + 1) % face.length];
223
+ if (vertex1[2] <= z && vertex2[2] >= z || vertex1[2] >= z && vertex2[2] <= z) {
224
+ let t = (z - vertex1[2]) / (vertex2[2] - vertex1[2]);
225
+ line.push([vertex1[0] + t * (vertex2[0] - vertex1[0]), vertex1[1] + t * (vertex2[1] - vertex1[1])]);
226
+ }
227
+ }
228
+ return line.length > 0 ? line : void 0;
229
+ };
230
+ var linesToPolyLines = (lineSegments) => {
231
+ if (!Array.isArray(lineSegments) || lineSegments.length === 0) {
232
+ throw new Error("Invalid input: Please provide a non-empty array of line segments.");
233
+ }
234
+ const segmentsMap = /* @__PURE__ */ new Map();
235
+ const polylines = [];
236
+ lineSegments.forEach(([start, end]) => {
237
+ const startKey = start.join(",");
238
+ const endKey = end.join(",");
239
+ segmentsMap.set(startKey, segmentsMap.get(startKey) || []);
240
+ segmentsMap.set(endKey, segmentsMap.get(endKey) || []);
241
+ segmentsMap.get(startKey).push(end);
242
+ segmentsMap.get(endKey).push(start);
243
+ });
244
+ const constructPolyline = (start, polyline) => {
245
+ let current = start;
246
+ while (segmentsMap.has(current.join(",")) && segmentsMap.get(current.join(",")).length > 0) {
247
+ const next = segmentsMap.get(current.join(",")).pop();
248
+ polyline.push(current);
249
+ if (next) {
250
+ const nextKey = next.join(",");
251
+ segmentsMap.delete(current.join(","));
252
+ current = next;
253
+ if (segmentsMap.has(nextKey)) {
254
+ segmentsMap.set(
255
+ nextKey,
256
+ (segmentsMap.get(nextKey) || []).filter((point) => point.join(",") !== current.join(","))
257
+ );
258
+ }
259
+ }
260
+ }
261
+ polyline.push(current);
262
+ return polyline;
263
+ };
264
+ const traverseSegmentsMap = (endpoints, startKey) => {
265
+ let start = startKey.split(",").map(Number);
266
+ while (endpoints.length > 0) {
267
+ const newPolyline = constructPolyline(start, []);
268
+ polylines.push(newPolyline);
269
+ start = endpoints.pop();
270
+ }
271
+ };
272
+ for (const [startKey, endpoints] of segmentsMap.entries()) {
273
+ if (endpoints && endpoints.length === 0) {
274
+ continue;
275
+ }
276
+ traverseSegmentsMap(endpoints || [], startKey);
277
+ }
278
+ return polylines;
279
+ };
280
+ var contourElevations = (minElevation, maxElevation, increment) => {
281
+ const elevations = [];
282
+ const start = Math.ceil(minElevation * 2) / 2;
283
+ const end = Math.floor(maxElevation * 2) / 2;
284
+ for (let elevation = start; elevation <= end; elevation += increment) {
285
+ elevations.push(elevation);
286
+ }
287
+ return elevations;
288
+ };
289
+ var constructGeojson = (elevationData) => {
290
+ const features = elevationData.reduce((prev, data) => {
291
+ const { elevation, polylines } = data;
292
+ return prev.concat(
293
+ polylines.map((polyline) => ({
294
+ type: "Feature",
295
+ geometry: {
296
+ type: "LineString",
297
+ coordinates: polyline
298
+ },
299
+ properties: {
300
+ z: elevation
301
+ }
302
+ }))
303
+ );
304
+ }, []);
305
+ return {
306
+ type: "FeatureCollection",
307
+ features
308
+ };
309
+ };
310
+ var getContours = (data, interval = 2) => __async(void 0, null, function* () {
311
+ const triangles = data.surfaceDefinition.faces.map(
312
+ (face) => face.map((vert) => data.surfaceDefinition.points[vert])
313
+ );
314
+ const [minElevation, maxElevation] = data.surfaceDefinition.points.reduce(
315
+ ([prevMin, prevMax], curr) => [Math.min(prevMin, curr[2]), Math.max(prevMax, curr[2])],
316
+ [Infinity, -Infinity]
317
+ );
318
+ const elevations = contourElevations(minElevation, maxElevation, interval);
319
+ const elevationPolylines = elevations.map((e) => ({
320
+ elevation: e,
321
+ polylines: linesToPolyLines(
322
+ triangles.reduce((prev, curr) => {
323
+ const line = contourLineOnFace(curr, e);
324
+ if (line)
325
+ prev.push(line);
326
+ return prev;
327
+ }, [])
328
+ )
329
+ }));
330
+ return constructGeojson(elevationPolylines);
331
+ });
332
+ var get_contours_default = getContours;
333
+
334
+ // src/public/to-geojson-contours.ts
335
+ var toGeojsonContours = (landXmlString, contourInterval = 2, surfaceId = -1) => __async(void 0, null, function* () {
336
+ let requestedParsedSurfaces = filter_by_surfaceId_default(yield parse_xml_default(landXmlString), surfaceId);
337
+ const contours = yield Promise.all(
338
+ requestedParsedSurfaces.map(
339
+ (surface) => new Promise((resolve, reject) => __async(void 0, null, function* () {
340
+ try {
341
+ const geojson = yield get_contours_default(surface, contourInterval);
342
+ const _a = surface, { surfaceDefinition } = _a, rest = __objRest(_a, ["surfaceDefinition"]);
343
+ resolve(__spreadProps(__spreadValues({}, rest), {
344
+ geojson
345
+ }));
346
+ } catch (e) {
347
+ reject(e);
348
+ }
349
+ }))
350
+ )
351
+ );
352
+ return contours;
353
+ });
354
+ var to_geojson_contours_default = toGeojsonContours;
355
+
356
+ // src/public/reproject-geojson.ts
357
+ var import_proj4 = __toESM(require("proj4"));
358
+ var reprojectGeoJson = (geojson, sourceProjection, targetProjection = "WGS84", keepOriginalGeometryAsFeatureProperty = true) => {
359
+ const transformCoordinates = (coordinates, sourceProjection2, targetProjection2) => {
360
+ if (Array.isArray(coordinates[0])) {
361
+ coordinates = coordinates.map(
362
+ (subCoordinates) => transformCoordinates(subCoordinates, sourceProjection2, targetProjection2)
363
+ );
364
+ } else {
365
+ coordinates = (0, import_proj4.default)(sourceProjection2, targetProjection2, coordinates);
366
+ }
367
+ return coordinates;
368
+ };
369
+ if (!geojson || !geojson.features || !Array.isArray(geojson.features) || !sourceProjection) {
370
+ throw new Error("Invalid GeoJSON or source projection.");
371
+ }
372
+ geojson.features.forEach((feature) => {
373
+ if (keepOriginalGeometryAsFeatureProperty)
374
+ feature.properties = feature.properties || {};
375
+ if (feature.geometry) {
376
+ if (keepOriginalGeometryAsFeatureProperty && feature.properties)
377
+ feature.properties._rawGeometry = __spreadValues({}, feature.geometry);
378
+ if (sourceProjection !== targetProjection) {
379
+ feature.geometry.coordinates = transformCoordinates(
380
+ feature.geometry.coordinates,
381
+ sourceProjection,
382
+ targetProjection
383
+ );
384
+ }
385
+ }
386
+ });
387
+ return geojson;
388
+ };
389
+ var reproject_geojson_default = reprojectGeoJson;
390
+ // Annotate the CommonJS export names for ESM import in node:
391
+ 0 && (module.exports = {
392
+ reprojectGeoJson,
393
+ toGeojsonContours,
394
+ toGlb
395
+ });
package/dist/index.mjs CHANGED
@@ -1,7 +1,359 @@
1
- // src/index.ts
2
- var src_default = () => {
3
- console.log("Hello world");
1
+ var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __spreadValues = (a, b) => {
9
+ for (var prop in b || (b = {}))
10
+ if (__hasOwnProp.call(b, prop))
11
+ __defNormalProp(a, prop, b[prop]);
12
+ if (__getOwnPropSymbols)
13
+ for (var prop of __getOwnPropSymbols(b)) {
14
+ if (__propIsEnum.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ }
17
+ return a;
4
18
  };
19
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
+ var __objRest = (source, exclude) => {
21
+ var target = {};
22
+ for (var prop in source)
23
+ if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
24
+ target[prop] = source[prop];
25
+ if (source != null && __getOwnPropSymbols)
26
+ for (var prop of __getOwnPropSymbols(source)) {
27
+ if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
28
+ target[prop] = source[prop];
29
+ }
30
+ return target;
31
+ };
32
+ var __async = (__this, __arguments, generator) => {
33
+ return new Promise((resolve, reject) => {
34
+ var fulfilled = (value) => {
35
+ try {
36
+ step(generator.next(value));
37
+ } catch (e) {
38
+ reject(e);
39
+ }
40
+ };
41
+ var rejected = (value) => {
42
+ try {
43
+ step(generator.throw(value));
44
+ } catch (e) {
45
+ reject(e);
46
+ }
47
+ };
48
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
49
+ step((generator = generator.apply(__this, __arguments)).next());
50
+ });
51
+ };
52
+
53
+ // src/private/filter-by-surfaceId.ts
54
+ var filterBySurfaceId = (parsedSurfaces, surfaceId) => {
55
+ let filtered = [...parsedSurfaces];
56
+ if (typeof surfaceId === "string") {
57
+ filtered = filtered.filter((s) => s.name === surfaceId);
58
+ if (filtered.length === 0)
59
+ throw "Provided SurfaceId doesn't exist within provided LandXML";
60
+ }
61
+ if (typeof surfaceId === "number" && surfaceId > 0) {
62
+ if (!filtered[surfaceId])
63
+ throw `Provided SurfaceId index is out of range. Provided LandXML has ${filtered.length} surfaces.`;
64
+ filtered = [filtered[surfaceId]];
65
+ }
66
+ return filtered;
67
+ };
68
+ var filter_by_surfaceId_default = filterBySurfaceId;
69
+
70
+ // src/private/get-glb.ts
71
+ import { Document, WebIO } from "@gltf-transform/core";
72
+ var findXYAxisMedians = (vertices) => {
73
+ var _a, _b;
74
+ vertices = vertices.slice().filter(Boolean);
75
+ const middleIndex = Math.floor(vertices.length / 2);
76
+ const medianX = (_a = vertices.slice().sort((a, b) => a[0] - b[0])[middleIndex]) == null ? void 0 : _a[0];
77
+ const medianY = (_b = vertices.slice().sort((a, b) => a[1] - b[1])[middleIndex]) == null ? void 0 : _b[1];
78
+ return [medianX, medianY];
79
+ };
80
+ var getGlb = (data, customCenter) => __async(void 0, null, function* () {
81
+ const center = customCenter || findXYAxisMedians(data.surfaceDefinition.points);
82
+ const vertices = [...data.surfaceDefinition.points].map((p) => p.slice()).map(([x, y, z]) => {
83
+ return [x - center[0], z, -(y - center[1])];
84
+ }).reduce((prev, curr) => prev.concat(curr), []);
85
+ const triangles = data.surfaceDefinition.faces.reduce((prev, curr) => prev.concat(curr), []);
86
+ const doc = new Document();
87
+ const buffer = doc.createBuffer();
88
+ const position = doc.createAccessor().setType("VEC3").setArray(new Float32Array(vertices)).setBuffer(buffer);
89
+ const indices = doc.createAccessor().setType("SCALAR").setArray(new Uint32Array(triangles)).setBuffer(buffer);
90
+ const prim = doc.createPrimitive().setAttribute("POSITION", position).setIndices(indices);
91
+ const mesh = doc.createMesh().addPrimitive(prim);
92
+ const node = doc.createNode().setMesh(mesh);
93
+ const scene = doc.createScene().addChild(node);
94
+ const glb = yield new WebIO().writeBinary(doc);
95
+ return { glb, center };
96
+ });
97
+ var get_glb_default = getGlb;
98
+
99
+ // src/private/download-glb.ts
100
+ var downloadGlb = (glbData, fileName) => {
101
+ const blobUrl = URL.createObjectURL(new Blob([glbData], { type: "model/gltf-binary" }));
102
+ const link = document.createElement("a");
103
+ link.href = blobUrl;
104
+ link.download = fileName;
105
+ document.body.appendChild(link);
106
+ link.click();
107
+ document.body.removeChild(link);
108
+ URL.revokeObjectURL(blobUrl);
109
+ };
110
+ var download_glb_default = downloadGlb;
111
+
112
+ // src/private/parse-xml.ts
113
+ import convert from "xml-js";
114
+ var parseXML = (xmlString) => __async(void 0, null, function* () {
115
+ const { LandXML } = convert.xml2js(xmlString, {
116
+ compact: true,
117
+ attributesKey: "attr",
118
+ textKey: "content"
119
+ });
120
+ const landXML_surfaces = Array.isArray(LandXML.Surfaces.Surface) ? LandXML.Surfaces.Surface : [LandXML.Surfaces.Surface];
121
+ return landXML_surfaces.map((surface) => {
122
+ var _a, _b;
123
+ let points = [];
124
+ let faces = [];
125
+ let pointIdMap = {};
126
+ surface.Definition.Pnts.P.forEach((pt) => {
127
+ const { attr, content } = pt;
128
+ const [y, x, z] = content.split(" ").map((v) => parseFloat(v));
129
+ points.push([x, y, z]);
130
+ pointIdMap[attr.id] = points.length - 1;
131
+ }, []);
132
+ faces = surface.Definition.Faces.F.map((face) => {
133
+ const { content } = face;
134
+ const [a, b, c] = content.split(" ").map((v) => pointIdMap[v]);
135
+ if ([a, b, c].filter((v) => typeof v === "undefined").length > 0) {
136
+ throw `Invalid LandXML. A face is referencing a point that doesn't exist. Face is referencing points: ${content}`;
137
+ }
138
+ return [a, b, c];
139
+ });
140
+ return {
141
+ sourceFile: LandXML.Project.attr.name,
142
+ timeStamp: LandXML.Application.attr.timeStamp,
143
+ name: surface.attr.name,
144
+ description: surface.attr.desc,
145
+ wktString: (_b = (_a = LandXML.CoordinateSystem) == null ? void 0 : _a.attr) == null ? void 0 : _b.ogcWktCode,
146
+ surfaceDefinition: {
147
+ points,
148
+ faces
149
+ }
150
+ };
151
+ });
152
+ });
153
+ var parse_xml_default = parseXML;
154
+
155
+ // src/public/to-glb.ts
156
+ var toGlb = (landXmlString, center = "auto", surfaceId = -1) => __async(void 0, null, function* () {
157
+ const requestedCenter = center == "origin" ? [0, 0] : center === "auto" ? void 0 : center;
158
+ let requestedParsedSurfaces = filter_by_surfaceId_default(yield parse_xml_default(landXmlString), surfaceId);
159
+ const glbs = yield Promise.all(
160
+ requestedParsedSurfaces.map(
161
+ (surface) => new Promise((resolve, reject) => __async(void 0, null, function* () {
162
+ try {
163
+ const { glb, center: center2 } = yield get_glb_default(surface, requestedCenter);
164
+ const _a = surface, { surfaceDefinition } = _a, rest = __objRest(_a, ["surfaceDefinition"]);
165
+ resolve(__spreadProps(__spreadValues({}, rest), {
166
+ glb,
167
+ center: center2,
168
+ download: () => {
169
+ download_glb_default(glb, surface.name.replace(/\.xml$/, `${JSON.stringify(center2)}.glb`));
170
+ }
171
+ }));
172
+ } catch (e) {
173
+ reject(e);
174
+ }
175
+ }))
176
+ )
177
+ );
178
+ return glbs;
179
+ });
180
+ var to_glb_default = toGlb;
181
+
182
+ // src/private/get-contours.ts
183
+ var contourLineOnFace = (face, z) => {
184
+ let line = [];
185
+ for (let i = 0; i < face.length; i++) {
186
+ let vertex1 = face[i];
187
+ let vertex2 = face[(i + 1) % face.length];
188
+ if (vertex1[2] <= z && vertex2[2] >= z || vertex1[2] >= z && vertex2[2] <= z) {
189
+ let t = (z - vertex1[2]) / (vertex2[2] - vertex1[2]);
190
+ line.push([vertex1[0] + t * (vertex2[0] - vertex1[0]), vertex1[1] + t * (vertex2[1] - vertex1[1])]);
191
+ }
192
+ }
193
+ return line.length > 0 ? line : void 0;
194
+ };
195
+ var linesToPolyLines = (lineSegments) => {
196
+ if (!Array.isArray(lineSegments) || lineSegments.length === 0) {
197
+ throw new Error("Invalid input: Please provide a non-empty array of line segments.");
198
+ }
199
+ const segmentsMap = /* @__PURE__ */ new Map();
200
+ const polylines = [];
201
+ lineSegments.forEach(([start, end]) => {
202
+ const startKey = start.join(",");
203
+ const endKey = end.join(",");
204
+ segmentsMap.set(startKey, segmentsMap.get(startKey) || []);
205
+ segmentsMap.set(endKey, segmentsMap.get(endKey) || []);
206
+ segmentsMap.get(startKey).push(end);
207
+ segmentsMap.get(endKey).push(start);
208
+ });
209
+ const constructPolyline = (start, polyline) => {
210
+ let current = start;
211
+ while (segmentsMap.has(current.join(",")) && segmentsMap.get(current.join(",")).length > 0) {
212
+ const next = segmentsMap.get(current.join(",")).pop();
213
+ polyline.push(current);
214
+ if (next) {
215
+ const nextKey = next.join(",");
216
+ segmentsMap.delete(current.join(","));
217
+ current = next;
218
+ if (segmentsMap.has(nextKey)) {
219
+ segmentsMap.set(
220
+ nextKey,
221
+ (segmentsMap.get(nextKey) || []).filter((point) => point.join(",") !== current.join(","))
222
+ );
223
+ }
224
+ }
225
+ }
226
+ polyline.push(current);
227
+ return polyline;
228
+ };
229
+ const traverseSegmentsMap = (endpoints, startKey) => {
230
+ let start = startKey.split(",").map(Number);
231
+ while (endpoints.length > 0) {
232
+ const newPolyline = constructPolyline(start, []);
233
+ polylines.push(newPolyline);
234
+ start = endpoints.pop();
235
+ }
236
+ };
237
+ for (const [startKey, endpoints] of segmentsMap.entries()) {
238
+ if (endpoints && endpoints.length === 0) {
239
+ continue;
240
+ }
241
+ traverseSegmentsMap(endpoints || [], startKey);
242
+ }
243
+ return polylines;
244
+ };
245
+ var contourElevations = (minElevation, maxElevation, increment) => {
246
+ const elevations = [];
247
+ const start = Math.ceil(minElevation * 2) / 2;
248
+ const end = Math.floor(maxElevation * 2) / 2;
249
+ for (let elevation = start; elevation <= end; elevation += increment) {
250
+ elevations.push(elevation);
251
+ }
252
+ return elevations;
253
+ };
254
+ var constructGeojson = (elevationData) => {
255
+ const features = elevationData.reduce((prev, data) => {
256
+ const { elevation, polylines } = data;
257
+ return prev.concat(
258
+ polylines.map((polyline) => ({
259
+ type: "Feature",
260
+ geometry: {
261
+ type: "LineString",
262
+ coordinates: polyline
263
+ },
264
+ properties: {
265
+ z: elevation
266
+ }
267
+ }))
268
+ );
269
+ }, []);
270
+ return {
271
+ type: "FeatureCollection",
272
+ features
273
+ };
274
+ };
275
+ var getContours = (data, interval = 2) => __async(void 0, null, function* () {
276
+ const triangles = data.surfaceDefinition.faces.map(
277
+ (face) => face.map((vert) => data.surfaceDefinition.points[vert])
278
+ );
279
+ const [minElevation, maxElevation] = data.surfaceDefinition.points.reduce(
280
+ ([prevMin, prevMax], curr) => [Math.min(prevMin, curr[2]), Math.max(prevMax, curr[2])],
281
+ [Infinity, -Infinity]
282
+ );
283
+ const elevations = contourElevations(minElevation, maxElevation, interval);
284
+ const elevationPolylines = elevations.map((e) => ({
285
+ elevation: e,
286
+ polylines: linesToPolyLines(
287
+ triangles.reduce((prev, curr) => {
288
+ const line = contourLineOnFace(curr, e);
289
+ if (line)
290
+ prev.push(line);
291
+ return prev;
292
+ }, [])
293
+ )
294
+ }));
295
+ return constructGeojson(elevationPolylines);
296
+ });
297
+ var get_contours_default = getContours;
298
+
299
+ // src/public/to-geojson-contours.ts
300
+ var toGeojsonContours = (landXmlString, contourInterval = 2, surfaceId = -1) => __async(void 0, null, function* () {
301
+ let requestedParsedSurfaces = filter_by_surfaceId_default(yield parse_xml_default(landXmlString), surfaceId);
302
+ const contours = yield Promise.all(
303
+ requestedParsedSurfaces.map(
304
+ (surface) => new Promise((resolve, reject) => __async(void 0, null, function* () {
305
+ try {
306
+ const geojson = yield get_contours_default(surface, contourInterval);
307
+ const _a = surface, { surfaceDefinition } = _a, rest = __objRest(_a, ["surfaceDefinition"]);
308
+ resolve(__spreadProps(__spreadValues({}, rest), {
309
+ geojson
310
+ }));
311
+ } catch (e) {
312
+ reject(e);
313
+ }
314
+ }))
315
+ )
316
+ );
317
+ return contours;
318
+ });
319
+ var to_geojson_contours_default = toGeojsonContours;
320
+
321
+ // src/public/reproject-geojson.ts
322
+ import proj4 from "proj4";
323
+ var reprojectGeoJson = (geojson, sourceProjection, targetProjection = "WGS84", keepOriginalGeometryAsFeatureProperty = true) => {
324
+ const transformCoordinates = (coordinates, sourceProjection2, targetProjection2) => {
325
+ if (Array.isArray(coordinates[0])) {
326
+ coordinates = coordinates.map(
327
+ (subCoordinates) => transformCoordinates(subCoordinates, sourceProjection2, targetProjection2)
328
+ );
329
+ } else {
330
+ coordinates = proj4(sourceProjection2, targetProjection2, coordinates);
331
+ }
332
+ return coordinates;
333
+ };
334
+ if (!geojson || !geojson.features || !Array.isArray(geojson.features) || !sourceProjection) {
335
+ throw new Error("Invalid GeoJSON or source projection.");
336
+ }
337
+ geojson.features.forEach((feature) => {
338
+ if (keepOriginalGeometryAsFeatureProperty)
339
+ feature.properties = feature.properties || {};
340
+ if (feature.geometry) {
341
+ if (keepOriginalGeometryAsFeatureProperty && feature.properties)
342
+ feature.properties._rawGeometry = __spreadValues({}, feature.geometry);
343
+ if (sourceProjection !== targetProjection) {
344
+ feature.geometry.coordinates = transformCoordinates(
345
+ feature.geometry.coordinates,
346
+ sourceProjection,
347
+ targetProjection
348
+ );
349
+ }
350
+ }
351
+ });
352
+ return geojson;
353
+ };
354
+ var reproject_geojson_default = reprojectGeoJson;
5
355
  export {
6
- src_default as default
356
+ reproject_geojson_default as reprojectGeoJson,
357
+ to_geojson_contours_default as toGeojsonContours,
358
+ to_glb_default as toGlb
7
359
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "landxml",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "description": "Parse LandXML surfaces on the modern web.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -23,9 +23,16 @@
23
23
  "license": "MIT",
24
24
  "devDependencies": {
25
25
  "@changesets/cli": "^2.26.2",
26
+ "@types/geojson": "^7946.0.13",
27
+ "@types/proj4": "^2.5.5",
28
+ "@types/xml2json": "^0.11.6",
26
29
  "tsup": "^8.0.0",
27
30
  "typescript": "^5.2.2",
28
31
  "vitest": "^0.34.6"
29
32
  },
30
- "dependencies": {}
33
+ "dependencies": {
34
+ "@gltf-transform/core": "^3.9.0",
35
+ "proj4": "^2.9.2",
36
+ "xml-js": "^1.6.11"
37
+ }
31
38
  }