Skip to content

Commit

Permalink
fix(geojson): strip duplicate adjacent polygon ring vertices
Browse files Browse the repository at this point in the history
  • Loading branch information
missinglink committed Sep 19, 2024
1 parent 67d6471 commit c71b8d0
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 7 deletions.
66 changes: 66 additions & 0 deletions geojson/RegionCoverer_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type * as geojson from 'geojson'
import { test, describe } from 'node:test'
import { deepEqual } from 'node:assert/strict'
import { RegionCoverer } from './RegionCoverer'
import * as cellid from '../s2/cellid'

describe('RegionCoverer', () => {
test('polygon - incorrect winding + duplicate adjacent vertices', (t) => {
const polygon: geojson.Polygon = {
type: 'Polygon',
coordinates: [
[
[-1.599437, 53.803895],
[-1.598511, 53.803895],
[-1.595764, 53.803895],
[-1.593018, 53.803895],
[-1.593018, 53.802273],
[-1.590271, 53.802273],
[-1.587524, 53.802273],
[-1.585241, 53.802273],
[-1.584778, 53.802273],
[-1.582031, 53.802273],
[-1.582031, 53.801097],
[-1.582031, 53.800651],
[-1.579285, 53.800651],
[-1.576538, 53.800651],
[-1.576538, 53.799029],
[-1.576538, 53.797406],
[-1.577464, 53.797406],
[-1.577464, 53.797406],
[-1.581105, 53.797406],
[-1.581424, 53.795784],
[-1.584778, 53.795784],
[-1.584778, 53.794162],
[-1.584778, 53.794144],
[-1.587524, 53.792594],
[-1.587524, 53.790917],
[-1.587524, 53.790917],
[-1.592091, 53.790917],
[-1.592091, 53.790917],
[-1.593018, 53.790917],
[-1.593018, 53.792539],
[-1.595764, 53.792539],
[-1.596722, 53.794162],
[-1.596722, 53.794162],
[-1.595764, 53.795784],
[-1.595764, 53.797406],
[-1.595764, 53.799029],
[-1.595764, 53.800651],
[-1.598511, 53.800651],
[-1.599205, 53.801827],
[-1.599118, 53.802273],
[-1.599437, 53.803895],
[-1.599437, 53.803895]
]
]
}

const cov = new RegionCoverer()
const union = cov.covering(polygon)
deepEqual(
[...union.map(cellid.toToken)],
['48795eb9', '48795ec4', '48795ed04', '48795ed0c', '48795ed74', '48795edc', '48795ee7c', '48795ee84']
)
})
})
1 change: 1 addition & 0 deletions geojson/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const marshal = (loop: Loop, ordinal: number): geojson.Position[] => {
export const unmarshal = (ring: geojson.Position[], ordinal: number): Loop => {
ring = ring.slice() // make a copy to avoid mutating input
ring.length -= 1 // remove matching start/end points
ring = ring.filter((p, i) => !i || !position.equal(ring.at(i - 1)!, p, 0)) // remove equal+adjacent vertices
if (ordinal > 0) ring.reverse() // ensure all rings are CCW
return new Loop(ring.map(position.unmarshal))
}
7 changes: 7 additions & 0 deletions geojson/position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ export const marshal = (point: Point): geojson.Position => {
export const unmarshal = (position: geojson.Position): Point => {
return Point.fromLatLng(LatLng.fromDegrees(position[1], position[0]))
}

/**
* Returns true IFF the two positions are equal.
*/
export const equal = (a: geojson.Position, b: geojson.Position, epsilon = 0) => {
return Math.abs(a[0] - b[0]) <= epsilon && Math.abs(a[1] - b[1]) <= epsilon
}
11 changes: 4 additions & 7 deletions geojson/testing.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import type * as geojson from 'geojson'
import * as position from './position'

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

export const approxEqualPosition = (a: geojson.Position, b: geojson.Position, epsilon = EPSILON) => {
return Math.abs(a[0] - b[0]) <= epsilon && Math.abs(a[1] - b[1]) <= epsilon
}

export const approxEqual = (a: geojson.Geometry, b: geojson.Geometry, epsilon = EPSILON) => {
if (a?.type !== b?.type) return false
switch (a.type) {
case 'Point': {
const aa = a as geojson.Point
const bb = b as geojson.Point
return approxEqualPosition(aa.coordinates, bb.coordinates, epsilon)
return position.equal(aa.coordinates, bb.coordinates, epsilon)
}

case 'LineString': {
const aa = a as geojson.LineString
const bb = b as geojson.LineString
if (aa.coordinates.length !== bb.coordinates.length) return false
return aa.coordinates.every((c, i) => approxEqualPosition(c, bb.coordinates[i], epsilon))
return aa.coordinates.every((c, i) => position.equal(c, bb.coordinates[i], epsilon))
}

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

Expand Down

0 comments on commit c71b8d0

Please sign in to comment.