Skip to content

Commit f4dea9c

Browse files
Rewrite @turf/isolines (#2918)
* Reimplement @turf/isolines to clear up the licensing concerns around marchingsquares. --------- Co-authored-by: James Beard <[email protected]>
1 parent e352195 commit f4dea9c

File tree

9 files changed

+867
-824
lines changed

9 files changed

+867
-824
lines changed

eslint.config.mjs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,5 @@ export default tsEslint.config(
7171
},
7272
},
7373
},
74-
{
75-
files: [
76-
"packages/turf-isobands/lib/marchingsquares-isobands.js",
77-
"packages/turf-isolines/lib/marchingsquares-isocontours.js",
78-
],
79-
80-
languageOptions: {
81-
globals: {
82-
...globals.browser,
83-
},
84-
},
85-
},
8674
prettierRecommended
8775
);

packages/turf-isobands/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"description": "Takes a grid of values (GeoJSON format) and a set of threshold ranges. It outputs polygons that group areas within those ranges, effectively creating filled contour isobands.",
55
"author": "Turf Authors",
66
"contributors": [
7-
"Stefano Borghi <@stebogit>"
7+
"Stefano Borghi <@stebogit>",
8+
"Matt Fedderly <@mfedderly>"
89
],
910
"license": "MIT",
1011
"bugs": {

packages/turf-isolines/index.ts

Lines changed: 219 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ import { bbox } from "@turf/bbox";
22
import { coordEach } from "@turf/meta";
33
import { collectionOf } from "@turf/invariant";
44
import { multiLineString, featureCollection, isObject } from "@turf/helpers";
5-
// @ts-expect-error Legacy JS library with no types defined
6-
import { isoContours } from "marchingsquares";
75
import { gridToMatrix } from "./lib/grid-to-matrix.js";
86
import {
97
FeatureCollection,
108
Point,
119
MultiLineString,
1210
Feature,
1311
GeoJsonProperties,
12+
Position,
1413
} from "geojson";
1514

1615
/**
@@ -70,6 +69,10 @@ function isolines(
7069
// Isoline methods
7170
const matrix = gridToMatrix(pointGrid, { zProperty: zProperty, flip: true });
7271

72+
// A quick note on what 'top' and 'bottom' mean in coordinate system of `matrix`:
73+
// Remember that the southern hemisphere is represented by negative numbers,
74+
// so a matrix Y of 0 is actually the *bottom*, and a Y of dy - 1 is the *top*.
75+
7376
// check that the resulting matrix has consistent x and y dimensions and
7477
// has at least a 2x2 size so that we can actually build grid squares
7578
const dx = matrix[0].length;
@@ -122,18 +125,226 @@ function createIsoLines(
122125

123126
const properties = { ...commonProperties, ...breaksProperties[i] };
124127
properties[zProperty] = threshold;
125-
// Pass options to marchingsquares lib to reproduce historical turf
126-
// behaviour.
127-
const isoline = multiLineString(
128-
isoContours(matrix, threshold, { linearRing: false, noFrame: true }),
129-
properties
130-
);
128+
const isoline = multiLineString(isoContours(matrix, threshold), properties);
131129

132130
results.push(isoline);
133131
}
134132
return results;
135133
}
136134

135+
function isoContours(
136+
matrix: ReadonlyArray<ReadonlyArray<number>>,
137+
threshold: number
138+
): Position[][] {
139+
// see https://en.wikipedia.org/wiki/Marching_squares
140+
const segments: [Position, Position][] = [];
141+
142+
const dy = matrix.length;
143+
const dx = matrix[0].length;
144+
145+
for (let y = 0; y < dy - 1; y++) {
146+
for (let x = 0; x < dx - 1; x++) {
147+
const tr = matrix[y + 1][x + 1];
148+
const br = matrix[y][x + 1];
149+
const bl = matrix[y][x];
150+
const tl = matrix[y + 1][x];
151+
152+
let grid =
153+
(tl >= threshold ? 8 : 0) |
154+
(tr >= threshold ? 4 : 0) |
155+
(br >= threshold ? 2 : 0) |
156+
(bl >= threshold ? 1 : 0);
157+
158+
switch (grid) {
159+
case 0:
160+
continue;
161+
case 1:
162+
segments.push([
163+
[x + frac(bl, br), y],
164+
[x, y + frac(bl, tl)],
165+
]);
166+
break;
167+
case 2:
168+
segments.push([
169+
[x + 1, y + frac(br, tr)],
170+
[x + frac(bl, br), y],
171+
]);
172+
break;
173+
case 3:
174+
segments.push([
175+
[x + 1, y + frac(br, tr)],
176+
[x, y + frac(bl, tl)],
177+
]);
178+
break;
179+
case 4:
180+
segments.push([
181+
[x + frac(tl, tr), y + 1],
182+
[x + 1, y + frac(br, tr)],
183+
]);
184+
break;
185+
case 5: {
186+
// use the average of the 4 corners to differentiate the saddle case and correctly honor the counter-clockwise winding
187+
const avg = (tl + tr + br + bl) / 4;
188+
const above = avg >= threshold;
189+
190+
if (above) {
191+
segments.push(
192+
[
193+
[x + frac(tl, tr), y + 1],
194+
[x, y + frac(bl, tl)],
195+
],
196+
[
197+
[x + frac(bl, br), y],
198+
[x + 1, y + frac(br, tr)],
199+
]
200+
);
201+
} else {
202+
segments.push(
203+
[
204+
[x + frac(tl, tr), y + 1],
205+
[x + 1, y + frac(br, tr)],
206+
],
207+
[
208+
[x + frac(bl, br), y],
209+
[x, y + frac(bl, tl)],
210+
]
211+
);
212+
}
213+
break;
214+
}
215+
case 6:
216+
segments.push([
217+
[x + frac(tl, tr), y + 1],
218+
[x + frac(bl, br), y],
219+
]);
220+
break;
221+
case 7:
222+
segments.push([
223+
[x + frac(tl, tr), y + 1],
224+
[x, y + frac(bl, tl)],
225+
]);
226+
break;
227+
case 8:
228+
segments.push([
229+
[x, y + frac(bl, tl)],
230+
[x + frac(tl, tr), y + 1],
231+
]);
232+
break;
233+
case 9:
234+
segments.push([
235+
[x + frac(bl, br), y],
236+
[x + frac(tl, tr), y + 1],
237+
]);
238+
break;
239+
case 10: {
240+
const avg = (tl + tr + br + bl) / 4;
241+
const above = avg >= threshold;
242+
243+
if (above) {
244+
segments.push(
245+
[
246+
[x, y + frac(bl, tl)],
247+
[x + frac(bl, br), y],
248+
],
249+
[
250+
[x + 1, y + frac(br, tr)],
251+
[x + frac(tl, tr), y + 1],
252+
]
253+
);
254+
} else {
255+
segments.push(
256+
[
257+
[x, y + frac(bl, tl)],
258+
[x + frac(tl, tr), y + 1],
259+
],
260+
[
261+
[x + 1, y + frac(br, tr)],
262+
[x + frac(bl, br), y],
263+
]
264+
);
265+
}
266+
break;
267+
}
268+
case 11:
269+
segments.push([
270+
[x + 1, y + frac(br, tr)],
271+
[x + frac(tl, tr), y + 1],
272+
]);
273+
break;
274+
case 12:
275+
segments.push([
276+
[x, y + frac(bl, tl)],
277+
[x + 1, y + frac(br, tr)],
278+
]);
279+
break;
280+
case 13:
281+
segments.push([
282+
[x + frac(bl, br), y],
283+
[x + 1, y + frac(br, tr)],
284+
]);
285+
break;
286+
case 14:
287+
segments.push([
288+
[x, y + frac(bl, tl)],
289+
[x + frac(bl, br), y],
290+
]);
291+
break;
292+
case 15:
293+
// all above
294+
continue;
295+
}
296+
}
297+
}
298+
299+
const contours: Position[][] = [];
300+
301+
while (segments.length > 0) {
302+
const contour: Position[] = [...segments.shift()!];
303+
contours.push(contour);
304+
305+
let found: boolean;
306+
do {
307+
found = false;
308+
for (let i = 0; i < segments.length; i++) {
309+
const segment = segments[i];
310+
// add the segment's end point to the end of the contour
311+
if (
312+
segment[0][0] === contour[contour.length - 1][0] &&
313+
segment[0][1] === contour[contour.length - 1][1]
314+
) {
315+
found = true;
316+
contour.push(segment[1]);
317+
segments.splice(i, 1);
318+
break;
319+
}
320+
// add the segment's start point to the start of the contour
321+
if (
322+
segment[1][0] === contour[0][0] &&
323+
segment[1][1] === contour[0][1]
324+
) {
325+
found = true;
326+
contour.unshift(segment[0]);
327+
segments.splice(i, 1);
328+
break;
329+
}
330+
}
331+
} while (found);
332+
}
333+
334+
return contours;
335+
336+
// get the linear interpolation fraction of how far z is between z0 and z1
337+
// See https://github.com/fschutt/marching-squares/blob/master/src/lib.rs
338+
function frac(z0: number, z1: number): number {
339+
if (z0 === z1) {
340+
return 0.5;
341+
}
342+
343+
let t = (threshold - z0) / (z1 - z0);
344+
return t > 1 ? 1 : t < 0 ? 0 : t;
345+
}
346+
}
347+
137348
/**
138349
* Translates and scales isolines
139350
*

packages/turf-isolines/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"description": "Generate contour lines from a grid of data.",
55
"author": "Turf Authors",
66
"contributors": [
7-
"Stefano Borghi <@stebogit>"
7+
"Stefano Borghi <@stebogit>",
8+
"Matt Fedderly <@mfedderly>"
89
],
910
"license": "MIT",
1011
"bugs": {
@@ -79,7 +80,6 @@
7980
"@turf/invariant": "workspace:*",
8081
"@turf/meta": "workspace:*",
8182
"@types/geojson": "^7946.0.10",
82-
"marchingsquares": "^1.3.3",
8383
"tslib": "^2.8.1"
8484
}
8585
}

0 commit comments

Comments
 (0)