Skip to content

Commit ad8dd0a

Browse files
committed
implemented web workers for performance
1 parent a07e7ea commit ad8dd0a

16 files changed

+580023
-473
lines changed

package-lock.json

+1,460-403
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "landxml",
3+
"type": "module",
34
"version": "0.5.2",
45
"description": "Parse LandXML surfaces on the modern web.",
56
"main": "dist/index.js",
@@ -12,8 +13,8 @@
1213
"url": "https://github.com/abrman/landxml"
1314
},
1415
"scripts": {
15-
"dev": "vitest",
16-
"test": "vitest run",
16+
"dev": "vitest --config ./vitest.config.ts --slow-test-threshold=0",
17+
"test": "vitest --config ./vitest.config.ts run",
1718
"build": "tsup src/index.ts --format cjs,esm --dts",
1819
"lint": "tsc",
1920
"ci": "npm run lint && npm run test && npm run build",
@@ -30,14 +31,20 @@
3031
"@changesets/cli": "^2.26.2",
3132
"@types/geojson": "^7946.0.13",
3233
"@types/proj4": "^2.5.5",
34+
"@types/sax": "^1.2.7",
3335
"@types/xml2json": "^0.11.6",
36+
"jsdom": "^24.0.0",
3437
"tsup": "^8.0.0",
3538
"typescript": "^5.2.2",
36-
"vitest": "^0.34.6"
39+
"vitest": "^1.6.0"
3740
},
3841
"dependencies": {
3942
"@gltf-transform/core": "^3.9.0",
43+
"@vitest/web-worker": "^1.6.0",
44+
"easy-web-worker": "^6.2.0",
4045
"proj4": "^2.9.2",
41-
"xml-js": "^1.6.11"
46+
"sax": "^1.3.0",
47+
"xml-js": "^1.6.11",
48+
"xml2json": "^0.12.0"
4249
}
4350
}

src/easyWebWorkerMock.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import EasyWebWorker, { createEasyWebWorker } from "easy-web-worker";
3+
4+
// Doesn't support multi-threading
5+
const fakeCreateEasyWebWorker = (callback: (onMessage: any) => void) => ({
6+
send: (payload: any) =>
7+
new Promise(async (resolve, reject) => {
8+
const onMessage = (callback: ({ payload, resolve }: { payload: any; resolve: any }) => void) => {
9+
callback({ payload, resolve });
10+
};
11+
callback({ onMessage });
12+
}),
13+
});
14+
15+
vi.mock("easy-web-worker", () => {
16+
return {
17+
createEasyWebWorker: vi.fn().mockImplementation(fakeCreateEasyWebWorker),
18+
};
19+
});

src/index.test.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { describe, expect, it } from "vitest";
1+
import { describe, expect, it, vi } from "vitest";
22
import parseXML from "./private/parse-xml";
33
import getGlb from "./private/get-glb";
44
import getContours, { linesToPolyLines, contourElevations, contourLineOnFace } from "./private/get-contours";
55
import toGeojsonContours from "./public/to-geojson-contours";
66
import reprojectGeoJson from "./public/reproject-geojson";
7+
import fs from "fs";
78

89
const example_single_surface_landxml_with_sourcedata_breakline = `<?xml version="1.0"?>
910
<LandXML xmlns="http://www.landxml.org/schema/LandXML-1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.landxml.org/schema/LandXML-1.2 http://www.landxml.org/schema/LandXML-1.2/LandXML-1.2.xsd" date="2023-11-20" time="12:40:44" version="1.2" language="English" readOnly="false">
@@ -231,4 +232,20 @@ describe("Parse LandXMLs", () => {
231232
[0.4, 0.6, 0.8, 1, 1.2, 1.4, 1.6, 1.8, 2].map(twoDecimalNumberString)
232233
);
233234
});
235+
236+
it("Large file test", async () => {
237+
const n = 5; // size of the landXML to process
238+
const landXmlString = fs.readFileSync(`./src/test_assets/S${n}.xml`, { encoding: "utf-8" });
239+
let rawContours = await toGeojsonContours(landXmlString, 1);
240+
const projection =
241+
"+proj=utm +zone=16 +ellps=GRS80 +towgs84=-0.9738,1.9453,0.5486,-1.3357e-07,-4.872e-08,-5.507e-08,0 +units=m +no_defs +type=crs"; // any
242+
243+
if (rawContours[0]) {
244+
const geojson = reprojectGeoJson(rawContours[0].geojson, projection, "WGS84", true);
245+
// fs.writeFileSync(`./src/test_assets/S${n}_result.json`, JSON.stringify(geojson));
246+
}
247+
});
248+
{
249+
timeout: -1;
250+
}
234251
});

src/private/get-contours.ts

+131-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,129 @@
11
import type { Feature, FeatureCollection, LineString, Position } from "geojson";
22
import type { ParsedSurface } from "./parse-xml";
3+
import { createEasyWebWorker } from "easy-web-worker";
4+
5+
const contoursWorker = createEasyWebWorker<
6+
{
7+
triangles: [x: number, y: number, z: number][][];
8+
elevation: number;
9+
},
10+
{
11+
elevation: number;
12+
polylines: [number, number][][];
13+
}
14+
>(
15+
({ onMessage }) => {
16+
const contourLineOnFace = (face: [x: number, y: number, z: number][], z: number) => {
17+
let vertsAtElevation = 0;
18+
let line: [x: number, z: number][] = [];
19+
for (let i = 0; i < face.length; i++) {
20+
let vertex1 = face[i] as [x: number, y: number, z: number];
21+
let vertex2 = face[(i + 1) % face.length] as [x: number, y: number, z: number];
22+
if (vertex1[2] === z) vertsAtElevation++;
23+
24+
if (
25+
((vertex1[2] <= z && vertex2[2] >= z) || (vertex1[2] >= z && vertex2[2] <= z)) &&
26+
!Number.isNaN((z - vertex1[2]) / (vertex2[2] - vertex1[2]))
27+
) {
28+
let t = (z - vertex1[2]) / (vertex2[2] - vertex1[2]);
29+
line.push([vertex1[0] + t * (vertex2[0] - vertex1[0]), vertex1[1] + t * (vertex2[1] - vertex1[1])]);
30+
}
31+
}
32+
33+
// If an edge is going to be detected by two triangles, prioritize the triangle with 3rd vertex at lower elevation
34+
if (vertsAtElevation >= 2 && face.map((f) => f[2]).reduce((a, b) => a + b) > z * face.length) return undefined;
35+
36+
// Prevent zero length lines
37+
if (
38+
line.length === 2 &&
39+
(line[0] as any)[0] === (line[1] as any)[0] &&
40+
(line[0] as any)[1] === (line[1] as any)[1]
41+
)
42+
return undefined;
43+
44+
if (line.length > 2) {
45+
line = [...new Set(line.map((v) => JSON.stringify(v)))].map((s) => JSON.parse(s));
46+
}
47+
return line.length > 0 ? (line as [[x: number, z: number], [x: number, z: number]]) : undefined;
48+
};
49+
50+
const linesToPolyLines = (lineSegments: [[number, number], [number, number]][]) => {
51+
if (!Array.isArray(lineSegments) || lineSegments?.length === 0) {
52+
return [];
53+
// throw new Error("Invalid input: Please provide a non-empty array of line segments.");
54+
}
55+
56+
const segmentsMapIndexes: { [coordinateKey: string]: number[] } = {};
57+
const polylines: [number, number][][] = [];
58+
const parsedSegmentIndexes: number[] = [];
59+
60+
const lineSegmentStrings = lineSegments.map((v) => v.map((c) => c.join(","))) as [string, string][];
61+
62+
lineSegmentStrings.forEach(([start, end], i) => {
63+
segmentsMapIndexes[start] = segmentsMapIndexes[start] ? [...(segmentsMapIndexes[start] || []), i] : [i];
64+
segmentsMapIndexes[end] = segmentsMapIndexes[end] ? [...(segmentsMapIndexes[end] || []), i] : [i];
65+
});
66+
67+
for (let i = 0; i < lineSegmentStrings.length; i++) {
68+
if (parsedSegmentIndexes.includes(i)) continue;
69+
70+
parsedSegmentIndexes.push(i);
71+
72+
let [start, end]: (string | null)[] = lineSegmentStrings[i] as [string, string];
73+
let polyline = [start, end];
74+
75+
while (start && segmentsMapIndexes[start]) {
76+
const nextLineIndex: number | undefined = segmentsMapIndexes[start]?.find(
77+
(lineIndex) => !parsedSegmentIndexes.includes(lineIndex)
78+
);
79+
if (nextLineIndex) {
80+
parsedSegmentIndexes.push(nextLineIndex);
81+
const nextLineSegment = lineSegmentStrings[nextLineIndex] as [string, string];
82+
const nextLineSegmentPointIndex: number = nextLineSegment[0] === start ? 1 : 0;
83+
const newPoint = nextLineSegment[nextLineSegmentPointIndex] as string;
84+
polyline.unshift(newPoint);
85+
start = newPoint;
86+
} else {
87+
start = null;
88+
}
89+
}
90+
91+
while (end && segmentsMapIndexes[end]) {
92+
const nextLineIndex: number | undefined = segmentsMapIndexes[end]?.find(
93+
(lineIndex) => !parsedSegmentIndexes.includes(lineIndex)
94+
);
95+
if (nextLineIndex) {
96+
parsedSegmentIndexes.push(nextLineIndex);
97+
const nextLineSegment = lineSegmentStrings[nextLineIndex] as [string, string];
98+
const nextLineSegmentPointIndex: number = nextLineSegment[0] === end ? 1 : 0;
99+
const newPoint = nextLineSegment[nextLineSegmentPointIndex] as string;
100+
polyline.push(newPoint);
101+
end = newPoint;
102+
} else {
103+
end = null;
104+
}
105+
}
106+
polylines.push(polyline.map((coord) => coord.split(",").map((v) => parseFloat(v)) as [number, number]));
107+
}
108+
return polylines;
109+
};
110+
111+
onMessage((message) => {
112+
const { triangles, elevation } = message.payload;
113+
const linesAtElevationE = triangles.reduce((prev, curr) => {
114+
const line = contourLineOnFace(curr, elevation);
115+
if (line) prev.push(line);
116+
return prev;
117+
}, [] as [[x: number, z: number], [x: number, z: number]][]);
118+
119+
message.resolve({
120+
elevation,
121+
polylines: linesToPolyLines(linesAtElevationE),
122+
});
123+
});
124+
},
125+
{ maxWorkers: 10 }
126+
);
3127

4128
const contourLineOnFace = (face: [x: number, y: number, z: number][], z: number) => {
5129
let vertsAtElevation = 0;
@@ -32,8 +156,9 @@ const contourLineOnFace = (face: [x: number, y: number, z: number][], z: number)
32156
};
33157

34158
const linesToPolyLines = (lineSegments: [[number, number], [number, number]][]) => {
35-
if (!Array.isArray(lineSegments) || lineSegments.length === 0) {
36-
throw new Error("Invalid input: Please provide a non-empty array of line segments.");
159+
if (!Array.isArray(lineSegments) || lineSegments?.length === 0) {
160+
return [];
161+
// throw new Error("Invalid input: Please provide a non-empty array of line segments.");
37162
}
38163

39164
const segmentsMapIndexes: { [coordinateKey: string]: number[] } = {};
@@ -150,23 +275,10 @@ const getContours = async (data: ParsedSurface, interval: number = 2) => {
150275

151276
const elevations = contourElevations(minElevation, maxElevation, interval);
152277

153-
const elevationPolylines = elevations.map((e) => {
154-
const linesAtElevationE = triangles.reduce((prev, curr) => {
155-
const line = contourLineOnFace(curr, e);
156-
if (line) prev.push(line);
157-
return prev;
158-
}, [] as [[x: number, z: number], [x: number, z: number]][]);
159-
160-
const polylinesAtElevationE = linesToPolyLines(linesAtElevationE);
161-
if (e === 442) {
162-
console.log("linesAtElevationE", JSON.stringify(linesAtElevationE));
163-
console.log("polylinesAtElevationE", JSON.stringify(polylinesAtElevationE));
164-
}
165-
return {
166-
elevation: e,
167-
polylines: polylinesAtElevationE,
168-
};
169-
});
278+
const elevationPolylines: {
279+
elevation: number;
280+
polylines: [number, number][][];
281+
}[] = await Promise.all(elevations.map((elevation) => (contoursWorker.send as any)({ triangles, elevation })));
170282

171283
return constructGeojson(elevationPolylines);
172284
};

src/private/landxml.d.ts

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
interface LandXML {
2+
LandXML: {
3+
attr: {
4+
xmlns: string;
5+
"xmlns:xsi": string;
6+
"xsi:schemaLocation": string;
7+
date: string;
8+
time: string;
9+
version: string;
10+
language: string;
11+
readOnly: string;
12+
};
13+
Units:
14+
| {
15+
Imperial: {
16+
attr: {
17+
areaUnit: string;
18+
linearUnit: string;
19+
volumeUnit: string;
20+
temperatureUnit: string;
21+
pressureUnit: string;
22+
diameterUnit: string;
23+
angularUnit: string;
24+
directionUnit: string;
25+
};
26+
};
27+
}
28+
| {
29+
Metric: {
30+
attr: {
31+
areaUnit: string;
32+
linearUnit: string;
33+
volumeUnit: string;
34+
temperatureUnit: string;
35+
pressureUnit: string;
36+
diameterUnit: string;
37+
angularUnit: string;
38+
directionUnit: string;
39+
};
40+
};
41+
};
42+
Project: {
43+
attr: {
44+
name: string;
45+
};
46+
};
47+
CoordinateSystem: {
48+
attr: {
49+
desc: string;
50+
ogcWktCode: string;
51+
};
52+
};
53+
Application: {
54+
attr: {
55+
name: string;
56+
desc: string;
57+
manufacturer: string;
58+
version: string;
59+
timeStamp: string;
60+
};
61+
};
62+
Surfaces: {
63+
Surface: Surface | Surface[];
64+
};
65+
};
66+
}
67+
68+
interface Surface {
69+
attr: {
70+
name: string;
71+
desc: string;
72+
};
73+
SourceData?: any;
74+
Definition: {
75+
attr: {
76+
surfType: string;
77+
};
78+
Pnts: {
79+
P: SurfacePoint[];
80+
};
81+
Faces: {
82+
F: SurfaceFace[];
83+
};
84+
};
85+
}
86+
87+
type SurfacePoint = {
88+
attr: {
89+
id: string;
90+
};
91+
content: string;
92+
};
93+
94+
type SurfaceFace =
95+
| {
96+
attr: {
97+
i?: string;
98+
};
99+
content: string;
100+
}
101+
| string;

0 commit comments

Comments
 (0)