diff --git a/geojson/RegionCoverer.ts b/geojson/RegionCoverer.ts index 3edc75f..ac9e69d 100644 --- a/geojson/RegionCoverer.ts +++ b/geojson/RegionCoverer.ts @@ -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' @@ -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 @@ -104,9 +111,13 @@ 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 } @@ -114,6 +125,7 @@ export class RegionCoverer { 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 } diff --git a/geojson/RegionCoverer_test.ts b/geojson/RegionCoverer_test.ts index 77a9783..48d0b60 100644 --- a/geojson/RegionCoverer_test.ts +++ b/geojson/RegionCoverer_test.ts @@ -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' @@ -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',