Skip to content

Commit 651ddcd

Browse files
authored
perf(boolean-contains): speed up point, line, and MultiPolygon hot paths (#3099)
* perf(boolean-contains): use native MultiPolygon support in Point check booleanPointInPolygon iterates MultiPolygon members natively, so a single call replaces one wrapped Polygon call per member - paying the per-call setup (validation, getCoord, getGeom) once instead of N times and avoiding the intermediate Polygon object allocations. Identical semantics: a point strictly inside any member polygon. * perf(boolean-contains): reduce point-on-line probes in MultiPoint check isMultiPointOnLine ran the interior probe (ignoreEndVertices: true) unconditionally for every point, even after an interior point had already been found, and ran it before the membership check - so points not on the line paid for both scans before failing. Check membership first (early exit), and only probe for an interior point until one is found. Identical semantics. * perf(boolean-contains): reduce point-on-line probes in LineString check isLineOnLine ran the interior probe (ignoreEndVertices: true) unconditionally for every vertex, even after an interior point had already been found, ran it before the membership check, and wrapped every coordinate in a fresh Point object although booleanPointOnLine accepts raw positions. Check membership first (early exit), only probe for an interior point (vertex, then segment midpoint) until one is found, and pass raw coordinates. Identical semantics, including the segment-midpoint interior detection from #3080. * perf(boolean-contains): avoid redundant bbox computation in MultiPolygon polygon checks isPolygonInMultiPolygon and isMultiPolygonInMultiPolygon called isPolyInPoly once per member polygon, and each call recomputed the bbox of both geometries - so the candidate polygon's bbox was recomputed N times for an N-member MultiPolygon. Let isPolyInPoly accept an optional precomputed bbox for the candidate polygon and compute it once per candidate in the MultiPolygon loops. Identical semantics. An additional whole-MultiPolygon bbox prefilter was tried: it sped up the fully-outside case by about 50%, but regressed the overlapping-bbox cases by ~15%, since `calcBbox` over all members is pure added cost whenever the check passes.
1 parent 07aae6f commit 651ddcd

1 file changed

Lines changed: 40 additions & 46 deletions

File tree

packages/turf-boolean-contains/index.ts

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,10 @@ function booleanContains(
110110
}
111111

112112
function isPolygonInMultiPolygon(multiPolygon: MultiPolygon, polygon: Polygon) {
113+
// Compute the polygon's bbox once instead of once per member polygon
114+
const polygonBbox = calcBbox(polygon);
113115
return multiPolygon.coordinates.some((coords) =>
114-
isPolyInPoly({ type: "Polygon", coordinates: coords }, polygon)
116+
isPolyInPoly({ type: "Polygon", coordinates: coords }, polygon, polygonBbox)
115117
);
116118
}
117119

@@ -124,15 +126,9 @@ function isPolygonInMultiPolygon(multiPolygon: MultiPolygon, polygon: Polygon) {
124126
* @returns {boolean} true if point is inside the interior of any polygon in the MultiPolygon
125127
*/
126128
function isPointInMultiPolygon(multiPolygon: MultiPolygon, point: Point) {
127-
return multiPolygon.coordinates.some((coords) =>
128-
booleanPointInPolygon(
129-
point,
130-
{ type: "Polygon", coordinates: coords },
131-
{
132-
ignoreBoundary: true,
133-
}
134-
)
135-
);
129+
// booleanPointInPolygon supports MultiPolygon natively - a single call is
130+
// significantly cheaper than one wrapped call per member polygon
131+
return booleanPointInPolygon(point, multiPolygon, { ignoreBoundary: true });
136132
}
137133

138134
/**
@@ -236,10 +232,14 @@ function isMultiPolygonInMultiPolygon(
236232
multiPolygon2: MultiPolygon
237233
) {
238234
for (const poly2Coords of multiPolygon2.coordinates) {
235+
const poly2: Polygon = { type: "Polygon", coordinates: poly2Coords };
236+
// Compute the candidate polygon's bbox once instead of per member polygon
237+
const poly2Bbox = calcBbox(poly2);
239238
const polyInside = multiPolygon1.coordinates.some((poly1Coords) =>
240239
isPolyInPoly(
241240
{ type: "Polygon", coordinates: poly1Coords },
242-
{ type: "Polygon", coordinates: poly2Coords }
241+
poly2,
242+
poly2Bbox
243243
)
244244
);
245245
if (!polyInside) {
@@ -289,17 +289,19 @@ function isMultiPointInMultiPoint(
289289
function isMultiPointOnLine(lineString: LineString, multiPoint: MultiPoint) {
290290
let haveFoundInteriorPoint = false;
291291
for (const coord of multiPoint.coordinates) {
292-
if (isPointOnLine(coord, lineString, { ignoreEndVertices: true })) {
293-
haveFoundInteriorPoint = true;
294-
}
292+
// Membership check first so points not on the line exit early
295293
if (!isPointOnLine(coord, lineString)) {
296294
return false;
297295
}
296+
// Only probe for an interior point until one has been found
297+
if (
298+
!haveFoundInteriorPoint &&
299+
isPointOnLine(coord, lineString, { ignoreEndVertices: true })
300+
) {
301+
haveFoundInteriorPoint = true;
302+
}
298303
}
299-
if (haveFoundInteriorPoint) {
300-
return true;
301-
}
302-
return false;
304+
return haveFoundInteriorPoint;
303305
}
304306

305307
function isMultiPointInPoly(polygon: Polygon, multiPoint: MultiPoint) {
@@ -325,35 +327,26 @@ function isLineOnLine(lineString1: LineString, lineString2: LineString) {
325327
const coordinates = lineString2.coordinates;
326328
for (let i = 0; i < coordinates.length; i++) {
327329
const coords = coordinates[i];
328-
if (
329-
isPointOnLine({ type: "Point", coordinates: coords }, lineString1, {
330-
ignoreEndVertices: true,
331-
})
332-
) {
333-
haveFoundInteriorPoint = true;
334-
}
335-
if (
336-
!isPointOnLine({ type: "Point", coordinates: coords }, lineString1, {
337-
ignoreEndVertices: false,
338-
})
339-
) {
330+
// Membership check first so vertices not on the line exit early
331+
if (!isPointOnLine(coords, lineString1)) {
340332
return false;
341333
}
342-
// A segment whose endpoints are both on lineString1 (e.g. lineString2's
343-
// vertices coincide with lineString1's boundary) still shares interior with
344-
// lineString1. Probe the segment midpoint so an interior overlap is detected
345-
// even when no vertex of lineString2 is strictly interior.
346-
if (!haveFoundInteriorPoint && i > 0) {
347-
const midpoint: Position = [
348-
(coordinates[i - 1][0] + coords[0]) / 2,
349-
(coordinates[i - 1][1] + coords[1]) / 2,
350-
];
351-
if (
352-
isPointOnLine({ type: "Point", coordinates: midpoint }, lineString1, {
353-
ignoreEndVertices: true,
354-
})
355-
) {
334+
// Only probe for an interior point until one has been found
335+
if (!haveFoundInteriorPoint) {
336+
if (isPointOnLine(coords, lineString1, { ignoreEndVertices: true })) {
356337
haveFoundInteriorPoint = true;
338+
} else if (i > 0) {
339+
// A segment whose endpoints are both on lineString1 (e.g. lineString2's
340+
// vertices coincide with lineString1's boundary) still shares interior
341+
// with lineString1. Probe the segment midpoint so an interior overlap
342+
// is detected even when no vertex of lineString2 is strictly interior.
343+
const midpoint: Position = [
344+
(coordinates[i - 1][0] + coords[0]) / 2,
345+
(coordinates[i - 1][1] + coords[1]) / 2,
346+
];
347+
if (isPointOnLine(midpoint, lineString1, { ignoreEndVertices: true })) {
348+
haveFoundInteriorPoint = true;
349+
}
357350
}
358351
}
359352
}
@@ -452,7 +445,8 @@ function lineInPolyStatus(
452445
*/
453446
function isPolyInPoly(
454447
feature1: Feature<Polygon> | Polygon,
455-
feature2: Feature<Polygon> | Polygon
448+
feature2: Feature<Polygon> | Polygon,
449+
feature2Bbox?: BBox
456450
) {
457451
// Handle Nulls
458452
if (feature1.type === "Feature" && feature1.geometry === null) {
@@ -463,7 +457,7 @@ function isPolyInPoly(
463457
}
464458

465459
const poly1Bbox = calcBbox(feature1);
466-
const poly2Bbox = calcBbox(feature2);
460+
const poly2Bbox = feature2Bbox ?? calcBbox(feature2);
467461
if (!doBBoxOverlap(poly1Bbox, poly2Bbox)) {
468462
return false;
469463
}

0 commit comments

Comments
 (0)