planefill 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,588 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/index.js
30
+ var src_exports = {};
31
+ __export(src_exports, {
32
+ DIVISION_STRATEGIES: () => DIVISION_STRATEGIES,
33
+ STRATEGIES: () => STRATEGIES,
34
+ dividePolygon: () => dividePolygon,
35
+ planMultiPath: () => planMultiPath,
36
+ planPath: () => planPath
37
+ });
38
+ module.exports = __toCommonJS(src_exports);
39
+
40
+ // src/planner.js
41
+ var turf7 = __toESM(require("@turf/turf"), 1);
42
+
43
+ // src/strategies/boustrophedon.js
44
+ var turf2 = __toESM(require("@turf/turf"), 1);
45
+
46
+ // src/utils/coords.js
47
+ function metersToDegreesLat(meters) {
48
+ return meters / 111e3;
49
+ }
50
+ function metersToDegreesLng(meters, latDeg) {
51
+ const cosLat = Math.cos(latDeg * Math.PI / 180);
52
+ if (Math.abs(cosLat) < 1e-10) return 0;
53
+ return meters / (111e3 * cosLat);
54
+ }
55
+
56
+ // src/utils/geometry.js
57
+ var turf = __toESM(require("@turf/turf"), 1);
58
+ function clipScanLine(polygon4, y, xMin, xMax) {
59
+ const EPS = 1e-10;
60
+ const scanLine = turf.lineString([
61
+ [xMin - 1, y + EPS],
62
+ [xMax + 1, y + EPS]
63
+ ]);
64
+ const hits = turf.lineIntersect(scanLine, polygon4);
65
+ if (!hits || hits.features.length === 0) return [];
66
+ let xs = hits.features.map((f) => f.geometry.coordinates[0]).sort((a, b) => a - b);
67
+ xs = xs.filter((x, i) => i === 0 || Math.abs(x - xs[i - 1]) > 1e-8);
68
+ if (xs.length % 2 !== 0) xs.pop();
69
+ const segments = [];
70
+ for (let i = 0; i + 1 < xs.length; i += 2) {
71
+ segments.push([xs[i], xs[i + 1]]);
72
+ }
73
+ return segments;
74
+ }
75
+ function findClosestIndex(coords, target) {
76
+ let bestIdx = 0;
77
+ let bestDist = Infinity;
78
+ for (let i = 0; i < coords.length; i++) {
79
+ const dx = coords[i][0] - target[0];
80
+ const dy = coords[i][1] - target[1];
81
+ const d = dx * dx + dy * dy;
82
+ if (d < bestDist) {
83
+ bestDist = d;
84
+ bestIdx = i;
85
+ }
86
+ }
87
+ return bestIdx;
88
+ }
89
+ function reorderRing(coords, startIndex) {
90
+ const isClosed = coords[0][0] === coords[coords.length - 1][0] && coords[0][1] === coords[coords.length - 1][1];
91
+ const ring = isClosed ? coords.slice(0, -1) : coords.slice();
92
+ return [...ring.slice(startIndex), ...ring.slice(0, startIndex)];
93
+ }
94
+ function getLargestPolygon(feature) {
95
+ if (!feature) return null;
96
+ const geom = feature.geometry ?? feature;
97
+ if (!geom) return null;
98
+ if (geom.type === "Polygon") {
99
+ return { type: "Feature", geometry: geom, properties: {} };
100
+ }
101
+ if (geom.type === "MultiPolygon") {
102
+ let best = null;
103
+ let bestArea = -Infinity;
104
+ for (const coords of geom.coordinates) {
105
+ const poly = turf.polygon(coords);
106
+ const a = turf.area(poly);
107
+ if (a > bestArea) {
108
+ bestArea = a;
109
+ best = poly;
110
+ }
111
+ }
112
+ return best;
113
+ }
114
+ return null;
115
+ }
116
+
117
+ // src/strategies/boustrophedon.js
118
+ function boustrophedon(polygon4, { spacing, angle = 0 }) {
119
+ const centroidPt = turf2.centroid(polygon4);
120
+ const rotated = angle !== 0 ? turf2.transformRotate(polygon4, -angle, { pivot: centroidPt }) : polygon4;
121
+ const [minX, minY, maxX, maxY] = turf2.bbox(rotated);
122
+ const spacingDeg = metersToDegreesLat(spacing);
123
+ const ys = [];
124
+ for (let y = minY + spacingDeg / 2; y < maxY + spacingDeg / 2; y += spacingDeg) {
125
+ ys.push(y);
126
+ }
127
+ if (ys.length === 0) {
128
+ const c = centroidPt.geometry.coordinates;
129
+ return turf2.lineString([c, c]);
130
+ }
131
+ const coords = [];
132
+ let leftToRight = true;
133
+ for (const y of ys) {
134
+ const segments = clipScanLine(rotated, y, minX, maxX);
135
+ if (segments.length === 0) continue;
136
+ if (leftToRight) {
137
+ for (const [x0, x1] of segments) {
138
+ coords.push([x0, y]);
139
+ coords.push([x1, y]);
140
+ }
141
+ } else {
142
+ for (const [x0, x1] of [...segments].reverse()) {
143
+ coords.push([x1, y]);
144
+ coords.push([x0, y]);
145
+ }
146
+ }
147
+ leftToRight = !leftToRight;
148
+ }
149
+ if (coords.length < 2) {
150
+ const c = centroidPt.geometry.coordinates;
151
+ return turf2.lineString([c, c]);
152
+ }
153
+ const path = turf2.lineString(coords);
154
+ return angle !== 0 ? turf2.transformRotate(path, angle, { pivot: centroidPt }) : path;
155
+ }
156
+
157
+ // src/strategies/outerIn.js
158
+ var turf3 = __toESM(require("@turf/turf"), 1);
159
+ function outerIn(polygon4, { spacing }) {
160
+ const rings = collectRings(polygon4, spacing);
161
+ if (rings.length === 0) {
162
+ const c = turf3.centroid(polygon4).geometry.coordinates;
163
+ return turf3.lineString([c, c]);
164
+ }
165
+ return connectRings(rings);
166
+ }
167
+ function collectRings(polygon4, spacingMeters) {
168
+ const spacingKm = spacingMeters / 1e3;
169
+ const rings = [];
170
+ rings.push(polygon4.geometry.coordinates[0].slice());
171
+ let current = polygon4;
172
+ for (let i = 0; i < 1e3; i++) {
173
+ let eroded;
174
+ try {
175
+ eroded = turf3.buffer(current, -spacingKm, { units: "kilometers", steps: 16 });
176
+ } catch {
177
+ break;
178
+ }
179
+ if (!eroded) break;
180
+ const largest = getLargestPolygon(eroded);
181
+ if (!largest) break;
182
+ const outerRing = largest.geometry.coordinates[0];
183
+ if (!outerRing || outerRing.length < 4) break;
184
+ rings.push(outerRing.slice());
185
+ current = largest;
186
+ }
187
+ return rings;
188
+ }
189
+ function connectRings(rings) {
190
+ const path = [];
191
+ for (const ring of rings) {
192
+ const isClosed = ring[0][0] === ring[ring.length - 1][0] && ring[0][1] === ring[ring.length - 1][1];
193
+ const open = isClosed ? ring.slice(0, -1) : ring.slice();
194
+ if (path.length === 0) {
195
+ path.push(...open);
196
+ } else {
197
+ const lastPt = path[path.length - 1];
198
+ const startIdx = findClosestIndex(open, lastPt);
199
+ path.push(...reorderRing(open, startIdx));
200
+ }
201
+ }
202
+ if (path.length < 2) return turf3.lineString([path[0], path[0]]);
203
+ return turf3.lineString(path);
204
+ }
205
+
206
+ // src/strategies/innerOut.js
207
+ var turf4 = __toESM(require("@turf/turf"), 1);
208
+ function innerOut(polygon4, { spacing }) {
209
+ const rings = collectRings(polygon4, spacing);
210
+ if (rings.length === 0) {
211
+ const c = turf4.centroid(polygon4).geometry.coordinates;
212
+ return turf4.lineString([c, c]);
213
+ }
214
+ return connectRings([...rings].reverse());
215
+ }
216
+
217
+ // src/strategies/hilbert.js
218
+ var turf5 = __toESM(require("@turf/turf"), 1);
219
+ var MAX_N = 512;
220
+ function hilbert(polygon4, { spacing }) {
221
+ const [minX, minY, maxX, maxY] = turf5.bbox(polygon4);
222
+ const centerLat = (minY + maxY) / 2;
223
+ const cellW = metersToDegreesLng(spacing, centerLat);
224
+ const cellH = metersToDegreesLat(spacing);
225
+ const numCols = Math.max(1, Math.ceil((maxX - minX) / cellW));
226
+ const numRows = Math.max(1, Math.ceil((maxY - minY) / cellH));
227
+ const n = Math.min(MAX_N, nextPowerOf2(Math.max(numCols, numRows)));
228
+ const insideCoords = [];
229
+ for (let d = 0; d < n * n; d++) {
230
+ const { x, y } = hilbertDToXY(n, d);
231
+ if (x >= numCols || y >= numRows) continue;
232
+ const lng = minX + (x + 0.5) * cellW;
233
+ const lat = minY + (y + 0.5) * cellH;
234
+ if (turf5.booleanPointInPolygon(turf5.point([lng, lat]), polygon4)) {
235
+ insideCoords.push([lng, lat]);
236
+ }
237
+ }
238
+ if (insideCoords.length === 0) {
239
+ const c = turf5.centroid(polygon4).geometry.coordinates;
240
+ return turf5.lineString([c, c]);
241
+ }
242
+ if (insideCoords.length === 1) {
243
+ return turf5.lineString([insideCoords[0], insideCoords[0]]);
244
+ }
245
+ return turf5.lineString(insideCoords);
246
+ }
247
+ function hilbertDToXY(n, d) {
248
+ let x = 0, y = 0;
249
+ let t = d;
250
+ for (let s = 1; s < n; s *= 2) {
251
+ const rx = 1 & Math.floor(t / 2);
252
+ const ry = 1 & (t ^ rx);
253
+ if (ry === 0) {
254
+ if (rx === 1) {
255
+ x = s - 1 - x;
256
+ y = s - 1 - y;
257
+ }
258
+ const tmp = x;
259
+ x = y;
260
+ y = tmp;
261
+ }
262
+ x += s * rx;
263
+ y += s * ry;
264
+ t = Math.floor(t / 4);
265
+ }
266
+ return { x, y };
267
+ }
268
+ function nextPowerOf2(n) {
269
+ if (n <= 1) return 1;
270
+ let p = 1;
271
+ while (p < n) p *= 2;
272
+ return p;
273
+ }
274
+
275
+ // src/strategies/gridSpiral.js
276
+ var turf6 = __toESM(require("@turf/turf"), 1);
277
+ function gridSpiral(polygon4, { spacing }) {
278
+ const [minX, minY, maxX, maxY] = turf6.bbox(polygon4);
279
+ const centerLat = (minY + maxY) / 2;
280
+ const cellW = metersToDegreesLng(spacing, centerLat);
281
+ const cellH = metersToDegreesLat(spacing);
282
+ const numCols = Math.max(1, Math.ceil((maxX - minX) / cellW));
283
+ const numRows = Math.max(1, Math.ceil((maxY - minY) / cellH));
284
+ const [cx, cy] = turf6.centroid(polygon4).geometry.coordinates;
285
+ const startCol = Math.min(numCols - 1, Math.max(0, Math.floor((cx - minX) / cellW)));
286
+ const startRow = Math.min(numRows - 1, Math.max(0, Math.floor((cy - minY) / cellH)));
287
+ const insideCoords = [];
288
+ for (const [col, row] of spiralOrder(startCol, startRow, numCols, numRows)) {
289
+ const lng = minX + (col + 0.5) * cellW;
290
+ const lat = minY + (row + 0.5) * cellH;
291
+ if (turf6.booleanPointInPolygon(turf6.point([lng, lat]), polygon4)) {
292
+ insideCoords.push([lng, lat]);
293
+ }
294
+ }
295
+ if (insideCoords.length === 0) {
296
+ const c = turf6.centroid(polygon4).geometry.coordinates;
297
+ return turf6.lineString([c, c]);
298
+ }
299
+ if (insideCoords.length === 1) {
300
+ return turf6.lineString([insideCoords[0], insideCoords[0]]);
301
+ }
302
+ return turf6.lineString(insideCoords);
303
+ }
304
+ function* spiralOrder(startCol, startRow, numCols, numRows) {
305
+ const DIRS = [[1, 0], [0, 1], [-1, 0], [0, -1]];
306
+ let col = startCol;
307
+ let row = startRow;
308
+ yield [col, row];
309
+ const maxRadius = Math.max(numCols, numRows);
310
+ let dirIdx = 0;
311
+ for (let stepSize = 1; stepSize <= maxRadius * 2; stepSize++) {
312
+ for (let turn = 0; turn < 2; turn++) {
313
+ const [dc, dr] = DIRS[dirIdx];
314
+ for (let i = 0; i < stepSize; i++) {
315
+ col += dc;
316
+ row += dr;
317
+ if (col >= 0 && col < numCols && row >= 0 && row < numRows) {
318
+ yield [col, row];
319
+ }
320
+ }
321
+ dirIdx = (dirIdx + 1) % 4;
322
+ }
323
+ }
324
+ }
325
+
326
+ // src/utils/simplify.js
327
+ function simplifyPath(coords, toleranceMeters) {
328
+ if (coords.length <= 2) return coords;
329
+ const result = rdp(coords, 0, coords.length - 1, toleranceMeters);
330
+ return result.map((i) => coords[i]);
331
+ }
332
+ function rdp(coords, lo, hi, tol) {
333
+ if (hi - lo < 2) {
334
+ const out = [];
335
+ for (let i = lo; i <= hi; i++) out.push(i);
336
+ return out;
337
+ }
338
+ let maxDist = 0;
339
+ let maxIdx = lo;
340
+ for (let i = lo + 1; i < hi; i++) {
341
+ const d = ptSegDistMeters(coords[i], coords[lo], coords[hi]);
342
+ if (d > maxDist) {
343
+ maxDist = d;
344
+ maxIdx = i;
345
+ }
346
+ }
347
+ if (maxDist > tol) {
348
+ const left = rdp(coords, lo, maxIdx, tol);
349
+ const right = rdp(coords, maxIdx, hi, tol);
350
+ return [...left, ...right.slice(1)];
351
+ }
352
+ return [lo, hi];
353
+ }
354
+ function ptSegDistMeters(p, a, b) {
355
+ const M_PER_DEG_LAT = 111e3;
356
+ const cosLat = Math.cos(p[1] * Math.PI / 180);
357
+ const px = p[0] * cosLat * M_PER_DEG_LAT, py = p[1] * M_PER_DEG_LAT;
358
+ const ax = a[0] * cosLat * M_PER_DEG_LAT, ay = a[1] * M_PER_DEG_LAT;
359
+ const bx = b[0] * cosLat * M_PER_DEG_LAT, by = b[1] * M_PER_DEG_LAT;
360
+ const dx = bx - ax, dy = by - ay;
361
+ const lenSq = dx * dx + dy * dy;
362
+ let cx, cy;
363
+ if (lenSq === 0) {
364
+ cx = px - ax;
365
+ cy = py - ay;
366
+ } else {
367
+ const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / lenSq));
368
+ cx = px - (ax + t * dx);
369
+ cy = py - (ay + t * dy);
370
+ }
371
+ return Math.sqrt(cx * cx + cy * cy);
372
+ }
373
+
374
+ // src/planner.js
375
+ var STRATEGIES = {
376
+ boustrophedon: (polygon4, opts) => boustrophedon(polygon4, opts),
377
+ diagonal: (polygon4, opts) => boustrophedon(polygon4, { ...opts, angle: 45 }),
378
+ "outer-in": (polygon4, opts) => outerIn(polygon4, opts),
379
+ "inner-out": (polygon4, opts) => innerOut(polygon4, opts),
380
+ hilbert: (polygon4, opts) => hilbert(polygon4, opts),
381
+ "grid-spiral": (polygon4, opts) => gridSpiral(polygon4, opts)
382
+ };
383
+ function planPath(geojson, options = {}) {
384
+ const { strategy = "boustrophedon", spacing, angle = 0 } = options;
385
+ if (!spacing || spacing <= 0) {
386
+ throw new Error("options.spacing must be a positive number (metres)");
387
+ }
388
+ if (!(strategy in STRATEGIES)) {
389
+ throw new Error(
390
+ `Unknown strategy "${strategy}". Valid strategies: ${Object.keys(STRATEGIES).join(", ")}`
391
+ );
392
+ }
393
+ const polygon4 = normalizeToPolygon(geojson);
394
+ const stratFn = STRATEGIES[strategy];
395
+ let result;
396
+ try {
397
+ result = stratFn(polygon4, { spacing, angle });
398
+ } catch (err) {
399
+ throw new Error(`Strategy "${strategy}" failed: ${err.message}`);
400
+ }
401
+ if (!result || result.geometry.coordinates.length < 2) {
402
+ const c = turf7.centroid(polygon4).geometry.coordinates;
403
+ return turf7.lineString([c, c], { strategy, spacing, degenerate: true });
404
+ }
405
+ const simplified = simplifyPath(result.geometry.coordinates, spacing * 0.12);
406
+ result = turf7.lineString(
407
+ simplified.length >= 2 ? simplified : result.geometry.coordinates,
408
+ result.properties
409
+ );
410
+ result.properties = { ...result.properties, strategy, spacing };
411
+ return result;
412
+ }
413
+ function normalizeToPolygon(geojson) {
414
+ if (!geojson) throw new Error("geojson is required");
415
+ const feature = geojson.type === "Feature" ? geojson : { type: "Feature", geometry: geojson, properties: {} };
416
+ const geom = feature.geometry;
417
+ if (!geom) throw new Error("GeoJSON feature has no geometry");
418
+ if (geom.type === "Polygon") return feature;
419
+ if (geom.type === "MultiPolygon") {
420
+ let best = null;
421
+ let bestArea = -Infinity;
422
+ for (const coords of geom.coordinates) {
423
+ const poly = turf7.polygon(coords);
424
+ const a = turf7.area(poly);
425
+ if (a > bestArea) {
426
+ bestArea = a;
427
+ best = poly;
428
+ }
429
+ }
430
+ if (!best) throw new Error("MultiPolygon contains no valid polygons");
431
+ return best;
432
+ }
433
+ throw new Error(
434
+ `Unsupported geometry type "${geom.type}". planPath accepts Polygon or MultiPolygon.`
435
+ );
436
+ }
437
+
438
+ // src/multi.js
439
+ var turf8 = __toESM(require("@turf/turf"), 1);
440
+ var DIVISION_STRATEGIES = ["vertical", "horizontal", "grid", "balanced"];
441
+ function planMultiPath(geojson, {
442
+ parties = 2,
443
+ divisionStrategy = "vertical",
444
+ coverageStrategy = "boustrophedon",
445
+ spacing,
446
+ angle = 0
447
+ } = {}) {
448
+ if (!spacing || spacing <= 0) {
449
+ throw new Error("options.spacing must be a positive number (metres)");
450
+ }
451
+ if (!Number.isInteger(parties) || parties < 1) {
452
+ throw new Error("options.parties must be a positive integer");
453
+ }
454
+ const polygon4 = normalizeToPolygon2(geojson);
455
+ const regions = dividePolygon(polygon4, parties, divisionStrategy);
456
+ if (regions.length === 0) {
457
+ throw new Error("No regions produced \u2014 polygon may be too small for the requested division");
458
+ }
459
+ const features = regions.map((region, i) => {
460
+ const path = planPath(region, { strategy: coverageStrategy, spacing, angle });
461
+ path.properties = { ...path.properties, party: i };
462
+ return path;
463
+ });
464
+ return turf8.featureCollection(features);
465
+ }
466
+ function dividePolygon(polygon4, n, strategy = "vertical") {
467
+ if (n === 1) return [polygon4];
468
+ switch (strategy) {
469
+ case "vertical":
470
+ return stripDivide(polygon4, n, "x");
471
+ case "horizontal":
472
+ return stripDivide(polygon4, n, "y");
473
+ case "grid":
474
+ return gridDivide(polygon4, n);
475
+ case "balanced":
476
+ return balancedDivide(polygon4, n);
477
+ default:
478
+ throw new Error(
479
+ `Unknown division strategy "${strategy}". Valid: ${DIVISION_STRATEGIES.join(", ")}`
480
+ );
481
+ }
482
+ }
483
+ function stripDivide(polygon4, n, axis) {
484
+ const [minX, minY, maxX, maxY] = turf8.bbox(polygon4);
485
+ const regions = [];
486
+ for (let i = 0; i < n; i++) {
487
+ const t0 = i / n, t1 = (i + 1) / n;
488
+ const clip = axis === "x" ? turf8.bboxPolygon([minX + t0 * (maxX - minX), minY, minX + t1 * (maxX - minX), maxY]) : turf8.bboxPolygon([minX, minY + t0 * (maxY - minY), maxX, minY + t1 * (maxY - minY)]);
489
+ const region = turf8.intersect(polygon4, clip);
490
+ if (region) regions.push(largestPolygon(region));
491
+ }
492
+ return regions;
493
+ }
494
+ function gridDivide(polygon4, n) {
495
+ const [minX, minY, maxX, maxY] = turf8.bbox(polygon4);
496
+ const cols = Math.ceil(Math.sqrt(n));
497
+ const rows = Math.ceil(n / cols);
498
+ const cellW = (maxX - minX) / cols;
499
+ const cellH = (maxY - minY) / rows;
500
+ const regions = [];
501
+ outer: for (let r = 0; r < rows; r++) {
502
+ for (let c = 0; c < cols; c++) {
503
+ if (regions.length >= n) break outer;
504
+ const clip = turf8.bboxPolygon([
505
+ minX + c * cellW,
506
+ minY + r * cellH,
507
+ minX + (c + 1) * cellW,
508
+ minY + (r + 1) * cellH
509
+ ]);
510
+ const region = turf8.intersect(polygon4, clip);
511
+ if (region) regions.push(largestPolygon(region));
512
+ }
513
+ }
514
+ return regions;
515
+ }
516
+ function balancedDivide(polygon4, n) {
517
+ if (n <= 1) return [polygon4];
518
+ const nLeft = Math.floor(n / 2);
519
+ const nRight = n - nLeft;
520
+ const [minX, minY, maxX, maxY] = turf8.bbox(polygon4);
521
+ const cosLat = Math.cos((minY + maxY) / 2 * Math.PI / 180);
522
+ const widthM = (maxX - minX) * cosLat * 111e3;
523
+ const heightM = (maxY - minY) * 111e3;
524
+ const axis = widthM >= heightM ? "x" : "y";
525
+ const [half1, half2] = bisectPolygon(polygon4, axis, nLeft / n);
526
+ return [
527
+ ...half1 ? balancedDivide(half1, nLeft) : [],
528
+ ...half2 ? balancedDivide(half2, nRight) : []
529
+ ];
530
+ }
531
+ function bisectPolygon(polygon4, axis, targetFraction) {
532
+ const [minX, minY, maxX, maxY] = turf8.bbox(polygon4);
533
+ const totalArea = turf8.area(polygon4);
534
+ const targetArea = totalArea * targetFraction;
535
+ let lo = axis === "x" ? minX : minY;
536
+ let hi = axis === "x" ? maxX : maxY;
537
+ for (let i = 0; i < 24; i++) {
538
+ const mid2 = (lo + hi) / 2;
539
+ const clip = axis === "x" ? turf8.bboxPolygon([minX, minY, mid2, maxY]) : turf8.bboxPolygon([minX, minY, maxX, mid2]);
540
+ const part = turf8.intersect(polygon4, clip);
541
+ const partArea = part ? turf8.area(part) : 0;
542
+ if (partArea < targetArea) lo = mid2;
543
+ else hi = mid2;
544
+ }
545
+ const mid = (lo + hi) / 2;
546
+ const clipLeft = axis === "x" ? turf8.bboxPolygon([minX, minY, mid, maxY]) : turf8.bboxPolygon([minX, minY, maxX, mid]);
547
+ const clipRight = axis === "x" ? turf8.bboxPolygon([mid, minY, maxX, maxY]) : turf8.bboxPolygon([minX, mid, maxX, maxY]);
548
+ const left = turf8.intersect(polygon4, clipLeft);
549
+ const right = turf8.intersect(polygon4, clipRight);
550
+ return [
551
+ left ? largestPolygon(left) : null,
552
+ right ? largestPolygon(right) : null
553
+ ];
554
+ }
555
+ function largestPolygon(feature) {
556
+ if (feature.geometry.type === "Polygon") return feature;
557
+ let best = null, bestArea = -Infinity;
558
+ for (const coords of feature.geometry.coordinates) {
559
+ const poly = turf8.polygon(coords);
560
+ const a = turf8.area(poly);
561
+ if (a > bestArea) {
562
+ bestArea = a;
563
+ best = poly;
564
+ }
565
+ }
566
+ return best;
567
+ }
568
+ function normalizeToPolygon2(geojson) {
569
+ if (!geojson) throw new Error("geojson is required");
570
+ const feature = geojson.type === "Feature" ? geojson : { type: "Feature", geometry: geojson, properties: {} };
571
+ const geom = feature.geometry;
572
+ if (!geom) throw new Error("GeoJSON feature has no geometry");
573
+ if (geom.type === "Polygon") return feature;
574
+ if (geom.type === "MultiPolygon") {
575
+ let best = null, bestArea = -Infinity;
576
+ for (const coords of geom.coordinates) {
577
+ const poly = turf8.polygon(coords);
578
+ const a = turf8.area(poly);
579
+ if (a > bestArea) {
580
+ bestArea = a;
581
+ best = poly;
582
+ }
583
+ }
584
+ return best;
585
+ }
586
+ throw new Error(`Unsupported geometry type "${geom.type}"`);
587
+ }
588
+ //# sourceMappingURL=index.cjs.map