Skip to content

Commit 5fd684d

Browse files
transform-scale, transform-translate, transform-rotate: Invalidate stale bboxes after coordinate transforms (#3106)
* Invalidate stale bboxes after transforms * Share recursive bbox removal helper --------- Co-authored-by: mfedderly <24275386+mfedderly@users.noreply.github.com>
1 parent decb5cd commit 5fd684d

10 files changed

Lines changed: 237 additions & 6 deletions

File tree

packages/turf-helpers/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,18 @@ turf.isObject('foo')
569569

570570
Returns **[boolean][23]** true/false, including false for Arrays and Functions
571571

572+
## removeBbox
573+
574+
Recursively removes bounding boxes from a GeoJSON object.
575+
576+
This function mutates the input GeoJSON object.
577+
578+
### Parameters
579+
580+
* `geojson` **[GeoJSON][24]** GeoJSON object whose bounding boxes should be removed
581+
582+
Returns **void**&#x20;
583+
572584
[1]: https://www.thoughtco.com/degree-of-latitude-and-longitude-distance-4070616
573585

574586
[2]: #units
@@ -615,6 +627,8 @@ Returns **[boolean][23]** true/false, including false for Arrays and Functions
615627

616628
[23]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
617629

630+
[24]: https://tools.ietf.org/html/rfc7946#section-3
631+
618632
<!-- This file is automatically generated. Please don't edit it directly. If you find an error, edit the source file of the module in question (likely index.js or index.ts), and re-run "yarn docs" from the root of the turf project. -->
619633

620634
---

packages/turf-helpers/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Polygon,
1414
Position,
1515
GeoJsonProperties,
16+
GeoJSON,
1617
} from "geojson";
1718

1819
import { Id } from "./lib/geojson.js";
@@ -877,6 +878,27 @@ export function isObject(input: any): boolean {
877878
return input !== null && typeof input === "object" && !Array.isArray(input);
878879
}
879880

881+
/**
882+
* Recursively removes bounding boxes from a GeoJSON object.
883+
*
884+
* This function mutates the input GeoJSON object.
885+
*
886+
* @function
887+
* @param {GeoJSON} geojson GeoJSON object whose bounding boxes should be removed
888+
* @returns {void}
889+
*/
890+
export function removeBbox(geojson: GeoJSON): void {
891+
delete geojson.bbox;
892+
893+
if (geojson.type === "Feature") {
894+
if (geojson.geometry) removeBbox(geojson.geometry);
895+
} else if (geojson.type === "FeatureCollection") {
896+
geojson.features.forEach(removeBbox);
897+
} else if (geojson.type === "GeometryCollection") {
898+
geojson.geometries.forEach(removeBbox);
899+
}
900+
}
901+
880902
/**
881903
* Validate BBox
882904
*

packages/turf-helpers/test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
isObject,
2323
isNumber,
2424
earthRadius,
25+
removeBbox,
2526
} from "./index.js";
2627
import * as turf from "./index.js";
2728

@@ -47,6 +48,38 @@ test("point", (t) => {
4748
t.end();
4849
});
4950

51+
test("removeBbox", (t) => {
52+
const line = lineString(
53+
[
54+
[0, 0],
55+
[1, 1],
56+
],
57+
{ bbox: "property value is preserved" }
58+
);
59+
line.bbox = [0, 0, 1, 1];
60+
line.geometry.bbox = [0, 0, 1, 1];
61+
62+
const collection = geometryCollection([line.geometry]);
63+
collection.bbox = [0, 0, 1, 1];
64+
collection.geometry.bbox = [0, 0, 1, 1];
65+
66+
const input = featureCollection([collection]);
67+
input.bbox = [0, 0, 1, 1];
68+
69+
removeBbox(input);
70+
71+
t.notOk(input.bbox, "removes FeatureCollection bbox");
72+
t.notOk(collection.bbox, "removes Feature bbox");
73+
t.notOk(collection.geometry.bbox, "removes GeometryCollection bbox");
74+
t.notOk(line.geometry.bbox, "removes nested geometry bbox");
75+
t.equal(
76+
line.properties.bbox,
77+
"property value is preserved",
78+
"does not alter feature properties"
79+
);
80+
t.end();
81+
});
82+
5083
test("polygon", (t) => {
5184
const poly = polygon(
5285
[

packages/turf-helpers/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
point,
1717
polygon,
1818
radiansToLength,
19+
removeBbox,
1920
} from "./index.js";
2021

2122
// Fixtures
@@ -58,6 +59,8 @@ const multiPoly = multiPolygon([
5859
],
5960
]);
6061

62+
removeBbox(multiPoly);
63+
6164
// radiansToLength & lengthToRadians
6265
radiansToLength(5);
6366
lengthToRadians(10);

packages/turf-transform-rotate/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { rhumbDestination } from "@turf/rhumb-destination";
66
import { clone } from "@turf/clone";
77
import { coordEach } from "@turf/meta";
88
import { getCoords } from "@turf/invariant";
9-
import { isObject, Coord } from "@turf/helpers";
9+
import { isObject, removeBbox, Coord } from "@turf/helpers";
1010

1111
/**
1212
* Rotates any geojson Feature or Geometry of a specified angle, around its `centroid` or a given `pivot` point.
@@ -66,6 +66,7 @@ function transformRotate<T extends GeoJSON | GeometryCollection>(
6666
pointCoords[0] = newCoords[0];
6767
pointCoords[1] = newCoords[1];
6868
});
69+
removeBbox(geojson);
6970
return geojson;
7071
}
7172

packages/turf-transform-rotate/test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,53 @@ function makePivot(pivot: Coord, geojson: GeoJSON | GeometryCollection) {
147147
}
148148
return point(getCoord(pivot), { "marker-symbol": "circle" });
149149
}
150+
151+
test("rotate -- removes stale bbox values", (t) => {
152+
const input = nestedGeojsonWithBboxes();
153+
const rotated = rotate(input, 45);
154+
155+
t.equal(countBboxes(rotated), 0, "removes bboxes from cloned output");
156+
t.ok(countBboxes(input) > 0, "does not alter input by default");
157+
158+
rotate(input, 45, { mutate: true });
159+
t.equal(countBboxes(input), 0, "removes bboxes from mutated input");
160+
t.end();
161+
});
162+
163+
function nestedGeojsonWithBboxes() {
164+
const geojson = featureCollection([
165+
geometryCollection([
166+
lineString([
167+
[0, 0],
168+
[1, 1],
169+
]).geometry,
170+
]),
171+
]);
172+
addBboxes(geojson);
173+
return geojson;
174+
}
175+
176+
function addBboxes(geojson: GeoJSON | GeometryCollection): void {
177+
geojson.bbox = [0, 0, 1, 1];
178+
if (geojson.type === "Feature" && geojson.geometry) {
179+
addBboxes(geojson.geometry);
180+
} else if (geojson.type === "FeatureCollection") {
181+
geojson.features.forEach(addBboxes);
182+
} else if (geojson.type === "GeometryCollection") {
183+
geojson.geometries.forEach(addBboxes);
184+
}
185+
}
186+
187+
function countBboxes(geojson: GeoJSON | GeometryCollection): number {
188+
let count = geojson.bbox ? 1 : 0;
189+
if (geojson.type === "Feature" && geojson.geometry) {
190+
count += countBboxes(geojson.geometry);
191+
} else if (geojson.type === "FeatureCollection") {
192+
geojson.features.forEach((feature) => (count += countBboxes(feature)));
193+
} else if (geojson.type === "GeometryCollection") {
194+
geojson.geometries.forEach((geometry) => {
195+
count += countBboxes(geometry);
196+
});
197+
}
198+
return count;
199+
}

packages/turf-transform-scale/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Corners, Coord } from "@turf/helpers";
1+
import { Corners, Coord, removeBbox } from "@turf/helpers";
22
import { FeatureCollection, GeoJSON, GeometryCollection } from "geojson";
33
import { clone } from "@turf/clone";
44
import { center } from "@turf/center";
@@ -67,6 +67,7 @@ function transformScale<T extends GeoJSON | GeometryCollection>(
6767
origin
6868
);
6969
});
70+
if (factor !== 1) delete geojson.bbox;
7071
return geojson;
7172
}
7273
// Scale Feature/Geometry
@@ -108,7 +109,7 @@ function scale<T extends GeoJSON | GeometryCollection>(
108109
if (coord.length === 3) coord[2] *= factor;
109110
});
110111

111-
delete feature.bbox;
112+
removeBbox(feature);
112113

113114
return feature;
114115
}

packages/turf-transform-scale/test.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { BBox, Feature, FeatureCollection } from "geojson";
1+
import {
2+
BBox,
3+
Feature,
4+
FeatureCollection,
5+
GeoJSON,
6+
GeometryCollection,
7+
} from "geojson";
28
import fs from "fs";
39
import test from "tape";
410
import path from "path";
@@ -277,6 +283,56 @@ function colorize(geojson: FeatureCollection | Feature) {
277283
return geojson;
278284
}
279285

286+
test("scale -- removes stale bbox values", (t) => {
287+
const input = nestedGeojsonWithBboxes();
288+
const scaled = scale(input, 2, { origin: [0, 0] });
289+
290+
t.equal(countBboxes(scaled), 0, "removes bboxes from cloned output");
291+
t.ok(countBboxes(input) > 0, "does not alter input by default");
292+
293+
scale(input, 2, { origin: [0, 0], mutate: true });
294+
t.equal(countBboxes(input), 0, "removes bboxes from mutated input");
295+
t.end();
296+
});
297+
298+
function nestedGeojsonWithBboxes() {
299+
const geojson = featureCollection([
300+
geometryCollection([
301+
lineString([
302+
[0, 0],
303+
[1, 1],
304+
]).geometry,
305+
]),
306+
]);
307+
addBboxes(geojson);
308+
return geojson;
309+
}
310+
311+
function addBboxes(geojson: GeoJSON | GeometryCollection): void {
312+
geojson.bbox = [0, 0, 1, 1];
313+
if (geojson.type === "Feature" && geojson.geometry) {
314+
addBboxes(geojson.geometry);
315+
} else if (geojson.type === "FeatureCollection") {
316+
geojson.features.forEach(addBboxes);
317+
} else if (geojson.type === "GeometryCollection") {
318+
geojson.geometries.forEach(addBboxes);
319+
}
320+
}
321+
322+
function countBboxes(geojson: GeoJSON | GeometryCollection): number {
323+
let count = geojson.bbox ? 1 : 0;
324+
if (geojson.type === "Feature" && geojson.geometry) {
325+
count += countBboxes(geojson.geometry);
326+
} else if (geojson.type === "FeatureCollection") {
327+
geojson.features.forEach((feature) => (count += countBboxes(feature)));
328+
} else if (geojson.type === "GeometryCollection") {
329+
geojson.geometries.forEach((geometry) => {
330+
count += countBboxes(geometry);
331+
});
332+
}
333+
return count;
334+
}
335+
280336
// define origin, as defined in transform-scale, and style it
281337
function markedOrigin(
282338
geojson: Feature,

packages/turf-transform-translate/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { GeoJSON, GeometryCollection } from "geojson";
22
import { coordEach } from "@turf/meta";
3-
import { isObject, Units } from "@turf/helpers";
3+
import { isObject, removeBbox, Units } from "@turf/helpers";
44
import { getCoords } from "@turf/invariant";
55
import { clone } from "@turf/clone";
66
import { rhumbDestination } from "@turf/rhumb-destination";
@@ -79,6 +79,7 @@ function transformTranslate<T extends GeoJSON | GeometryCollection>(
7979
if (zTranslation && pointCoords.length === 3)
8080
pointCoords[2] += zTranslation;
8181
});
82+
removeBbox(geojson);
8283
return geojson;
8384
}
8485

packages/turf-transform-translate/test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Feature } from "geojson";
1+
import { Feature, GeoJSON, GeometryCollection } from "geojson";
22
import fs from "fs";
33
import test from "tape";
44
import path from "path";
@@ -152,3 +152,53 @@ function colorize(geojson: Feature) {
152152
}
153153
return geojson;
154154
}
155+
156+
test("translate -- removes stale bbox values", (t) => {
157+
const input = nestedGeojsonWithBboxes();
158+
const translated = translate(input, 10, 45);
159+
160+
t.equal(countBboxes(translated), 0, "removes bboxes from cloned output");
161+
t.ok(countBboxes(input) > 0, "does not alter input by default");
162+
163+
translate(input, 10, 45, { mutate: true });
164+
t.equal(countBboxes(input), 0, "removes bboxes from mutated input");
165+
t.end();
166+
});
167+
168+
function nestedGeojsonWithBboxes() {
169+
const geojson = featureCollection([
170+
geometryCollection([
171+
lineString([
172+
[0, 0],
173+
[1, 1],
174+
]).geometry,
175+
]),
176+
]);
177+
addBboxes(geojson);
178+
return geojson;
179+
}
180+
181+
function addBboxes(geojson: GeoJSON | GeometryCollection): void {
182+
geojson.bbox = [0, 0, 1, 1];
183+
if (geojson.type === "Feature" && geojson.geometry) {
184+
addBboxes(geojson.geometry);
185+
} else if (geojson.type === "FeatureCollection") {
186+
geojson.features.forEach(addBboxes);
187+
} else if (geojson.type === "GeometryCollection") {
188+
geojson.geometries.forEach(addBboxes);
189+
}
190+
}
191+
192+
function countBboxes(geojson: GeoJSON | GeometryCollection): number {
193+
let count = geojson.bbox ? 1 : 0;
194+
if (geojson.type === "Feature" && geojson.geometry) {
195+
count += countBboxes(geojson.geometry);
196+
} else if (geojson.type === "FeatureCollection") {
197+
geojson.features.forEach((feature) => (count += countBboxes(feature)));
198+
} else if (geojson.type === "GeometryCollection") {
199+
geojson.geometries.forEach((geometry) => {
200+
count += countBboxes(geometry);
201+
});
202+
}
203+
return count;
204+
}

0 commit comments

Comments
 (0)