@@ -2,15 +2,14 @@ import { bbox } from "@turf/bbox";
22import { coordEach } from "@turf/meta" ;
33import { collectionOf } from "@turf/invariant" ;
44import { multiLineString , featureCollection , isObject } from "@turf/helpers" ;
5- // @ts -expect-error Legacy JS library with no types defined
6- import { isoContours } from "marchingsquares" ;
75import { gridToMatrix } from "./lib/grid-to-matrix.js" ;
86import {
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 *
0 commit comments