From c71b8d05e78b5c025770682ea4e3d80159f9a6a6 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Thu, 19 Sep 2024 12:04:11 +0200 Subject: [PATCH] fix(geojson): strip duplicate adjacent polygon ring vertices --- geojson/RegionCoverer_test.ts | 66 +++++++++++++++++++++++++++++++++++ geojson/loop.ts | 1 + geojson/position.ts | 7 ++++ geojson/testing.ts | 11 +++--- 4 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 geojson/RegionCoverer_test.ts diff --git a/geojson/RegionCoverer_test.ts b/geojson/RegionCoverer_test.ts new file mode 100644 index 0000000..73969cf --- /dev/null +++ b/geojson/RegionCoverer_test.ts @@ -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'] + ) + }) +}) diff --git a/geojson/loop.ts b/geojson/loop.ts index ac46a96..82a5ea5 100644 --- a/geojson/loop.ts +++ b/geojson/loop.ts @@ -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)) } diff --git a/geojson/position.ts b/geojson/position.ts index 17b5658..2fd8570 100644 --- a/geojson/position.ts +++ b/geojson/position.ts @@ -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 +} diff --git a/geojson/testing.ts b/geojson/testing.ts index b62bf47..5d31564 100644 --- a/geojson/testing.ts +++ b/geojson/testing.ts @@ -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': { @@ -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)) }) }