Skip to content

Commit

Permalink
fix(geojson): ignore zero area geometries
Browse files Browse the repository at this point in the history
  • Loading branch information
missinglink committed Sep 22, 2024
1 parent 590c589 commit 95881ab
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 2 deletions.
14 changes: 13 additions & 1 deletion geojson/RegionCoverer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CellUnion } from '../s2/CellUnion'
import { fromGeoJSON } from './geometry'
import { Polyline } from '../s2/Polyline'
import { Polygon } from '../s2/Polygon'
import { Rect } from '../s2/Rect'
import type { Region } from '../s2/Region'
import type { RegionCovererOptions as S2RegionCovererOptions } from '../s2/RegionCoverer'
import { RegionCoverer as S2RegionCoverer } from '../s2/RegionCoverer'
Expand Down Expand Up @@ -81,8 +82,14 @@ export class RegionCoverer {

let union = new CellUnion()
shapes.forEach((shape: Region) => {
const area = RegionCoverer.area(shape)
const isPolygon = shape instanceof Polygon

// discard zero-area polygons
if (isPolygon && area <= 0) return

// optionally elect to use a fast covering method for small areas
const fast = union.length >= this.memberCoverer.maxCells && RegionCoverer.area(shape) < this.smallAreaEpsilon
const fast = union.length >= this.memberCoverer.maxCells && area < this.smallAreaEpsilon
const cov = fast ? this.memberCoverer.fastCovering(shape) : this.memberCoverer.covering(shape)

// discard errorneous members which cover the entire planet
Expand All @@ -104,16 +111,21 @@ export class RegionCoverer {
const shape = fromGeoJSON(geometry)
if (Array.isArray(shape)) return this.mutliMemberCovering(shape as Region[])

// discard zero-area polygons
if (shape instanceof Polygon && RegionCoverer.area(shape) <= 0) return new CellUnion()

// discard errorneous shapes which cover the entire planet
const cov = this.coverer.covering(shape)
if (!RegionCoverer.validCovering(shape, cov)) return new CellUnion()

return cov
}

/** Computes the area of a shape */
private static area(shape: Region): number {
if (shape instanceof Polygon) return shape.area()
if (shape instanceof Polyline) shape.capBound().area()
if (shape instanceof Rect) shape.capBound().area()
return 0
}

Expand Down
116 changes: 115 additions & 1 deletion geojson/RegionCoverer_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type * as geojson from 'geojson'
import { test, describe } from 'node:test'
import { deepEqual } from 'node:assert/strict'
import { ok, deepEqual } from 'node:assert/strict'
import { RegionCoverer } from './RegionCoverer'
import * as cellid from '../s2/cellid'

Expand Down Expand Up @@ -147,6 +147,120 @@ describe('RegionCoverer', () => {
deepEqual([...union.map(cellid.toToken)], [])
})

test('multipolygon - second ring is invalid & should be ignored', (t) => {
const mpolygon: geojson.MultiPolygon = {
type: 'MultiPolygon',
coordinates: [
[
[
[-82.463379, 28.07198],
[-82.468872, 28.07198],
[-82.468872, 28.07198],
[-82.474369, 28.07198],
[-82.473222, 28.070161],
[-82.469386, 28.057119],
[-82.470504, 28.055128],
[-82.474365, 28.054253],
[-82.475276, 28.054321],
[-82.479858, 28.052591],
[-82.481073, 28.052591],
[-82.485352, 28.047743],
[-82.485352, 28.047743],
[-82.485352, 28.047743],
[-82.489631, 28.042894],
[-82.490355, 28.042894],
[-82.493591, 28.044561],
[-82.497859, 28.043243],
[-82.502753, 28.037698],
[-82.507658, 28.035846],
[-82.508323, 28.0346],
[-82.508892, 28.032724],
[-82.509084, 28.030773],
[-82.508892, 28.028823],
[-82.508323, 28.026947],
[-82.507399, 28.025218],
[-82.506156, 28.023702],
[-82.50464, 28.022459],
[-82.502911, 28.021535],
[-82.501035, 28.020966],
[-82.499084, 28.020773],
[-82.497134, 28.020966],
[-82.495258, 28.021535],
[-82.493529, 28.022459],
[-82.492013, 28.023702],
[-82.49077, 28.025218],
[-82.490395, 28.025919],
[-82.490377, 28.025914],
[-82.490049, 28.025814],
[-82.488098, 28.025622],
[-82.486147, 28.025814],
[-82.484271, 28.026383],
[-82.482542, 28.027307],
[-82.481027, 28.028551],
[-82.479858, 28.029975],
[-82.47869, 28.028551],
[-82.478443, 28.028349],
[-82.468266, 28.028349],
[-82.466125, 28.033198],
[-82.466125, 28.034935],
[-82.463379, 28.038046],
[-82.463379, 28.038046],
[-82.463379, 28.041511],
[-82.463379, 28.042895],
[-82.463379, 28.047047],
[-82.463225, 28.047569],
[-82.460632, 28.046981],
[-82.454902, 28.04828],
[-82.45407, 28.050167],
[-82.455139, 28.052591],
[-82.455139, 28.057438],
[-82.45565, 28.058595],
[-82.46045, 28.061315],
[-82.45935, 28.070039],
[-82.458494, 28.07198],
[-82.463379, 28.07198],
[-82.463379, 28.07198]
]
],
[
[
[-82.505951, 28.075921],
[-82.507902, 28.075729],
[-82.509778, 28.07516],
[-82.511507, 28.074236],
[-82.513022, 28.072993],
[-82.514266, 28.071477],
[-82.51519, 28.069748],
[-82.515759, 28.067872],
[-82.515951, 28.065921],
[-82.515759, 28.063971],
[-82.51519, 28.062095],
[-82.514644, 28.061074],
[-82.513285, 28.061074],
[-82.506411, 28.062632],
[-82.505495, 28.062286],
[-82.501831, 28.060902],
[-82.499539, 28.060036],
[-82.496338, 28.061591],
[-82.495524, 28.062513],
[-82.4957, 28.06471],
[-82.495524, 28.066905],
[-82.503457, 28.075897],
[-82.504578, 28.075786],
[-82.505951, 28.075921]
]
]
]
}
const cov = new RegionCoverer({ maxLevel: 15 })
const union = cov.covering(mpolygon)
ok(!union.includes(1152921505680588800n), 'contains null island cellid') // null island
deepEqual(
[...union.map(cellid.toToken)],
['88c2c08b4', '88c2c08d', '88c2c093', '88c2c095', '88c2c0c1', '88c2c0c3', '88c2c0ec', '88c2c0f4']
)
})

test('linestring - should generate covering', (t) => {
const linestring: geojson.LineString = {
type: 'LineString',
Expand Down

0 comments on commit 95881ab

Please sign in to comment.