diff --git a/CHANGELOG.md b/CHANGELOG.md index 22adf97..eb579b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # @koopjs/geoservice-utils +### Unreleased +### Changed +- constrains any WGS84 out-of-bounds coordinates to their known limits (e.g., -95 latitude -> -90 latitude). +- WGS84 latitudes of -90 or 90 are modified to -89.99999 and -89.99999, respectively, allow reprojection to proceed as expected. + ## 2.2.3 ### Fixed - add missing WKT-parser dependency to package.json diff --git a/src/standardize-geometry-filter/clip-to-envelope.spec.ts b/src/standardize-geometry-filter/clip-to-envelope.spec.ts new file mode 100644 index 0000000..5bc25c9 --- /dev/null +++ b/src/standardize-geometry-filter/clip-to-envelope.spec.ts @@ -0,0 +1,8 @@ +import { clipToEnvelope } from './clip-to-envelope'; + +describe('clipToBounds', () => { + test('clipToBoundsOfSpatialReference', () => { + const result = clipToEnvelope([[-190, 95], [-185, 45]], {ymin: -90, ymax: 90, xmin: -180, xmax: 180}) + expect(result).toEqual([[-180, 90], [-180, 45]]); + }); +}); diff --git a/src/standardize-geometry-filter/clip-to-envelope.ts b/src/standardize-geometry-filter/clip-to-envelope.ts new file mode 100644 index 0000000..40391c3 --- /dev/null +++ b/src/standardize-geometry-filter/clip-to-envelope.ts @@ -0,0 +1,26 @@ +import { transformCoordinates } from './traverse-coordinates'; +import { Coordinates } from './common-types'; +import { IEnvelope } from '@esri/arcgis-rest-types'; + +export function clipToEnvelope(coordinates: Coordinates , envelope: IEnvelope): Coordinates { + const { xmin, xmax, ymin, ymax } = envelope; + + const repositionInvalidCoordinates = (coords): Coordinates => { + const [lon, lat] = coords; + return [constrainNumber(lon, xmax, xmin), constrainNumber(lat, ymax, ymin)]; + }; + + return transformCoordinates(coordinates, repositionInvalidCoordinates); +} + +function constrainNumber (num:number, max:number, min:number): number { + if (num > max) { + return max; + } + + if (num < min) { + return min; + } + + return num; +} \ No newline at end of file diff --git a/src/standardize-geometry-filter/common-types.ts b/src/standardize-geometry-filter/common-types.ts new file mode 100644 index 0000000..a01efd5 --- /dev/null +++ b/src/standardize-geometry-filter/common-types.ts @@ -0,0 +1,40 @@ +import { + LineString, + MultiLineString, + MultiPoint, + MultiPolygon, + Point, + Polygon, + Position +} from 'geojson'; +import { + IEnvelope, + IPoint, + IPolyline, + IPolygon, + ISpatialReference, +} from '@esri/arcgis-rest-types'; + +export type Geometry = + | Point + | MultiPoint + | LineString + | MultiLineString + | Polygon + | MultiPolygon; + +export type GeometryFilter = IEnvelope | IPoint | IPolyline | IPolygon | number[]; + +export type Coordinates = Position | Position[] | Position[][] | Position[][][]; + +export type ArcgisSpatialReference = number | ISpatialReference; + +export type SpatialReference = { + wkid?: number; + latestWkid?: number; + wkt?: string; + vcsWkid?: number; + latestVcsWkid?: number; +}; + +export type SpatialReferenceInput = string | number | SpatialReference; diff --git a/src/standardize-geometry-filter/helpers.ts b/src/standardize-geometry-filter/helpers.ts index 181dcdb..d3dcb53 100644 --- a/src/standardize-geometry-filter/helpers.ts +++ b/src/standardize-geometry-filter/helpers.ts @@ -1,10 +1,15 @@ import joi from 'joi'; -import { - IEnvelope, - IPoint, - IPolyline, - IPolygon, -} from '@esri/arcgis-rest-types'; +import * as esriProjCodes from '@esri/proj-codes'; +import { GeometryFilter } from './common-types'; + +export const spatialReferenceSchema = joi.object({ + wkid: joi.number().strict().integer().optional(), + latestWkid: joi.number().strict().integer().optional(), + vcsWkid: joi.number().strict().integer().optional(), + latestVcsWkid: joi.number().strict().integer().optional(), + wkt: joi.string().optional(), +}).unknown() + .or('wkid', 'latestWkid', 'vcsWkid', 'latestVcsWkid', 'wkt') const envelopeSchema = joi .object({ @@ -12,12 +17,7 @@ const envelopeSchema = joi ymax: joi.number().strict().required(), xmin: joi.number().strict().required(), xmax: joi.number().strict().required(), - spatialReference: joi - .object({ - wkid: joi.number().strict().required(), - }) - .unknown(true) - .optional(), + spatialReference: spatialReferenceSchema.optional() }) .unknown(true); @@ -112,37 +112,23 @@ export const filterSchema = joi ) .required(); -type GeometryFilter = - | IEnvelope - | IPoint - | IPolyline - | IPolygon - | number[] - | string; - -function passesValidation(schema: joi.Schema, input: GeometryFilter): boolean { - const { error } = schema.validate(input); - return !error; -} - -export function isArcgisObject(input: GeometryFilter): boolean { - return ( - passesValidation(envelopeSchema, input) || - passesValidation(pointSchema, input) || - passesValidation(lineStringSchema, input) || - passesValidation(polygonSchema, input) || - passesValidation(multiPointSchema, input) || - passesValidation(multiLineStringSchema, input) || - passesValidation(multipolygonSchema, input) - ); -} - -export function isSinglePointArray(input: GeometryFilter): boolean { +export function isSinglePointArray(input: GeometryFilter | string): boolean { const { error } = pointArraySchema.validate(input); return !error; } -export function isEnvelopeArray(input: GeometryFilter): boolean { +export function isEnvelopeArray(input: GeometryFilter | string): boolean { const { error } = envelopeArraySchema.validate(input); return !error; } + +const wgs = esriProjCodes.lookup(4326); + +export const wgsWkt = wgs.wkt; + +export const wgsExtentEnvelope = { + ymin: wgs.extent.slat, + ymax: wgs.extent.nlat, + xmin: wgs.extent.llon, + xmax: wgs.extent.rlon +}; \ No newline at end of file diff --git a/src/standardize-geometry-filter/index.spec.ts b/src/standardize-geometry-filter/index.spec.ts index 6ebc11d..3a44ba4 100644 --- a/src/standardize-geometry-filter/index.spec.ts +++ b/src/standardize-geometry-filter/index.spec.ts @@ -1,8 +1,21 @@ -const { standardizeGeometryFilter } = require('./'); +import { ISpatialReference } from '@esri/arcgis-rest-types'; +import { standardizeGeometryFilter } from './'; +import * as projCodes from '@esri/proj-codes'; +import { GeometryFilter } from './common-types'; +jest.mock('@esri/proj-codes', () => ({ + __esModule: true, + // @ts-ignore + ...jest.requireActual('@esri/proj-codes'), +})); + +const mockLogger = describe('standardizeGeometryFilter', () => { + afterAll(() => { + jest.resetAllMocks(); + }); test('delimited point', () => { - const result = standardizeGeometryFilter({ geometry: '-123, 48'}); + const result = standardizeGeometryFilter({ geometry: '-123, 48' }); expect(result).toEqual({ geometry: { coordinates: [-123, 48], @@ -14,7 +27,8 @@ describe('standardizeGeometryFilter', () => { }); test('delimited point with options', () => { - const result = standardizeGeometryFilter({ geometry: '-123, 48', + const result = standardizeGeometryFilter({ + geometry: '-123, 48', inSR: 4326, reprojectionSR: 3857, spatialRel: 'esriSpatialRelIntersects', @@ -25,12 +39,17 @@ describe('standardizeGeometryFilter', () => { type: 'Point', }, relation: 'esriSpatialRelIntersects', - spatialReference: 3857, + spatialReference: { + wkid: 3857, + wkt: 'PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",0.0],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]', + }, }); }); test('delimited bbox', () => { - const result = standardizeGeometryFilter({ geometry: '-123, 48, -122, 49' }); + const result = standardizeGeometryFilter({ + geometry: '-123, 48, -122, 49', + }); expect(result).toEqual({ geometry: { coordinates: [ @@ -50,7 +69,7 @@ describe('standardizeGeometryFilter', () => { }); test('point', () => { - const result = standardizeGeometryFilter({ geometry: { x: -123, y: 48 }}); + const result = standardizeGeometryFilter({ geometry: { x: -123, y: 48 } }); expect(result).toEqual({ geometry: { coordinates: [-123, 48], @@ -62,12 +81,14 @@ describe('standardizeGeometryFilter', () => { }); test('envelope object without spatial reference', () => { - const result = standardizeGeometryFilter({ geometry: { - xmin: -123, - xmax: -122, - ymin: 48, - ymax: 49 - }}); + const result = standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 48, + ymax: 49, + }, + }); expect(result).toEqual({ geometry: { coordinates: [ @@ -87,13 +108,15 @@ describe('standardizeGeometryFilter', () => { }); test('envelope object with spatial reference', () => { - const result = standardizeGeometryFilter({ geometry: { - xmin: -123, - xmax: -122, - ymin: 48, - ymax: 49, - spatialReference: { wkid: 4326, latestWkid: 9999 } - }}); + const result = standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 48, + ymax: 49, + spatialReference: { wkid: 4326 }, + }, + }); expect(result).toEqual({ geometry: { coordinates: [ @@ -108,18 +131,283 @@ describe('standardizeGeometryFilter', () => { type: 'Polygon', }, relation: 'esriSpatialRelIntersects', - spatialReference: { wkid: 4326, latestWkid: 9999 }, + spatialReference: { + wkid: 4326, + wkt: 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]', + }, + }); + }); + + test('envelope object with spatial reference and reproject', () => { + const result = standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 48, + ymax: 49, + spatialReference: { wkid: 4326 }, + }, + reprojectionSR: 3857, + }); + expect(result).toEqual({ + geometry: { + coordinates: [ + [ + [-13580977.876779376, 6274861.394006576], + [-13692297.36757265, 6274861.394006576], + [-13692297.36757265, 6106854.834885075], + [-13580977.876779376, 6106854.834885075], + [-13580977.876779376, 6274861.394006576], + ], + ], + type: 'Polygon', + }, + relation: 'esriSpatialRelIntersects', + spatialReference: { + wkid: 3857, + wkt: 'PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",0.0],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]', + }, + }); + }); + + test('envelope object with unsupported spatial reference', () => { + try { + const result = standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 48, + ymax: 49, + spatialReference: 'foo-bar' as unknown as ISpatialReference, + }, + }); + throw new Error('should have thrown'); + } catch (error) { + expect(error.message).toBe( + 'Unsupported inSR format; must be a spatial reference ID or object', + ); + } + }); + + test('envelope object with unsupported spatial reference wkt', () => { + try { + const result = standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 48, + ymax: 49, + spatialReference: { wkt: 'foo-bar' }, + }, + }); + throw new Error('should have thrown'); + } catch (error) { + expect(error.message).toBe( + 'Spatial reference WKT is unparseable: "foo-bar"', + ); + } + }); + + test('envelope object with unknown spatial reference', () => { + const result = standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 48, + ymax: 49, + }, + inSR: 9999 + }); + + expect(result).toEqual({ + geometry: { + coordinates: [ + [ + [-122, 49], + [-123, 49], + [-123, 48], + [-122, 48], + [-122, 49], + ], + ], + type: 'Polygon', + }, + relation: 'esriSpatialRelIntersects', + spatialReference: undefined, + }); + }); + + test('envelope object with WGS84 spatial reference and clip option', () => { + const result = standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: -95, + ymax: 95, + spatialReference: { wkid: 4326 }, + }, + }); + expect(result).toEqual({ + geometry: { + coordinates: [ + [ + [-122, 90], + [-123, 90], + [-123, -90], + [-122, -90], + [-122, 90], + ], + ], + type: 'Polygon', + }, + relation: 'esriSpatialRelIntersects', + spatialReference: { + wkid: 4326, + wkt: 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]', + }, + }); + }); + + test('envelope object without spatial reference and has clip option', () => { + const result = standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 48, + ymax: 49, + }, + }); + expect(result).toEqual({ + geometry: { + coordinates: [ + [ + [-122, 49], + [-123, 49], + [-123, 48], + [-122, 48], + [-122, 49], + ], + ], + type: 'Polygon', + }, + relation: 'esriSpatialRelIntersects', + spatialReference: undefined, + }); + }); + + test('envelope object with reprojection spatial reference', () => { + const result = standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 45, + ymax: 90, + spatialReference: { wkid: 4326 }, + }, + reprojectionSR: 3857, + }); + expect(result).toEqual({ + geometry: { + coordinates: [ + [ + [-13580977.876779376, 147730758.19456753], + [-13692297.36757265, 147730758.19456753], + [-13692297.36757265, 5621521.486192066], + [-13580977.876779376, 5621521.486192066], + [-13580977.876779376, 147730758.19456753], + ], + ], + type: 'Polygon', + }, + relation: 'esriSpatialRelIntersects', + spatialReference: { + wkid: 3857, + wkt: 'PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",0.0],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]', + }, + }); + }); + + test('envelope object with reprojection spatial reference, without source spatial reference', () => { + try { + const result = standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 45, + ymax: 90, + }, + reprojectionSR: 3857, + }); + throw new Error('should have thrown'); + } catch (error) { + expect(error.message).toBe( + 'Unknown geometry filter spatial reference; unable to reproject', + ); + } + }); + + test('envelope object with unknown reprojection spatial reference', () => { + try { + const result = standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 45, + ymax: 90, + }, + inSR: 4326, + reprojectionSR: 99999, + }); + throw new Error('should have thrown'); + } catch (error) { + expect(error.message).toBe( + 'Unknown reprojection spatial reference; unable to reproject', + ); + } + }); + + test('envelope object with WKT spatial reference', () => { + const result = standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 43, + ymax: 44, + spatialReference: { + wkt: `GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]`, + }, + }, + }); + expect(result).toEqual({ + geometry: { + coordinates: [ + [ + [-122, 44], + [-123, 44], + [-123, 43], + [-122, 43], + [-122, 44], + ], + ], + type: 'Polygon', + }, + relation: 'esriSpatialRelIntersects', + spatialReference: { + wkt: `GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]`, + }, }); }); test('geometry submitted as stringified JSON', () => { - const filter = { geometry: JSON.stringify({ - xmin: -123, - xmax: -122, - ymin: 48, - ymax: 49, - spatialReference: { wkid: 4326, latestWkid: 9999 } - })}; + const filter = { + geometry: JSON.stringify({ + xmin: -123, + xmax: -122, + ymin: 48, + ymax: 49, + spatialReference: { wkid: 4326, latestWkid: 9999 }, + }), + }; const result = standardizeGeometryFilter(filter); @@ -137,22 +425,27 @@ describe('standardizeGeometryFilter', () => { type: 'Polygon', }, relation: 'esriSpatialRelIntersects', - spatialReference: { wkid: 4326, latestWkid: 9999 }, + spatialReference: { + wkid: 4326, + wkt: 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]', + }, }); }); test('polyline', () => { - const result = standardizeGeometryFilter({ geometry: { - paths: [ - [ - [1, 4], - [-2, 4], - [2, 3], - [3, 10], - [0, 10], + const result = standardizeGeometryFilter({ + geometry: { + paths: [ + [ + [1, 4], + [-2, 4], + [2, 3], + [3, 10], + [0, 10], + ], ], - ], - }}); + }, + }); expect(result).toEqual({ geometry: { coordinates: [ @@ -170,19 +463,21 @@ describe('standardizeGeometryFilter', () => { }); test('multi-polyline', () => { - const result = standardizeGeometryFilter({ geometry: { - paths: [ - [ - [1, 4], - [-2, 4], - [2, 3], - ], - [ - [3, 10], - [0, 10], + const result = standardizeGeometryFilter({ + geometry: { + paths: [ + [ + [1, 4], + [-2, 4], + [2, 3], + ], + [ + [3, 10], + [0, 10], + ], ], - ], - }}); + }, + }); expect(result).toEqual({ geometry: { coordinates: [ @@ -235,13 +530,94 @@ describe('standardizeGeometryFilter', () => { }); }); - test('unsupported', () => { + test('envelope object with spatial reference that has no wkt will not reproject', () => { + jest.spyOn(projCodes, 'lookup').mockReturnValue({}); + + try { + standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 48, + ymax: 95, + }, + inSR: 99999, + reprojectionSR: 3857, + }); + throw new Error('should have thrown'); + } catch (error) { + expect(error.message).toBe( + 'Unknown geometry filter spatial reference WKT; unable to reproject', + ); + } + }); + + test('reprojection spatial reference that has no wkt will not reproject', () => { + jest + .spyOn(projCodes, 'lookup') + .mockReturnValueOnce({ + wkt: `GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]`, + }) + .mockReturnValueOnce({}); + + try { + standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 48, + ymax: 95, + spatialReference: { wkid: 4326 }, + }, + reprojectionSR: 3857, + }); + throw new Error('should have thrown'); + } catch (error) { + expect(error.message).toBe( + 'Unknown reprojection spatial reference WKT; unable to reproject', + ); + } + }); + + test('envelope object with spatial reference that has no extent will not clip', () => { + jest.spyOn(projCodes, 'lookup').mockReturnValue({}); + const result = standardizeGeometryFilter({ + geometry: { + xmin: -123, + xmax: -122, + ymin: 48, + ymax: 95, + }, + inSR: 99999 + }); + + expect(result).toEqual({ + geometry: { + coordinates: [ + [ + [-122, 95], + [-123, 95], + [-123, 48], + [-122, 48], + [-122, 95], + ], + ], + type: 'Polygon', + }, + relation: 'esriSpatialRelIntersects', + spatialReference: { wkt: undefined, wkid: undefined }, + }); + }); + + test('unsupported filter format', () => { try { - standardizeGeometryFilter({ geometry: { hello: 'world' } }); + standardizeGeometryFilter({ + geometry: { hello: 'world' } as unknown as GeometryFilter, + }); throw new Error('should have thrown'); } catch (error) { expect(error.message).toEqual( - 'Unsupported geometry filter format: {"hello":"world"}', + 'Unsupported geometry filter format: {"hello":"world"}; must be a spatial reference ID or object', ); } }); diff --git a/src/standardize-geometry-filter/index.ts b/src/standardize-geometry-filter/index.ts index 8d3b82d..fca5b13 100644 --- a/src/standardize-geometry-filter/index.ts +++ b/src/standardize-geometry-filter/index.ts @@ -1,133 +1,278 @@ -import * as _ from "lodash"; +import * as _ from 'lodash'; +import Joi from 'joi'; +import wktParser from 'wkt-parser'; +import * as esriProjCodes from '@esri/proj-codes'; import { - IEnvelope, - IPoint, - IPolyline, - IPolygon, ISpatialReference, SpatialRelationship, } from '@esri/arcgis-rest-types'; import { arcgisToGeoJSON } from '@terraformer/arcgis'; import bboxToPolygon from '@turf/bbox-polygon'; - -import { transformSpatialReferenceToWkt } from './transform-spatial-reference-to-wkt'; import { projectCoordinates } from './project-coordinates'; import { isSinglePointArray, isEnvelopeArray, filterSchema, - isArcgisObject, + spatialReferenceSchema, + wgsWkt, + wgsExtentEnvelope, } from './helpers'; +import { BBox } from 'geojson'; +import { clipToEnvelope } from './clip-to-envelope'; import { - BBox, - LineString, - MultiLineString, - MultiPoint, - MultiPolygon, - Point, - Polygon, -} from 'geojson'; - -type Geometry = - | Point - | MultiPoint - | LineString - | MultiLineString - | Polygon - | MultiPolygon; - -type GeometryFilter = IEnvelope | IPoint | IPolyline | IPolygon | number[]; - -type ArcgisSpatialReference = string | number | ISpatialReference; + ArcgisSpatialReference, + Coordinates, + Geometry, + GeometryFilter, +} from './common-types'; +import { someCoordinates } from './traverse-coordinates'; export interface IStandardizedGeometryFilter { geometry: Geometry; - spatialReference?: ArcgisSpatialReference; + spatialReference?: ISpatialReference; relation: string; } -export function standardizeGeometryFilter(params: { +const inputSpatialReferenceSchema = Joi.any() + .when(Joi.number(), { + then: Joi.number().strict().integer(), + otherwise: spatialReferenceSchema, + }) + .optional(); + +export function standardizeGeometryFilter( + params: IStandardizedGeometryFilterParams, +): IStandardizedGeometryFilter { + return StandardizedGeometryFilter.build(params); +} + +interface IStandardizedGeometryFilterParams { geometry: GeometryFilter | string; - inSR?: T; - reprojectionSR?: T; + inSR?: ArcgisSpatialReference; + reprojectionSR?: ArcgisSpatialReference; spatialRel?: SpatialRelationship; -}): IStandardizedGeometryFilter { - const { geometry, inSR, reprojectionSR, spatialRel } = params; - const filter = ( - _.isString(geometry) ? parseString(geometry as string) : geometry - ) as GeometryFilter; +} - validateFilter(filter); +class StandardizedGeometryFilter { + public geometry; + public spatialReference; + public relation; + private filter; + private filterSpatialReference; + private reprojectionSpatialReference; - const spatialReference = - extractGeometryFilterSpatialReference(filter) || inSR || reprojectionSR; - const filterCrsWkt = - spatialReference && transformSpatialReferenceToWkt(spatialReference); - const targetCrsWkt = reprojectionSR && transformSpatialReferenceToWkt(reprojectionSR); + static build( + params: IStandardizedGeometryFilterParams, + ): IStandardizedGeometryFilter { + const { geometry, relation, spatialReference } = + new StandardizedGeometryFilter(params); + return { geometry, relation, spatialReference }; + } - const geojsonGeometry = transformGeometryToGeojson(filter); - const projectedGeometry = shouldReproject(filterCrsWkt, targetCrsWkt) - ? reproject(geojsonGeometry, filterCrsWkt, targetCrsWkt) - : geojsonGeometry; + constructor(params: IStandardizedGeometryFilterParams) { + const { geometry, inSR, reprojectionSR, spatialRel } = params; - return { - geometry: projectedGeometry, - spatialReference: reprojectionSR || spatialReference, - relation: spatialRel || 'esriSpatialRelIntersects', - }; -} + this.filter = _.isString(geometry) ? parseString(geometry) : geometry; + this.relation = spatialRel || 'esriSpatialRelIntersects'; -function validateFilter(filter: GeometryFilter): void { - const { error } = filterSchema.validate(filter); + this.filterSpatialReference = this.extractSR( + 'inSR', + this.filter?.spatialReference || inSR, + ); + this.reprojectionSpatialReference = this.extractSR( + 'reprojectionSR', + reprojectionSR, + ); - if (error) { - throw new Error( - `Unsupported geometry filter format: ${JSON.stringify(filter)}`, + this.validateFilterShape(); + this.geometry = this.transformGeometry(); + + this.spatialReference = packageSpatialReference( + this.filterSpatialReference, ); + + if (this.shouldClipOutOfBoundsFilter()) { + this.geometry.coordinates = clipToEnvelope( + this.geometry.coordinates, + wgsExtentEnvelope, + ); + } + + if (reprojectionSR && this.validateReproject()) { + this.geometry.coordinates = projectCoordinates( + this.geometry.coordinates, + this.filterSpatialReference.wkt, + this.reprojectionSpatialReference.wkt, + ); + + this.spatialReference = packageSpatialReference( + this.reprojectionSpatialReference, + ); + } } -} -function parseString(param: string): number[] { - try { - return JSON.parse(param); - } catch (error) { - return param.split(',').map((item) => Number(item.trim())); + validateFilterShape(): StandardizedGeometryFilter { + const { error } = filterSchema.validate(this.filter); + if (error) { + throw new Error( + `Unsupported geometry filter format: ${JSON.stringify( + this.filter, + )}; must be a spatial reference ID or object`, + ); + } + + return this; } -} -function extractGeometryFilterSpatialReference( - filter: GeometryFilter, -): ISpatialReference | number | undefined { - if (isArcgisObject(filter)) { - return (filter as IEnvelope).spatialReference; + extractSR( + srSource: string, + spatialReference: ArcgisSpatialReference, + ): ISpatialReference { + if (!spatialReference) { + return; + } + + const { error } = inputSpatialReferenceSchema.validate(spatialReference); + + if (error) { + throw new Error( + `Unsupported ${srSource} format; must be a spatial reference ID or object`, + ); + } + + if ( + Number.isInteger(spatialReference) || + getSrid(spatialReference as ISpatialReference) + ) { + return getSpatialReferenceFromCode(spatialReference); + } + + if ((spatialReference as ISpatialReference).wkt) { + weakValidateWkt((spatialReference as ISpatialReference).wkt); + return spatialReference as ISpatialReference; + } } -} -function transformGeometryToGeojson(input: GeometryFilter): Geometry { - if (isSinglePointArray(input)) { - return { - type: 'Point', - coordinates: (input as number[]).map(Number), - }; + validateReproject(): boolean { + if (!this.filterSpatialReference) { + throw new Error( + 'Unknown geometry filter spatial reference; unable to reproject', + ); + } + + if (!this.filterSpatialReference.wkt) { + throw new Error( + `Unknown geometry filter spatial reference WKT; unable to reproject`, + ); + } + + if (!this.reprojectionSpatialReference) { + throw new Error( + `Unknown reprojection spatial reference; unable to reproject`, + ); + } + + if (!this.reprojectionSpatialReference.wkt) { + throw new Error( + `Unknown reprojection spatial reference WKT; unable to reproject`, + ); + } + + return true; } - if (isEnvelopeArray(input)) { - const { geometry } = bboxToPolygon(input as BBox); - return geometry; + shouldClipOutOfBoundsFilter(): boolean { + return this.filterSpatialReference?.wkt === wgsWkt && this.hasOOBCoords(); } - return arcgisToGeoJSON(input); + hasOOBCoords(): boolean { + const extent = wgsExtentEnvelope; + + const predicate = (coords: Coordinates): boolean => { + const [lon, lat] = coords; + + return ( + lon > extent.xmax || + lon < extent.xmin || + lat > extent.ymax || + lat < extent.ymin + ); + }; + + return someCoordinates(this.geometry.coordinates, predicate); + } + + transformGeometry(): Geometry { + if (isSinglePointArray(this.filter)) { + return { + type: 'Point', + coordinates: (this.filter as number[]).map(Number), + }; + } + + if (isEnvelopeArray(this.filter)) { + return bboxToPolygon(this.filter as BBox).geometry; + } + + return arcgisToGeoJSON(this.filter); + } } -function shouldReproject(fromSr: string, toSr: string): boolean { - return toSr && toSr !== fromSr; +function packageSpatialReference( + spatialReference?: any, +): ISpatialReference | void { + if (!spatialReference) { + return; + } + const { wkid, wkt } = spatialReference; + return { wkid, wkt }; } -function reproject(geometry: Geometry, fromSr: string, toSr: string): Geometry { - const coordinates = projectCoordinates(geometry.coordinates, fromSr, toSr); +function getSpatialReferenceFromCode(sr: ArcgisSpatialReference): any { + const srid = Number.isInteger(sr) ? sr : getSrid(sr as ISpatialReference); + const spatialReferenceDefinition = esriProjCodes.lookup(srid); + + if (!spatialReferenceDefinition) { + console.warn(`Unknown spatial reference: ${srid}; ignoring`); + return; + } + + const extentEnvelope = getSpatialReferenceExtent(spatialReferenceDefinition); + return { - ...geometry, - coordinates, - } as Geometry; + wkid: spatialReferenceDefinition.wkid, + wkt: spatialReferenceDefinition.wkt, + extent: extentEnvelope, + }; +} + +function getSrid(sr: ISpatialReference): number { + return sr.wkid || sr.latestWkid || sr.vcsWkid || sr.latestVcsWkid; } +function getSpatialReferenceExtent(spatialReferenceDefinition: any): any { + const { extent } = spatialReferenceDefinition; + + if (!extent) { + return; + } + + const { llon, slat, rlon, nlat } = extent; + return { xmin: llon, ymin: slat, xmax: rlon, ymax: nlat }; +} + +function weakValidateWkt(wkt: string): void { + try { + wktParser(wkt); + } catch (error) { + throw new Error(`Spatial reference WKT is unparseable: "${wkt}"`); + } +} + +function parseString(param: string): number[] { + try { + return JSON.parse(param); + } catch (error) { + return param.split(',').map((item) => Number(item.trim())); + } +} diff --git a/src/standardize-geometry-filter/project-coordinates.spec.ts b/src/standardize-geometry-filter/project-coordinates.spec.ts index e4bab9b..8bfe440 100644 --- a/src/standardize-geometry-filter/project-coordinates.spec.ts +++ b/src/standardize-geometry-filter/project-coordinates.spec.ts @@ -5,67 +5,113 @@ jest.mock('proj4', () => { return jest.fn(() => ['lon', 'lat']); }); -describe('project-coordinates', () => { - +describe.only('project-coordinates', () => { beforeEach(() => { jest.clearAllMocks(); }); test('Do not project coordinates if one is null', () => { - const transformed = projectCoordinates([null as any, 63]); + const transformed = projectCoordinates([null as any, 63], 'test', 'test'); expect(transformed[0]).toEqual(null); expect(transformed[1]).toEqual(63); expect((proj4 as any as jest.Mock).mock.calls.length).toEqual(0); }); - + test('Do not project coordinates if both are null', () => { - const transformed = projectCoordinates([null as any, null]); + const transformed = projectCoordinates([null as any, null], 'test', 'test'); expect(transformed[0]).toEqual(null); expect(transformed[1]).toEqual(null); expect((proj4 as any as jest.Mock).mock.calls.length).toEqual(0); }); - + test('Do not project coordinates if empty array', () => { - const transformed = projectCoordinates([]); + const transformed = projectCoordinates([], 'test', 'test'); expect(transformed[0]).toEqual(undefined); expect(transformed[1]).toEqual(undefined); expect((proj4 as any as jest.Mock).mock.calls.length).toEqual(0); }); - - test('Do not project coordinates if no toSR', () => { - const transformed = projectCoordinates([45, 75]); - expect(transformed[0]).toEqual(45); - expect(transformed[1]).toEqual(75); - expect((proj4 as any as jest.Mock).mock.calls.length).toEqual(0); - }); - - test('Do not project coordinates if target and source spatial reference are the same', () => { - const transformed = projectCoordinates([45, 75], '4326', '4326'); - expect(transformed[0]).toEqual(45); - expect(transformed[1]).toEqual(75); - expect((proj4 as any as jest.Mock).mock.calls.length).toEqual(0); - }); + test('should reproject simple array', () => { const transformed = projectCoordinates([75, 45], '4326', '2991'); expect(transformed[0]).toEqual('lon'); expect(transformed[1]).toEqual('lat'); expect((proj4 as any as jest.Mock).mock.calls.length).toEqual(1); - expect((proj4 as any as jest.Mock).mock.calls[0]).toEqual(['4326', '2991', [75, 45]]) + expect((proj4 as any as jest.Mock).mock.calls[0]).toEqual([ + '4326', + '2991', + [75, 45], + ]); + }); + + test('should constrain 90 degree latitude to avoid proj4 errors', () => { + const transformed = projectCoordinates([75, 90], 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]', '2991'); + expect(transformed[0]).toEqual('lon'); + expect(transformed[1]).toEqual('lat'); + expect((proj4 as any as jest.Mock).mock.calls.length).toEqual(1); + expect((proj4 as any as jest.Mock).mock.calls[0]).toEqual([ + 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]', + '2991', + [75, 89.99999999], + ]); + }); + + test('should constrain -90 degree latitude to avoid proj4 errors', () => { + const transformed = projectCoordinates([75, -90], 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]', '2991'); + expect(transformed[0]).toEqual('lon'); + expect(transformed[1]).toEqual('lat'); + expect((proj4 as any as jest.Mock).mock.calls.length).toEqual(1); + expect((proj4 as any as jest.Mock).mock.calls[0]).toEqual([ + 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]', + '2991', + [75, -89.99999999], + ]); }); test('should reproject nested array', () => { - const transformed = projectCoordinates([[75, 45], [76, 48]], '4326', '2991'); - expect(transformed).toEqual([["lon", "lat"], ["lon", "lat"]]); + const transformed = projectCoordinates( + [ + [75, 45], + [76, 48], + ], + '4326', + '2991', + ); + expect(transformed).toEqual([ + ['lon', 'lat'], + ['lon', 'lat'], + ]); expect((proj4 as any as jest.Mock).mock.calls.length).toEqual(2); - expect((proj4 as any as jest.Mock).mock.calls[0]).toEqual(['4326', '2991', [75, 45]]); - expect((proj4 as any as jest.Mock).mock.calls[1]).toEqual(['4326', '2991', [76, 48]]); + expect((proj4 as any as jest.Mock).mock.calls[0]).toEqual([ + '4326', + '2991', + [75, 45], + ]); + expect((proj4 as any as jest.Mock).mock.calls[1]).toEqual([ + '4326', + '2991', + [76, 48], + ]); }); test('should reproject nested array', () => { - const transformed = projectCoordinates([[75, 45], [null as any, null]], '4326', '2991'); - expect(transformed).toEqual([["lon", "lat"], [null, null]]); - expect((proj4 as any as jest.Mock).mock.calls.length).toEqual(1); - expect((proj4 as any as jest.Mock).mock.calls[0]).toEqual(['4326', '2991', [75, 45]]); + const transformed = projectCoordinates( + [ + [75, 45], + [20, 30], + ], + '4326', + '2991', + ); + expect(transformed).toEqual([ + ['lon', 'lat'], + ['lon', 'lat'], + ]); + expect((proj4 as any as jest.Mock).mock.calls.length).toEqual(2); + expect((proj4 as any as jest.Mock).mock.calls[0]).toEqual([ + '4326', + '2991', + [75, 45], + ]); }); }); diff --git a/src/standardize-geometry-filter/project-coordinates.ts b/src/standardize-geometry-filter/project-coordinates.ts index 3bf1dfe..fe448b0 100644 --- a/src/standardize-geometry-filter/project-coordinates.ts +++ b/src/standardize-geometry-filter/project-coordinates.ts @@ -1,31 +1,35 @@ import proj4 from 'proj4'; import * as _ from 'lodash'; -import { Position } from 'geojson'; +import { transformCoordinates } from './traverse-coordinates'; +import { Coordinates } from './common-types'; +import { wgsExtentEnvelope, wgsWkt } from './helpers'; +const WGS_MIN_LAT = wgsExtentEnvelope.ymin; +const WGS_MAX_LAT = wgsExtentEnvelope.ymax; -type Coordinates = Position | Position[] | Position[][] | Position[][][]; - -export function projectCoordinates(coordinates: Coordinates , fromSR?: string, toSR?: string): Coordinates { - if (!toSR || fromSR === toSR) { - return coordinates; +export function projectCoordinates(coordinates: Coordinates , fromSR: string, toSR: string): Coordinates { + const reproject = (coords: Coordinates): Coordinates => { + if (shouldReproject(coords)) { + const [, lat] = coords; + if(shouldConstrainSourceX(fromSR, lat as number)) { + coords[1] = constrainX(lat as number); + } + return proj4(fromSR, toSR, coords); + } + return coords; } - - return recursiveCoordinatesReproject(coordinates, fromSR, toSR); + + return transformCoordinates(coordinates, reproject) } -function recursiveCoordinatesReproject(coordinates: Coordinates, fromSr: string, toSr: string): Coordinates { - if (Array.isArray(coordinates[0])) { - return coordinates.map((coords) => { - return recursiveCoordinatesReproject(coords, fromSr, toSr) - }) as Coordinates; - } - - if (shouldReproject(coordinates)) { - return proj4(fromSr, toSr, coordinates); - } - - return coordinates; +// Prevent error in event of null or undefined coordinates +function shouldReproject(coordinates: Coordinates): boolean { + return coordinates && _.isNumber(coordinates[0]) && _.isNumber(coordinates[1]); } -function shouldReproject(coordinates: Coordinates): boolean { - return _.isNumber(coordinates[0]) && _.isNumber(coordinates[1]); +function shouldConstrainSourceX(fromSR: string, x: number): boolean { + return fromSR === wgsWkt && (x === WGS_MIN_LAT || x === WGS_MAX_LAT); } + +function constrainX(x: number): number { + return x === WGS_MAX_LAT ? WGS_MAX_LAT - 1e-8 : WGS_MIN_LAT + 1e-8; +} \ No newline at end of file diff --git a/src/standardize-geometry-filter/transform-spatial-reference-to-wkt.spec.ts b/src/standardize-geometry-filter/transform-spatial-reference-to-wkt.spec.ts deleted file mode 100644 index f77fc0f..0000000 --- a/src/standardize-geometry-filter/transform-spatial-reference-to-wkt.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { transformSpatialReferenceToWkt } from './transform-spatial-reference-to-wkt'; -import * as projCodes from '@esri/proj-codes'; - -jest.mock('@esri/proj-codes', () => { - return { - lookup: jest.fn(() => { - return { wkt: 'the-wkt'} - }) - }; -}); - -jest.mock('@alloc/quick-lru', () => { - return function () { - this.get = () => {}; - this.set = () => {}; - }; -}); - -describe('transformSpatialReferenceToWkt', () => { - beforeEach(() => { - jest.clearAllMocks(); - }) - - test('invalid input', () => { - try { - /* tslint:disable-next-line */ - transformSpatialReferenceToWkt(undefined as any); - throw new Error('should have thrown') - } catch (error) { - expect(error.message).toEqual('Unsupported spatial reference format: "undefined"'); - } - }); - - test('wkid input', () => { - const result = transformSpatialReferenceToWkt(4326) - expect(result).toEqual('the-wkt'); - expect(projCodes.lookup.mock.calls[0]).toEqual([4326]); - }); - - test('string-wkid input', () => { - const result = transformSpatialReferenceToWkt('4326') - expect(result).toEqual('the-wkt'); - expect(projCodes.lookup.mock.calls[0]).toEqual([4326]); - }); - - test('spatial reference wkid input', () => { - const result = transformSpatialReferenceToWkt({ wkid: 4326}) - expect(result).toEqual('the-wkt'); - expect(projCodes.lookup.mock.calls[0]).toEqual([4326]); - }); - - test('wkt input', () => { - const result = transformSpatialReferenceToWkt('GEOGCS["WGS 84"]') - expect(result).toEqual('GEOGCS["WGS 84"]'); - }); - - test('spatial reference latest-wkid input', () => { - const result = transformSpatialReferenceToWkt({ latestWkid: 4326}) - expect(result).toEqual('the-wkt'); - expect(projCodes.lookup.mock.calls[0]).toEqual([4326]); - }); - - test('spatial reference wkt input', () => { - const result = transformSpatialReferenceToWkt({ wkt: 'GEOGCS["WGS 84"]'}) - expect(result).toEqual('GEOGCS["WGS 84"]'); - }); - - test('invalid spatial reference wkid input', () => { - jest.resetAllMocks(); - jest.mock('@esri/proj-codes', () => { - return { - lookup: jest.fn(() => { - return; - }) - }; - }); - try { - transformSpatialReferenceToWkt({ wkid: 99999}); - throw new Error('should have thrown') - } catch (error) { - expect(error.message).toEqual('"99999" is an unknown spatial reference'); - } - }); - - test('unparseable WKT', () => { - try { - transformSpatialReferenceToWkt({ wkt: 'test' }); - throw new Error('should have thrown') - } catch (error) { - expect(error.message).toEqual('Spatial reference WKT is unparseable: "test"'); - } - }); - -}); \ No newline at end of file diff --git a/src/standardize-geometry-filter/transform-spatial-reference-to-wkt.ts b/src/standardize-geometry-filter/transform-spatial-reference-to-wkt.ts deleted file mode 100644 index 74073ec..0000000 --- a/src/standardize-geometry-filter/transform-spatial-reference-to-wkt.ts +++ /dev/null @@ -1,105 +0,0 @@ -import * as esriProjCodes from '@esri/proj-codes'; -import * as _ from 'lodash'; -import Joi from 'joi'; -import wktParser from 'wkt-parser'; -import QuickLRU from '@alloc/quick-lru'; -const wkidCache = new QuickLRU({ maxSize: 10 }); - -const schema = Joi.alternatives( - Joi.string(), - Joi.number().integer(), - Joi.object({ - wkid: Joi.number().strict().integer().optional(), - latestWkid: Joi.number().strict().integer().optional(), - wkt: Joi.string().optional(), - }) - .unknown() - .or('wkid', 'latestWkid', 'wkt') - .required(), -).required(); - -type SpatialReference = { - wkid?: number; - latestWkid?: number; - wkt?: string; -}; - -type Input = string | number | SpatialReference; - -export function transformSpatialReferenceToWkt(input: Input): string { - const { error, value: castInput } = schema.validate(input); - - if (error) { - throw new Error( - `Unsupported spatial reference format: "${JSON.stringify(input)}"`, - ); - } - - const { type, value } = parseSpatialReferenceInput(castInput); - - if (type === 'wkid') { - return wkidCache.get(value) as string || esriWktLookup(value as number); - } - - return weakValidateWkt(value as string); -} - -type ParsedSpatialReference = { - value: string | number; - type: string; -} - -function parseSpatialReferenceInput(spatialReference: Input): ParsedSpatialReference { - if (isNumericSpatialReferenceId(spatialReference)) { - return { - type: 'wkid', - value: Number(spatialReference), - }; - } - - const { wkt, wkid, latestWkid } = spatialReference as SpatialReference; - - if (_.isString(spatialReference) || wkt) { - return { - type: 'wkt', - value: wkt || spatialReference as string, - }; - } - - return { - type: 'wkid', - value: wkid || latestWkid, - }; -} - -function isNumericSpatialReferenceId(spatialReference: Input): boolean { - return ( - Number.isInteger(spatialReference) || - Number.isInteger(Number(spatialReference)) - ); -} - -function esriWktLookup(wkid: number): string { - const result = esriProjCodes.lookup(wkid); - - if (!result) { - throw new Error(`"${wkid}" is an unknown spatial reference`); - } - - const { wkt } = result; - - // Add the WKT to the local lookup so we don't need to scan the Esri lookups next time - // TODO: move to LRU cache - wkidCache.set(wkid, wkt); - return wkt; -} - -function weakValidateWkt(wkt: string): any { - try { - wktParser(wkt); - } catch (error) { - throw new Error(`Spatial reference WKT is unparseable: "${wkt}"`); - } - - return wkt; -} diff --git a/src/standardize-geometry-filter/traverse-coordinates.spec.ts b/src/standardize-geometry-filter/traverse-coordinates.spec.ts new file mode 100644 index 0000000..7b6d521 --- /dev/null +++ b/src/standardize-geometry-filter/traverse-coordinates.spec.ts @@ -0,0 +1,34 @@ +import { Coordinates } from './common-types'; +import { someCoordinates, transformCoordinates } from './traverse-coordinates'; + +describe('transformCoordinates', () => { + test('traverse and transform array', () => { + const transform = (coordinates: Coordinates): Coordinates => { + const [x, y] = coordinates; + return [x as number + 1, y as number + 1]; + }; + const result = transformCoordinates([[45, 60]], transform); + expect(result).toEqual([[46, 61]]); + }); + + test('traverse and transform nested array', () => { + const transform = (coordinates: Coordinates): Coordinates => { + const [x, y] = coordinates; + return [x as number + 1, y as number + 1]; + }; + const result = transformCoordinates([[45, 60], [35, 40]], transform); + expect(result).toEqual([[46, 61], [36, 41]]); + }); +}); + + +describe('someCoordinates', () => { + test('traverse and until predicate is fufilled', () => { + const predicate = (coordinates: Coordinates): boolean => { + const [x, y] = coordinates; + return y as number > 90; + }; + const result = someCoordinates([[45, 60], [35, 95]], predicate); + expect(result).toEqual(true); + }); +}); \ No newline at end of file diff --git a/src/standardize-geometry-filter/traverse-coordinates.ts b/src/standardize-geometry-filter/traverse-coordinates.ts new file mode 100644 index 0000000..3084f86 --- /dev/null +++ b/src/standardize-geometry-filter/traverse-coordinates.ts @@ -0,0 +1,23 @@ +import { Coordinates } from "./common-types"; + +type TransformFunction = (coordinates: Coordinates) => Coordinates; + +export function transformCoordinates(coordinates: Coordinates, transform: TransformFunction): Coordinates { + if (Array.isArray(coordinates[0])) { + return coordinates.map((coords) => { + return transformCoordinates(coords, transform) + }) as Coordinates; + } + return transform(coordinates); +} + +type Predicate = (coordinates: Coordinates) => boolean; + +export function someCoordinates(coordinates: Coordinates, condition: Predicate): boolean { + if (Array.isArray(coordinates[0])) { + return coordinates.some((coords) => { + return someCoordinates(coords, condition) + }); + } + return condition(coordinates); +} diff --git a/tsconfig.json b/tsconfig.json index d9f59ba..29647c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "module": "commonjs", "esModuleInterop": true, - "target": "es6", + "target": "es2022", "moduleResolution": "node", "sourceMap": true, "outDir": "dist", @@ -13,6 +13,5 @@ "exclude": [ "node_modules", "**/*.spec.ts", - "transform-spatial-reference-to-wkt.spec.ts" ] } \ No newline at end of file