diff --git a/packages/turf-helpers/README.md b/packages/turf-helpers/README.md index 2e714cd74..fbef66c30 100644 --- a/packages/turf-helpers/README.md +++ b/packages/turf-helpers/README.md @@ -569,6 +569,18 @@ turf.isObject('foo') Returns **[boolean][23]** true/false, including false for Arrays and Functions +## removeBbox + +Recursively removes bounding boxes from a GeoJSON object. + +This function mutates the input GeoJSON object. + +### Parameters + +* `geojson` **[GeoJSON][24]** GeoJSON object whose bounding boxes should be removed + +Returns **void** + [1]: https://www.thoughtco.com/degree-of-latitude-and-longitude-distance-4070616 [2]: #units @@ -615,6 +627,8 @@ Returns **[boolean][23]** true/false, including false for Arrays and Functions [23]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean +[24]: https://tools.ietf.org/html/rfc7946#section-3 + --- diff --git a/packages/turf-helpers/index.ts b/packages/turf-helpers/index.ts index 3bf434cbb..9bf3aa15b 100644 --- a/packages/turf-helpers/index.ts +++ b/packages/turf-helpers/index.ts @@ -13,6 +13,7 @@ import { Polygon, Position, GeoJsonProperties, + GeoJSON, } from "geojson"; import { Id } from "./lib/geojson.js"; @@ -877,6 +878,27 @@ export function isObject(input: any): boolean { return input !== null && typeof input === "object" && !Array.isArray(input); } +/** + * Recursively removes bounding boxes from a GeoJSON object. + * + * This function mutates the input GeoJSON object. + * + * @function + * @param {GeoJSON} geojson GeoJSON object whose bounding boxes should be removed + * @returns {void} + */ +export function removeBbox(geojson: GeoJSON): void { + delete geojson.bbox; + + if (geojson.type === "Feature") { + if (geojson.geometry) removeBbox(geojson.geometry); + } else if (geojson.type === "FeatureCollection") { + geojson.features.forEach(removeBbox); + } else if (geojson.type === "GeometryCollection") { + geojson.geometries.forEach(removeBbox); + } +} + /** * Validate BBox * diff --git a/packages/turf-helpers/test.ts b/packages/turf-helpers/test.ts index ff2326a6a..99046d097 100644 --- a/packages/turf-helpers/test.ts +++ b/packages/turf-helpers/test.ts @@ -22,6 +22,7 @@ import { isObject, isNumber, earthRadius, + removeBbox, } from "./index.js"; import * as turf from "./index.js"; @@ -47,6 +48,38 @@ test("point", (t) => { t.end(); }); +test("removeBbox", (t) => { + const line = lineString( + [ + [0, 0], + [1, 1], + ], + { bbox: "property value is preserved" } + ); + line.bbox = [0, 0, 1, 1]; + line.geometry.bbox = [0, 0, 1, 1]; + + const collection = geometryCollection([line.geometry]); + collection.bbox = [0, 0, 1, 1]; + collection.geometry.bbox = [0, 0, 1, 1]; + + const input = featureCollection([collection]); + input.bbox = [0, 0, 1, 1]; + + removeBbox(input); + + t.notOk(input.bbox, "removes FeatureCollection bbox"); + t.notOk(collection.bbox, "removes Feature bbox"); + t.notOk(collection.geometry.bbox, "removes GeometryCollection bbox"); + t.notOk(line.geometry.bbox, "removes nested geometry bbox"); + t.equal( + line.properties.bbox, + "property value is preserved", + "does not alter feature properties" + ); + t.end(); +}); + test("polygon", (t) => { const poly = polygon( [ diff --git a/packages/turf-helpers/types.ts b/packages/turf-helpers/types.ts index f41410804..4a18a4b49 100644 --- a/packages/turf-helpers/types.ts +++ b/packages/turf-helpers/types.ts @@ -16,6 +16,7 @@ import { point, polygon, radiansToLength, + removeBbox, } from "./index.js"; // Fixtures @@ -58,6 +59,8 @@ const multiPoly = multiPolygon([ ], ]); +removeBbox(multiPoly); + // radiansToLength & lengthToRadians radiansToLength(5); lengthToRadians(10); diff --git a/packages/turf-transform-rotate/index.ts b/packages/turf-transform-rotate/index.ts index 9445b666f..93af2b3dd 100644 --- a/packages/turf-transform-rotate/index.ts +++ b/packages/turf-transform-rotate/index.ts @@ -6,7 +6,7 @@ import { rhumbDestination } from "@turf/rhumb-destination"; import { clone } from "@turf/clone"; import { coordEach } from "@turf/meta"; import { getCoords } from "@turf/invariant"; -import { isObject, Coord } from "@turf/helpers"; +import { isObject, removeBbox, Coord } from "@turf/helpers"; /** * Rotates any geojson Feature or Geometry of a specified angle, around its `centroid` or a given `pivot` point. @@ -66,6 +66,7 @@ function transformRotate( pointCoords[0] = newCoords[0]; pointCoords[1] = newCoords[1]; }); + removeBbox(geojson); return geojson; } diff --git a/packages/turf-transform-rotate/test.ts b/packages/turf-transform-rotate/test.ts index f60a98187..b8b31fdea 100644 --- a/packages/turf-transform-rotate/test.ts +++ b/packages/turf-transform-rotate/test.ts @@ -147,3 +147,53 @@ function makePivot(pivot: Coord, geojson: GeoJSON | GeometryCollection) { } return point(getCoord(pivot), { "marker-symbol": "circle" }); } + +test("rotate -- removes stale bbox values", (t) => { + const input = nestedGeojsonWithBboxes(); + const rotated = rotate(input, 45); + + t.equal(countBboxes(rotated), 0, "removes bboxes from cloned output"); + t.ok(countBboxes(input) > 0, "does not alter input by default"); + + rotate(input, 45, { mutate: true }); + t.equal(countBboxes(input), 0, "removes bboxes from mutated input"); + t.end(); +}); + +function nestedGeojsonWithBboxes() { + const geojson = featureCollection([ + geometryCollection([ + lineString([ + [0, 0], + [1, 1], + ]).geometry, + ]), + ]); + addBboxes(geojson); + return geojson; +} + +function addBboxes(geojson: GeoJSON | GeometryCollection): void { + geojson.bbox = [0, 0, 1, 1]; + if (geojson.type === "Feature" && geojson.geometry) { + addBboxes(geojson.geometry); + } else if (geojson.type === "FeatureCollection") { + geojson.features.forEach(addBboxes); + } else if (geojson.type === "GeometryCollection") { + geojson.geometries.forEach(addBboxes); + } +} + +function countBboxes(geojson: GeoJSON | GeometryCollection): number { + let count = geojson.bbox ? 1 : 0; + if (geojson.type === "Feature" && geojson.geometry) { + count += countBboxes(geojson.geometry); + } else if (geojson.type === "FeatureCollection") { + geojson.features.forEach((feature) => (count += countBboxes(feature))); + } else if (geojson.type === "GeometryCollection") { + geojson.geometries.forEach((geometry) => { + count += countBboxes(geometry); + }); + } + return count; +} diff --git a/packages/turf-transform-scale/index.ts b/packages/turf-transform-scale/index.ts index 583352060..595ebbdac 100644 --- a/packages/turf-transform-scale/index.ts +++ b/packages/turf-transform-scale/index.ts @@ -1,4 +1,4 @@ -import { Corners, Coord } from "@turf/helpers"; +import { Corners, Coord, removeBbox } from "@turf/helpers"; import { FeatureCollection, GeoJSON, GeometryCollection } from "geojson"; import { clone } from "@turf/clone"; import { center } from "@turf/center"; @@ -67,6 +67,7 @@ function transformScale( origin ); }); + if (factor !== 1) delete geojson.bbox; return geojson; } // Scale Feature/Geometry @@ -108,7 +109,7 @@ function scale( if (coord.length === 3) coord[2] *= factor; }); - delete feature.bbox; + removeBbox(feature); return feature; } diff --git a/packages/turf-transform-scale/test.ts b/packages/turf-transform-scale/test.ts index 00ed1d67a..b8b6615d7 100644 --- a/packages/turf-transform-scale/test.ts +++ b/packages/turf-transform-scale/test.ts @@ -1,4 +1,10 @@ -import { BBox, Feature, FeatureCollection } from "geojson"; +import { + BBox, + Feature, + FeatureCollection, + GeoJSON, + GeometryCollection, +} from "geojson"; import fs from "fs"; import test from "tape"; import path from "path"; @@ -277,6 +283,56 @@ function colorize(geojson: FeatureCollection | Feature) { return geojson; } +test("scale -- removes stale bbox values", (t) => { + const input = nestedGeojsonWithBboxes(); + const scaled = scale(input, 2, { origin: [0, 0] }); + + t.equal(countBboxes(scaled), 0, "removes bboxes from cloned output"); + t.ok(countBboxes(input) > 0, "does not alter input by default"); + + scale(input, 2, { origin: [0, 0], mutate: true }); + t.equal(countBboxes(input), 0, "removes bboxes from mutated input"); + t.end(); +}); + +function nestedGeojsonWithBboxes() { + const geojson = featureCollection([ + geometryCollection([ + lineString([ + [0, 0], + [1, 1], + ]).geometry, + ]), + ]); + addBboxes(geojson); + return geojson; +} + +function addBboxes(geojson: GeoJSON | GeometryCollection): void { + geojson.bbox = [0, 0, 1, 1]; + if (geojson.type === "Feature" && geojson.geometry) { + addBboxes(geojson.geometry); + } else if (geojson.type === "FeatureCollection") { + geojson.features.forEach(addBboxes); + } else if (geojson.type === "GeometryCollection") { + geojson.geometries.forEach(addBboxes); + } +} + +function countBboxes(geojson: GeoJSON | GeometryCollection): number { + let count = geojson.bbox ? 1 : 0; + if (geojson.type === "Feature" && geojson.geometry) { + count += countBboxes(geojson.geometry); + } else if (geojson.type === "FeatureCollection") { + geojson.features.forEach((feature) => (count += countBboxes(feature))); + } else if (geojson.type === "GeometryCollection") { + geojson.geometries.forEach((geometry) => { + count += countBboxes(geometry); + }); + } + return count; +} + // define origin, as defined in transform-scale, and style it function markedOrigin( geojson: Feature, diff --git a/packages/turf-transform-translate/index.ts b/packages/turf-transform-translate/index.ts index 04d446f5a..b862d951d 100644 --- a/packages/turf-transform-translate/index.ts +++ b/packages/turf-transform-translate/index.ts @@ -1,6 +1,6 @@ import { GeoJSON, GeometryCollection } from "geojson"; import { coordEach } from "@turf/meta"; -import { isObject, Units } from "@turf/helpers"; +import { isObject, removeBbox, Units } from "@turf/helpers"; import { getCoords } from "@turf/invariant"; import { clone } from "@turf/clone"; import { rhumbDestination } from "@turf/rhumb-destination"; @@ -79,6 +79,7 @@ function transformTranslate( if (zTranslation && pointCoords.length === 3) pointCoords[2] += zTranslation; }); + removeBbox(geojson); return geojson; } diff --git a/packages/turf-transform-translate/test.ts b/packages/turf-transform-translate/test.ts index ebc9a491a..5e3b4c57c 100644 --- a/packages/turf-transform-translate/test.ts +++ b/packages/turf-transform-translate/test.ts @@ -1,4 +1,4 @@ -import { Feature } from "geojson"; +import { Feature, GeoJSON, GeometryCollection } from "geojson"; import fs from "fs"; import test from "tape"; import path from "path"; @@ -152,3 +152,53 @@ function colorize(geojson: Feature) { } return geojson; } + +test("translate -- removes stale bbox values", (t) => { + const input = nestedGeojsonWithBboxes(); + const translated = translate(input, 10, 45); + + t.equal(countBboxes(translated), 0, "removes bboxes from cloned output"); + t.ok(countBboxes(input) > 0, "does not alter input by default"); + + translate(input, 10, 45, { mutate: true }); + t.equal(countBboxes(input), 0, "removes bboxes from mutated input"); + t.end(); +}); + +function nestedGeojsonWithBboxes() { + const geojson = featureCollection([ + geometryCollection([ + lineString([ + [0, 0], + [1, 1], + ]).geometry, + ]), + ]); + addBboxes(geojson); + return geojson; +} + +function addBboxes(geojson: GeoJSON | GeometryCollection): void { + geojson.bbox = [0, 0, 1, 1]; + if (geojson.type === "Feature" && geojson.geometry) { + addBboxes(geojson.geometry); + } else if (geojson.type === "FeatureCollection") { + geojson.features.forEach(addBboxes); + } else if (geojson.type === "GeometryCollection") { + geojson.geometries.forEach(addBboxes); + } +} + +function countBboxes(geojson: GeoJSON | GeometryCollection): number { + let count = geojson.bbox ? 1 : 0; + if (geojson.type === "Feature" && geojson.geometry) { + count += countBboxes(geojson.geometry); + } else if (geojson.type === "FeatureCollection") { + geojson.features.forEach((feature) => (count += countBboxes(feature))); + } else if (geojson.type === "GeometryCollection") { + geojson.geometries.forEach((geometry) => { + count += countBboxes(geometry); + }); + } + return count; +}