From cb6c8eed284a67272bee36dacc039506cc515dc7 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Fri, 20 Sep 2024 12:52:48 +0200 Subject: [PATCH] fix(geojson): detect invalid polygon loops --- geojson/RegionCoverer_test.ts | 47 +++++++++++++++++++++++++++++++++++ geojson/loop.ts | 25 ++++++++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/geojson/RegionCoverer_test.ts b/geojson/RegionCoverer_test.ts index 73969cf..6be3c30 100644 --- a/geojson/RegionCoverer_test.ts +++ b/geojson/RegionCoverer_test.ts @@ -63,4 +63,51 @@ describe('RegionCoverer', () => { ['48795eb9', '48795ec4', '48795ed04', '48795ed0c', '48795ed74', '48795edc', '48795ee7c', '48795ee84'] ) }) + + test('polygon - twisted + contains duplicate vertices', (t) => { + const mpolygon: geojson.MultiPolygon = { + type: 'MultiPolygon', + coordinates: [ + [ + [ + [-122.420357, 37.651333], + [-122.42047100000001, 37.652073], + [-122.421204, 37.651173], + [-122.42038700000001, 37.651276], + [-122.405418, 37.634312], + [-122.400352, 37.634029], + [-122.39450100000001, 37.632862], + [-122.388313, 37.633675], + [-122.37602200000001, 37.631088], + [-122.362549, 37.638908], + [-122.360283, 37.634068], + [-122.35611, 37.614025], + [-122.353706, 37.612396], + [-122.351601, 37.61134], + [-122.349411, 37.610287], + [-122.345123, 37.607704], + [-122.34137699999999, 37.590256], + [-122.359131, 37.585941], + [-122.36784400000001, 37.600216], + [-122.37653400000001, 37.605061], + [-122.380539, 37.607029], + [-122.383797, 37.607666], + [-122.395447, 37.60276], + [-122.401848, 37.605137], + [-122.404831, 37.611164], + [-122.405632, 37.613293], + [-122.40589900000001, 37.614941], + [-122.40582999999999, 37.615002], + [-122.40129899999999, 37.625465], + [-122.405418, 37.634312], + [-122.420357, 37.651333] + ] + ] + ] + } + + const cov = new RegionCoverer() + const union = cov.covering(mpolygon) + deepEqual([...union.map(cellid.toToken)], []) // cannot be fixed, return [] + }) }) diff --git a/geojson/loop.ts b/geojson/loop.ts index 82a5ea5..88b50b9 100644 --- a/geojson/loop.ts +++ b/geojson/loop.ts @@ -20,7 +20,30 @@ 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 + + // Loops are not allowed to have any duplicate vertices (whether adjacent or not) + if (containsDuplicateVertices(ring)) { + // adjacent duplicates are fixable + ring = removeAdjacentDuplicateVertices(ring) + + // non-adjacent duplicates are not fixable + if (containsDuplicateVertices(ring)) return new Loop([]) + } + return new Loop(ring.map(position.unmarshal)) } + +/** + * Removes *adjacent* duplicate (and near-duplicate) vertices from ring. + */ +export const removeAdjacentDuplicateVertices = (ring: geojson.Position[], epsilon = 1e-8): geojson.Position[] => { + return ring.filter((p, i) => !i || !position.equal(ring.at(i - 1)!, p, epsilon)) +} + +/** + * Returns true IFF ring contains duplicate vertices at any position. + */ +export const containsDuplicateVertices = (ring: geojson.Position[]): boolean => { + return ring.some((A, i) => ring.slice(i + 1).some((B) => position.equal(A, B))) +}