Skip to content

Commit c71b8d0

Browse files
committed
fix(geojson): strip duplicate adjacent polygon ring vertices
1 parent 67d6471 commit c71b8d0

4 files changed

Lines changed: 78 additions & 7 deletions

File tree

geojson/RegionCoverer_test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type * as geojson from 'geojson'
2+
import { test, describe } from 'node:test'
3+
import { deepEqual } from 'node:assert/strict'
4+
import { RegionCoverer } from './RegionCoverer'
5+
import * as cellid from '../s2/cellid'
6+
7+
describe('RegionCoverer', () => {
8+
test('polygon - incorrect winding + duplicate adjacent vertices', (t) => {
9+
const polygon: geojson.Polygon = {
10+
type: 'Polygon',
11+
coordinates: [
12+
[
13+
[-1.599437, 53.803895],
14+
[-1.598511, 53.803895],
15+
[-1.595764, 53.803895],
16+
[-1.593018, 53.803895],
17+
[-1.593018, 53.802273],
18+
[-1.590271, 53.802273],
19+
[-1.587524, 53.802273],
20+
[-1.585241, 53.802273],
21+
[-1.584778, 53.802273],
22+
[-1.582031, 53.802273],
23+
[-1.582031, 53.801097],
24+
[-1.582031, 53.800651],
25+
[-1.579285, 53.800651],
26+
[-1.576538, 53.800651],
27+
[-1.576538, 53.799029],
28+
[-1.576538, 53.797406],
29+
[-1.577464, 53.797406],
30+
[-1.577464, 53.797406],
31+
[-1.581105, 53.797406],
32+
[-1.581424, 53.795784],
33+
[-1.584778, 53.795784],
34+
[-1.584778, 53.794162],
35+
[-1.584778, 53.794144],
36+
[-1.587524, 53.792594],
37+
[-1.587524, 53.790917],
38+
[-1.587524, 53.790917],
39+
[-1.592091, 53.790917],
40+
[-1.592091, 53.790917],
41+
[-1.593018, 53.790917],
42+
[-1.593018, 53.792539],
43+
[-1.595764, 53.792539],
44+
[-1.596722, 53.794162],
45+
[-1.596722, 53.794162],
46+
[-1.595764, 53.795784],
47+
[-1.595764, 53.797406],
48+
[-1.595764, 53.799029],
49+
[-1.595764, 53.800651],
50+
[-1.598511, 53.800651],
51+
[-1.599205, 53.801827],
52+
[-1.599118, 53.802273],
53+
[-1.599437, 53.803895],
54+
[-1.599437, 53.803895]
55+
]
56+
]
57+
}
58+
59+
const cov = new RegionCoverer()
60+
const union = cov.covering(polygon)
61+
deepEqual(
62+
[...union.map(cellid.toToken)],
63+
['48795eb9', '48795ec4', '48795ed04', '48795ed0c', '48795ed74', '48795edc', '48795ee7c', '48795ee84']
64+
)
65+
})
66+
})

geojson/loop.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const marshal = (loop: Loop, ordinal: number): geojson.Position[] => {
2020
export const unmarshal = (ring: geojson.Position[], ordinal: number): Loop => {
2121
ring = ring.slice() // make a copy to avoid mutating input
2222
ring.length -= 1 // remove matching start/end points
23+
ring = ring.filter((p, i) => !i || !position.equal(ring.at(i - 1)!, p, 0)) // remove equal+adjacent vertices
2324
if (ordinal > 0) ring.reverse() // ensure all rings are CCW
2425
return new Loop(ring.map(position.unmarshal))
2526
}

geojson/position.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,10 @@ export const marshal = (point: Point): geojson.Position => {
1919
export const unmarshal = (position: geojson.Position): Point => {
2020
return Point.fromLatLng(LatLng.fromDegrees(position[1], position[0]))
2121
}
22+
23+
/**
24+
* Returns true IFF the two positions are equal.
25+
*/
26+
export const equal = (a: geojson.Position, b: geojson.Position, epsilon = 0) => {
27+
return Math.abs(a[0] - b[0]) <= epsilon && Math.abs(a[1] - b[1]) <= epsilon
28+
}

geojson/testing.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
11
import type * as geojson from 'geojson'
2+
import * as position from './position'
23

34
// default distance threshold for approx equality
45
const EPSILON = 1e-13
56

6-
export const approxEqualPosition = (a: geojson.Position, b: geojson.Position, epsilon = EPSILON) => {
7-
return Math.abs(a[0] - b[0]) <= epsilon && Math.abs(a[1] - b[1]) <= epsilon
8-
}
9-
107
export const approxEqual = (a: geojson.Geometry, b: geojson.Geometry, epsilon = EPSILON) => {
118
if (a?.type !== b?.type) return false
129
switch (a.type) {
1310
case 'Point': {
1411
const aa = a as geojson.Point
1512
const bb = b as geojson.Point
16-
return approxEqualPosition(aa.coordinates, bb.coordinates, epsilon)
13+
return position.equal(aa.coordinates, bb.coordinates, epsilon)
1714
}
1815

1916
case 'LineString': {
2017
const aa = a as geojson.LineString
2118
const bb = b as geojson.LineString
2219
if (aa.coordinates.length !== bb.coordinates.length) return false
23-
return aa.coordinates.every((c, i) => approxEqualPosition(c, bb.coordinates[i], epsilon))
20+
return aa.coordinates.every((c, i) => position.equal(c, bb.coordinates[i], epsilon))
2421
}
2522

2623
case 'Polygon': {
@@ -29,7 +26,7 @@ export const approxEqual = (a: geojson.Geometry, b: geojson.Geometry, epsilon =
2926
if (aa.coordinates.length !== bb.coordinates.length) return false
3027
return aa.coordinates.every((r, ri) => {
3128
if (r.length !== bb.coordinates[ri].length) return false
32-
return r.every((c, ci) => approxEqualPosition(c, bb.coordinates[ri][ci], epsilon))
29+
return r.every((c, ci) => position.equal(c, bb.coordinates[ri][ci], epsilon))
3330
})
3431
}
3532

0 commit comments

Comments
 (0)