Skip to content

Commit 15ae335

Browse files
committed
smart rewind (beta)
1 parent 00b0fea commit 15ae335

File tree

3 files changed

+131
-0
lines changed

3 files changed

+131
-0
lines changed

doc_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ geotoolbox.intersects(data1, data2);
120120
- [**`reverse()`**](global.html#reverse) - Converts CCW rings to CW. Reverses direction of LineStrings.
121121
- [**`rewind()`**](global.html#rewind) - Rewind a geoJSON (Fil recipe).
122122
- [**`rewind2()`**](global.html#rewind2) - Rewind a geoJSON (Mapbox recipe).
123+
- [**`smartrewind()`**](global.html#smartrewind) - Rewind a geoJSON (Homemade recipe).
123124
- [**`roundcoordinates()`**](global.html#roundcoordinates) - The function allows to round the coordinates of geometries.
124125

125126
**3 - Tests**

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export { centroid } from "./centroid.js";
3030
export { aggregate } from "./aggregate.js";
3131
export { rewind } from "./rewind.js";
3232
export { rewind2 } from "./rewind2.js";
33+
export { smartrewind } from "./smartrewind.js";
3334
export { reverse } from "./reverse.js";
3435
export { dissolve } from "./dissolve.js";
3536
export { clip } from "./clip.js";

src/smartrewind.js

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Rewind a GeoJSON FeatureCollection. A homemade approach that tries to work in most cases. The rewindPole parameter allows forcing the rewind of polar polygons.
3+
* @param {GeoJSON} data - The input GeoJSON FeatureCollection
4+
* @param {Object} options
5+
* @param {boolean} [options.mutate=false] - If false, a copy of the GeoJSON is returned; the original is not modified
6+
* @param {boolean} [options.rewindPole=false] - If true, forces rewind of polar polygons (Antarctic/Arctic)
7+
*/
8+
export function smartrewind(data, options = {}) {
9+
const mutate = options.mutate === true;
10+
const rewindPole = options.rewindPole === true;
11+
12+
// Work on a copy if mutate is false
13+
const geo = mutate ? data : structuredClone(data);
14+
15+
for (const feature of geo.features) {
16+
let geom = feature.geometry;
17+
if (!geom) continue;
18+
19+
// Clean up geometry (remove duplicate points, invalid rings)
20+
geom = cleanGeom(geom);
21+
if (!geom) {
22+
feature.geometry = null;
23+
continue;
24+
}
25+
26+
// ---------------------
27+
// Detect polar polygons
28+
// ---------------------
29+
let isPolar = false;
30+
let minLat = Infinity;
31+
let maxLat = -Infinity;
32+
33+
const coords =
34+
geom.type === "Polygon"
35+
? geom.coordinates.flat()
36+
: geom.type === "MultiPolygon"
37+
? geom.coordinates.flat(2)
38+
: [];
39+
40+
for (const [, lat] of coords) {
41+
if (lat < minLat) minLat = lat;
42+
if (lat > maxLat) maxLat = lat;
43+
}
44+
45+
if (minLat <= -89.9) isPolar = "south";
46+
else if (maxLat >= 89.9) isPolar = "north";
47+
48+
// ---------------------
49+
// Rewind polar polygons if requested
50+
// ---------------------
51+
if (isPolar && rewindPole) {
52+
if (geom.type === "Polygon") geom.coordinates[0].reverse();
53+
else if (geom.type === "MultiPolygon")
54+
geom.coordinates.forEach((poly) => poly[0].reverse());
55+
56+
feature.geometry = geom;
57+
continue; // skip normal rewind
58+
}
59+
60+
// ---------------------
61+
// Rewind regular polygons
62+
// ---------------------
63+
if (geom.type === "Polygon")
64+
geom.coordinates = fixPolySpherical(geom.coordinates);
65+
else if (geom.type === "MultiPolygon")
66+
geom.coordinates = geom.coordinates.map(fixPolySpherical);
67+
68+
feature.geometry =
69+
geom.coordinates && geom.coordinates.length ? geom : null;
70+
}
71+
72+
return geo;
73+
}
74+
75+
// ---------------------
76+
// Helper functions
77+
// ---------------------
78+
79+
// Clean rings: remove duplicate points, close rings, remove too-short rings
80+
function cleanGeom(geom) {
81+
function cleanRing(ring) {
82+
if (!ring || ring.length < 2) return null;
83+
const cleaned = [ring[0]];
84+
for (let i = 1; i < ring.length; i++) {
85+
const [x, y] = ring[i];
86+
const [x0, y0] = cleaned[cleaned.length - 1];
87+
if (x !== x0 || y !== y0) cleaned.push(ring[i]);
88+
}
89+
const f = cleaned[0],
90+
l = cleaned[cleaned.length - 1];
91+
if (f[0] !== l[0] || f[1] !== l[1]) cleaned.push([...f]);
92+
if (cleaned.length < 4) return null;
93+
return cleaned;
94+
}
95+
96+
if (geom.type === "Polygon") {
97+
const rings = geom.coordinates.map(cleanRing).filter(Boolean);
98+
return rings.length ? { ...geom, coordinates: rings } : null;
99+
}
100+
if (geom.type === "MultiPolygon") {
101+
const polys = geom.coordinates
102+
.map((poly) => poly.map(cleanRing).filter(Boolean))
103+
.filter((p) => p.length);
104+
return polys.length ? { ...geom, coordinates: polys } : null;
105+
}
106+
return geom;
107+
}
108+
109+
// Fix polygon orientation using spherical method
110+
function fixPolySpherical(rings) {
111+
const outer = rings[0];
112+
const inners = rings.slice(1);
113+
rewindRing(outer, true); // outer ring clockwise
114+
inners.forEach((r) => rewindRing(r, false)); // inner rings counter-clockwise
115+
return [outer, ...inners];
116+
}
117+
118+
// Rewind a ring based on desired direction
119+
function rewindRing(ring, dir) {
120+
let tArea = 0,
121+
err = 0;
122+
for (let i = 0, len = ring.length, j = len - 1; i < len; j = i++) {
123+
const k = (ring[i][0] - ring[j][0]) * (ring[j][1] + ring[i][1]);
124+
const m = tArea + k;
125+
err += Math.abs(tArea) >= Math.abs(k) ? tArea - m + k : k - m + tArea;
126+
tArea = m;
127+
}
128+
if (tArea + err >= 0 !== !!dir) ring.reverse();
129+
}

0 commit comments

Comments
 (0)