diff --git a/geojson/RegionCoverer.ts b/geojson/RegionCoverer.ts index 328eb68..3edc75f 100644 --- a/geojson/RegionCoverer.ts +++ b/geojson/RegionCoverer.ts @@ -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. @@ -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) @@ -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 */ @@ -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 + } } diff --git a/geojson/RegionCoverer_test.ts b/geojson/RegionCoverer_test.ts index 6be3c30..77a9783 100644 --- a/geojson/RegionCoverer_test.ts +++ b/geojson/RegionCoverer_test.ts @@ -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']) + }) })