@@ -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/**
@@ -69,6 +68,11 @@ function isolines(
6968
7069 // Isoline methods
7170 const matrix = gridToMatrix ( pointGrid , { zProperty : zProperty , flip : true } ) ;
71+
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+
7276 const createdIsoLines = createIsoLines (
7377 matrix ,
7478 breaks ,
@@ -109,18 +113,226 @@ function createIsoLines(
109113
110114 const properties = { ...commonProperties , ...breaksProperties [ i ] } ;
111115 properties [ zProperty ] = threshold ;
112- // Pass options to marchingsquares lib to reproduce historical turf
113- // behaviour.
114- const isoline = multiLineString (
115- isoContours ( matrix , threshold , { linearRing : false , noFrame : true } ) ,
116- properties
117- ) ;
116+ const isoline = multiLineString ( isoContours ( matrix , threshold ) , properties ) ;
118117
119118 results . push ( isoline ) ;
120119 }
121120 return results ;
122121}
123122
123+ function isoContours (
124+ matrix : ReadonlyArray < ReadonlyArray < number > > ,
125+ threshold : number
126+ ) : Position [ ] [ ] {
127+ // see https://en.wikipedia.org/wiki/Marching_squares
128+ const segments : [ Position , Position ] [ ] = [ ] ;
129+
130+ const dy = matrix . length ;
131+ const dx = matrix [ 0 ] . length ;
132+
133+ for ( let y = 0 ; y < dy - 1 ; y ++ ) {
134+ for ( let x = 0 ; x < dx - 1 ; x ++ ) {
135+ const tr = matrix [ y + 1 ] [ x + 1 ] ;
136+ const br = matrix [ y ] [ x + 1 ] ;
137+ const bl = matrix [ y ] [ x ] ;
138+ const tl = matrix [ y + 1 ] [ x ] ;
139+
140+ let grid =
141+ ( tl >= threshold ? 8 : 0 ) |
142+ ( tr >= threshold ? 4 : 0 ) |
143+ ( br >= threshold ? 2 : 0 ) |
144+ ( bl >= threshold ? 1 : 0 ) ;
145+
146+ switch ( grid ) {
147+ case 0 :
148+ continue ;
149+ case 1 :
150+ segments . push ( [
151+ [ x + frac ( bl , br ) , y ] ,
152+ [ x , y + frac ( bl , tl ) ] ,
153+ ] ) ;
154+ break ;
155+ case 2 :
156+ segments . push ( [
157+ [ x + 1 , y + frac ( br , tr ) ] ,
158+ [ x + frac ( bl , br ) , y ] ,
159+ ] ) ;
160+ break ;
161+ case 3 :
162+ segments . push ( [
163+ [ x + 1 , y + frac ( br , tr ) ] ,
164+ [ x , y + frac ( bl , tl ) ] ,
165+ ] ) ;
166+ break ;
167+ case 4 :
168+ segments . push ( [
169+ [ x + frac ( tl , tr ) , y + 1 ] ,
170+ [ x + 1 , y + frac ( br , tr ) ] ,
171+ ] ) ;
172+ break ;
173+ case 5 : {
174+ // use the average of the 4 corners to differentiate the saddle case and correctly honor the counter-clockwise winding
175+ const avg = ( tl + tr + br + bl ) / 4 ;
176+ const above = avg >= threshold ;
177+
178+ if ( above ) {
179+ segments . push (
180+ [
181+ [ x + frac ( tl , tr ) , y + 1 ] ,
182+ [ x , y + frac ( bl , tl ) ] ,
183+ ] ,
184+ [
185+ [ x + frac ( bl , br ) , y ] ,
186+ [ x + 1 , y + frac ( br , tr ) ] ,
187+ ]
188+ ) ;
189+ } else {
190+ segments . push (
191+ [
192+ [ x + frac ( tl , tr ) , y + 1 ] ,
193+ [ x + 1 , y + frac ( br , tr ) ] ,
194+ ] ,
195+ [
196+ [ x + frac ( bl , br ) , y ] ,
197+ [ x , y + frac ( bl , tl ) ] ,
198+ ]
199+ ) ;
200+ }
201+ break ;
202+ }
203+ case 6 :
204+ segments . push ( [
205+ [ x + frac ( tl , tr ) , y + 1 ] ,
206+ [ x + frac ( bl , br ) , y ] ,
207+ ] ) ;
208+ break ;
209+ case 7 :
210+ segments . push ( [
211+ [ x + frac ( tl , tr ) , y + 1 ] ,
212+ [ x , y + frac ( bl , tl ) ] ,
213+ ] ) ;
214+ break ;
215+ case 8 :
216+ segments . push ( [
217+ [ x , y + frac ( bl , tl ) ] ,
218+ [ x + frac ( tl , tr ) , y + 1 ] ,
219+ ] ) ;
220+ break ;
221+ case 9 :
222+ segments . push ( [
223+ [ x + frac ( bl , br ) , y ] ,
224+ [ x + frac ( tl , tr ) , y + 1 ] ,
225+ ] ) ;
226+ break ;
227+ case 10 : {
228+ const avg = ( tl + tr + br + bl ) / 4 ;
229+ const above = avg >= threshold ;
230+
231+ if ( above ) {
232+ segments . push (
233+ [
234+ [ x , y + frac ( bl , tl ) ] ,
235+ [ x + frac ( bl , br ) , y ] ,
236+ ] ,
237+ [
238+ [ x + 1 , y + frac ( br , tr ) ] ,
239+ [ x + frac ( tl , tr ) , y + 1 ] ,
240+ ]
241+ ) ;
242+ } else {
243+ segments . push (
244+ [
245+ [ x , y + frac ( bl , tl ) ] ,
246+ [ x + frac ( tl , tr ) , y + 1 ] ,
247+ ] ,
248+ [
249+ [ x + 1 , y + frac ( br , tr ) ] ,
250+ [ x + frac ( bl , br ) , y ] ,
251+ ]
252+ ) ;
253+ }
254+ break ;
255+ }
256+ case 11 :
257+ segments . push ( [
258+ [ x + 1 , y + frac ( br , tr ) ] ,
259+ [ x + frac ( tl , tr ) , y + 1 ] ,
260+ ] ) ;
261+ break ;
262+ case 12 :
263+ segments . push ( [
264+ [ x , y + frac ( bl , tl ) ] ,
265+ [ x + 1 , y + frac ( br , tr ) ] ,
266+ ] ) ;
267+ break ;
268+ case 13 :
269+ segments . push ( [
270+ [ x + frac ( bl , br ) , y ] ,
271+ [ x + 1 , y + frac ( br , tr ) ] ,
272+ ] ) ;
273+ break ;
274+ case 14 :
275+ segments . push ( [
276+ [ x , y + frac ( bl , tl ) ] ,
277+ [ x + frac ( bl , br ) , y ] ,
278+ ] ) ;
279+ break ;
280+ case 15 :
281+ // all above
282+ continue ;
283+ }
284+ }
285+ }
286+
287+ const contours : Position [ ] [ ] = [ ] ;
288+
289+ while ( segments . length > 0 ) {
290+ const contour : Position [ ] = [ ...segments . shift ( ) ! ] ;
291+ contours . push ( contour ) ;
292+
293+ let found : boolean ;
294+ do {
295+ found = false ;
296+ for ( let i = 0 ; i < segments . length ; i ++ ) {
297+ const segment = segments [ i ] ;
298+ // add the segment's end point to the end of the contour
299+ if (
300+ segment [ 0 ] [ 0 ] === contour [ contour . length - 1 ] [ 0 ] &&
301+ segment [ 0 ] [ 1 ] === contour [ contour . length - 1 ] [ 1 ]
302+ ) {
303+ found = true ;
304+ contour . push ( segment [ 1 ] ) ;
305+ segments . splice ( i , 1 ) ;
306+ break ;
307+ }
308+ // add the segment's start point to the start of the contour
309+ if (
310+ segment [ 1 ] [ 0 ] === contour [ 0 ] [ 0 ] &&
311+ segment [ 1 ] [ 1 ] === contour [ 0 ] [ 1 ]
312+ ) {
313+ found = true ;
314+ contour . unshift ( segment [ 0 ] ) ;
315+ segments . splice ( i , 1 ) ;
316+ break ;
317+ }
318+ }
319+ } while ( found ) ;
320+ }
321+
322+ return contours ;
323+
324+ // get the linear interpolation fraction of how far z is between z0 and z1
325+ // See https://github.com/fschutt/marching-squares/blob/master/src/lib.rs
326+ function frac ( z0 : number , z1 : number ) : number {
327+ if ( z0 === z1 ) {
328+ return 0.5 ;
329+ }
330+
331+ let t = ( threshold - z0 ) / ( z1 - z0 ) ;
332+ return t > 1 ? 1 : t < 0 ? 0 : t ;
333+ }
334+ }
335+
124336/**
125337 * Translates and scales isolines
126338 *
0 commit comments