Skip to content

Commit

Permalink
fix(geojson): avoid global coverings for invalid geometries
Browse files Browse the repository at this point in the history
  • Loading branch information
missinglink committed Sep 20, 2024
1 parent cb6c8ee commit 0329f64
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 5 deletions.
30 changes: 25 additions & 5 deletions geojson/RegionCoverer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Polygon } from '../s2/Polygon'
import type { Region } from '../s2/Region'
import type { RegionCovererOptions as S2RegionCovererOptions } from '../s2/RegionCoverer'
import { RegionCoverer as S2RegionCoverer } from '../s2/RegionCoverer'
import * as cellid from '../s2/cellid'

/**
* RegionCovererOptions allows the RegionCoverer to be configured.
Expand Down Expand Up @@ -82,12 +83,13 @@ export class RegionCoverer {
shapes.forEach((shape: Region) => {
// optionally elect to use a fast covering method for small areas
const fast = union.length >= this.memberCoverer.maxCells && RegionCoverer.area(shape) < this.smallAreaEpsilon
const cov = fast ? this.memberCoverer.fastCovering(shape) : this.memberCoverer.covering(shape)

// discard errorneous members which cover the entire planet
if (!RegionCoverer.validCovering(shape, cov)) return

// append covering to union
union = CellUnion.fromUnion(
union,
fast ? this.memberCoverer.fastCovering(shape) : this.memberCoverer.covering(shape)
)
union = CellUnion.fromUnion(union, cov)

// force compact large coverings to avoid OOM errors
if (union.length >= this.compactAt) union = this.coverer.covering(union)
Expand All @@ -101,7 +103,11 @@ export class RegionCoverer {
covering(geometry: geojson.Geometry): CellUnion {
const shape = fromGeoJSON(geometry)
if (Array.isArray(shape)) return this.mutliMemberCovering(shape as Region[])
return this.coverer.covering(shape)

// 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 */
Expand All @@ -110,4 +116,18 @@ export class RegionCoverer {
if (shape instanceof Polyline) shape.capBound().area()
return 0
}

/** Attempts to detect invalid geometries which produce global coverings */
private static validCovering(shape: Region, covering: CellUnion): boolean {
if (covering.length !== 6 || !covering.every(cellid.isFace)) return true

// compare the polygon covering with a covering of the outer ring as a linestring
if (shape instanceof Polygon) {
const union = new Polyline(shape.loop(0).vertices).cellUnionBound()
return union.length === 6 && union.every(cellid.isFace)
}

// area is too small to have a global covering
return this.area(shape) < Math.PI * 2
}
}
80 changes: 80 additions & 0 deletions geojson/RegionCoverer_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,84 @@ describe('RegionCoverer', () => {
const union = cov.covering(mpolygon)
deepEqual([...union.map(cellid.toToken)], []) // cannot be fixed, return []
})

test('polygon - should not generate global covering', (t) => {
const polygon: geojson.Polygon = {
type: 'Polygon',
coordinates: [
[
[-77.053846, 38.906842],
[-77.053847, 38.90684000000001],
[-77.053849, 38.906836000000006],
[-77.053846, 38.906842]
]
]
}
const cov = new RegionCoverer()
const union = cov.covering(polygon)
deepEqual([...union.map(cellid.toToken)], [])
})

test('multipolygon - should not generate global covering', (t) => {
const mpolygon: geojson.MultiPolygon = {
type: 'MultiPolygon',
coordinates: [
[
[
[-77.053846, 38.906842],
[-77.053847, 38.90684000000001],
[-77.053849, 38.906836000000006],
[-77.053846, 38.906842]
]
]
]
}
const cov = new RegionCoverer()
const union = cov.covering(mpolygon)
deepEqual([...union.map(cellid.toToken)], [])
})

test('linestring - should generate covering', (t) => {
const linestring: geojson.LineString = {
type: 'LineString',
coordinates: [
[-77.053846, 38.906842],
[-77.053847, 38.90684000000001],
[-77.053849, 38.906836000000006]
]
}
const cov = new RegionCoverer()
const union = cov.covering(linestring)
deepEqual(
[...union.map(cellid.toToken)],
[
'89b7b7b50b756d',
'89b7b7b50b7571',
'89b7b7b50b75724',
'89b7b7b50b757b',
'89b7b7b50b757c04',
'89b7b7b50b757f',
'89b7b7b50b9fd35',
'89b7b7b50b9fd5'
]
)
})

test('polygon - should generate global covering', (t) => {
const polygon: geojson.Polygon = {
type: 'Polygon',
coordinates: [
[
[-170, 69],
[170, 70],
[170, -70],
[-170, -70],
[-170, 69]
]
]
}
const cov = new RegionCoverer()
const union = cov.covering(polygon)
deepEqual([...union.map(cellid.toToken)], ['1', '3', '5', '7', '9', 'b'])
})
})

0 comments on commit 0329f64

Please sign in to comment.