Skip to content

feat(intersect-by-area-percentage): add new module #2877

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions packages/turf-intersect-by-area-percentage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import intersect from "@turf/intersect";
import area from "@turf/area";
import {
feature,
featureCollection,
isObject,
} from "@turf/helpers";
import {
Feature,
Polygon,
MultiPolygon,
} from "geojson";
import * as invariant from "@turf/invariant";

/**
* Checks if the intersection area between a target polygon (targetPolygon)
* and a test polygon (testPolygon) is greater than or equal to a specified
* threshold percentage of the test polygon's *own* area.
*
* @name intersectByAreaPercentage
* @param {Feature<Polygon|MultiPolygon>|Polygon|MultiPolygon} targetPolygon The target polygon feature.
* @param {Feature<Polygon|MultiPolygon>|Polygon|MultiPolygon} testPolygon The test polygon feature whose intersection area is being evaluated.
* @param {number} threshold The minimum required percentage (from 0.0 to 1.0) of testPolygon's area that must be inside targetPolygon.
* @returns {boolean} True if the ratio (intersectionArea / testPolygonArea) is >= threshold.
* Returns false if inputs are invalid, threshold is invalid, testPolygon has zero area and threshold > 0,
* or if an error occurs during calculation. Returns true if threshold is 0 and polygons do not intersect
* or if testPolygon has zero area.
* @example
* const poly1 = turf.polygon([[[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]]);
* const poly2 = turf.polygon([[[0, 0], [20, 0], [20, 20], [0, 20], [0, 0]]]); // Overlaps 1/4 (25%) of poly1
* const poly3 = turf.polygon([[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]]); // Fully contained in poly1
*
* turf.intersectByAreaPercentage(poly1, poly2, 0.25); // => true (intersection is 100 area units, poly2 is 400 area units => 100/400 = 0.25)
* turf.intersectByAreaPercentage(poly1, poly2, 0.26); // => false
* turf.intersectByAreaPercentage(poly1, poly3, 0.99); // => true (intersection is 100% of poly3's area)
* turf.intersectByAreaPercentage(poly1, poly3, 0.0); // => true
*/
function intersectByAreaPercentage(
targetPolygon: Feature<Polygon | MultiPolygon> | Polygon | MultiPolygon,
testPolygon: Feature<Polygon | MultiPolygon> | Polygon | MultiPolygon,
threshold: number
): boolean {
// Basic validation
if (!targetPolygon || !testPolygon || typeof threshold !== "number" || threshold < 0 || threshold > 1) {
return false;
}

// Use invariant.getGeom().type for safer type checking
let targetType: string;
let testType: string;
try {
targetType = invariant.getGeom(targetPolygon).type;
testType = invariant.getGeom(testPolygon).type;
} catch (e) {
return false; // Invalid geometry input
}

if (
(targetType !== "Polygon" && targetType !== "MultiPolygon") ||
(testType !== "Polygon" && testType !== "MultiPolygon")
) {
return false;
}

try {
const testPolygonArea = area(testPolygon);

// Handle zero area test polygon
if (testPolygonArea === 0) {
return threshold === 0;
}

// Ensure inputs are valid Features for intersect.
// intersect expects Feature<Polygon | MultiPolygon>.
let targetFeature: Feature<Polygon | MultiPolygon>;
let testFeature: Feature<Polygon | MultiPolygon>;

try {
const targetGeom = invariant.getGeom(targetPolygon);
const testGeom = invariant.getGeom(testPolygon);

// Re-check types after getting geometry
if ((targetGeom.type !== "Polygon" && targetGeom.type !== "MultiPolygon") || (testGeom.type !== "Polygon" && testGeom.type !== "MultiPolygon")) {
return false;
}

// If original input was Geometry, wrap in Feature. Otherwise, use the Feature.
targetFeature = targetGeom === targetPolygon ? feature(targetGeom) : targetPolygon as Feature<Polygon | MultiPolygon>;
testFeature = testGeom === testPolygon ? feature(testGeom) : testPolygon as Feature<Polygon | MultiPolygon>;

} catch (e) {
return false; // Error during geometry retrieval or type check
}

const fc = featureCollection([targetFeature, testFeature]);

const intersectionResult = intersect(fc);

// No intersection or only touch
if (intersectionResult === null) {
return threshold === 0;
}

const intersectionArea = area(intersectionResult);

// Handle zero area intersection (e.g., linear touch)
if (intersectionArea === 0) {
return threshold === 0;
}

// Calculate overlap ratio relative to testPolygon's area
const overlapRatio = intersectionArea / testPolygonArea;

return overlapRatio >= threshold;
} catch (e) {
// Treat calculation errors as 'condition not met'
// console.error(`Error during intersect/area calculation: ${e.message}`);
return false;
}
}

export default intersectByAreaPercentage;
78 changes: 78 additions & 0 deletions packages/turf-intersect-by-area-percentage/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"name": "@turf/intersect-by-area-percentage",
"version": "7.2.0",
"description": "Checks if the intersection area between two polygons meets a threshold percentage relative to the second polygon's area.",
"author": "Turf Authors",
"contributors": [
"Gairo Peralta <@Gerion9>"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/Turfjs/turf/issues"
},
"homepage": "https://github.com/Turfjs/turf/tree/master/packages/turf-intersect-by-area-percentage#readme",
"repository": {
"type": "git",
"url": "git://github.com/Turfjs/turf.git"
},
"funding": "https://opencollective.com/turf",
"publishConfig": {
"access": "public"
},
"keywords": [
"turf",
"polygon",
"intersection",
"overlap",
"percentage",
"area"
],
"type": "module",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/cjs/index.d.cts",
"default": "./dist/cjs/index.cjs"
}
}
},
"sideEffects": false,
"files": [
"dist"
],
"scripts": {
"bench": "tsx bench.ts",
"build": "tsup --config ../../tsup.config.ts",
"docs": "tsx ../../scripts/generate-readmes.ts",
"test": "npm-run-all --npm-path npm test:*",
"test:tape": "tsx test.ts"
},
"devDependencies": {
"@types/benchmark": "^2.1.5",
"@types/tape": "^4.13.4",
"benchmark": "^2.1.4",
"load-json-file": "^7.0.1",
"npm-run-all": "^4.1.5",
"tape": "^5.9.0",
"tsup": "^8.3.5",
"tsx": "^4.19.2",
"typescript": "^5.5.4",
"write-json-file": "^5.0.0"
},
"dependencies": {
"@turf/area": "workspace:^",
"@turf/helpers": "workspace:^",
"@turf/intersect": "workspace:^",
"@turf/invariant": "workspace:^",
"@types/geojson": "^7946.0.10",
"tslib": "^2.8.1"
}
}
84 changes: 84 additions & 0 deletions packages/turf-intersect-by-area-percentage/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { test } from "tape";
import { globSync } from "glob";
import path from "path";
import { fileURLToPath } from "url";
import { loadJsonFileSync } from "load-json-file";
import { polygon, multiPolygon, featureCollection } from "@turf/helpers";
import intersectByAreaPercentage from "./index.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

test("@turf/intersect-by-area-percentage", (t) => {
// Load test fixtures
const fixtures = globSync(path.join(__dirname, "test", "in", "*.json")).map(
(filepath) => {
const name = path.parse(filepath).name;
const geojson = loadJsonFileSync(filepath);
return {
name,
geojson,
filepath,
};
}
);

for (const { name, geojson } of fixtures) {
const { features } = geojson;
const [poly1, poly2, poly3, polyNoOverlap, multiPoly1] = features;

// Test cases based on fixture data
t.true(
intersectByAreaPercentage(poly1, poly3, 0.2),
`${name}: poly1 vs poly3 (threshold 0.2) - Should intersect >= 20%`
);
t.false(
intersectByAreaPercentage(poly1, poly2, 0.3),
`${name}: poly1 vs poly2 (threshold 0.3) - Should intersect < 30%`
);
t.true(
intersectByAreaPercentage(poly1, poly2, 0.25),
`${name}: poly1 vs poly2 (threshold 0.25) - Should intersect >= 25% (exact)`
);
t.false(
intersectByAreaPercentage(poly1, polyNoOverlap, 0.01),
`${name}: poly1 vs polyNoOverlap (threshold 0.01) - Should not intersect`
);
t.true(
intersectByAreaPercentage(poly1, polyNoOverlap, 0.0),
`${name}: poly1 vs polyNoOverlap (threshold 0.0) - Should be true for zero threshold`
);
t.true(
intersectByAreaPercentage(poly1, poly1, 1.0),
`${name}: poly1 vs poly1 (threshold 1.0) - Should be 100% overlap`
);
t.false(
intersectByAreaPercentage(poly1, poly1, 1.1),
`${name}: poly1 vs poly1 (invalid threshold > 1.0) - Should be false`
);
t.false(
intersectByAreaPercentage(poly1, poly1, -0.1),
`${name}: poly1 vs poly1 (invalid threshold < 0.0) - Should be false`
);
t.true(
intersectByAreaPercentage(poly1, multiPoly1, 0.49),
`${name}: poly1 vs multiPoly1 (threshold 0.49) - Should intersect >= 49%`
);
t.false(
intersectByAreaPercentage(poly1, multiPoly1, 0.51),
`${name}: poly1 vs multiPoly1 (threshold 0.51) - Should intersect < 51%`
);
}

// Additional edge cases
const zeroAreaPoly = polygon([[[0, 0], [0, 0], [0, 0], [0, 0]]]);
const targetPoly = polygon([[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]]);

t.true(intersectByAreaPercentage(targetPoly, zeroAreaPoly, 0.0), "Zero area test polygon with threshold 0.0");
t.false(intersectByAreaPercentage(targetPoly, zeroAreaPoly, 0.1), "Zero area test polygon with threshold > 0.0");
t.false(intersectByAreaPercentage(null, targetPoly, 0.5), "Null target polygon");
t.false(intersectByAreaPercentage(targetPoly, null, 0.5), "Null test polygon");
// @ts-expect-error testing invalid input
t.false(intersectByAreaPercentage(targetPoly, targetPoly, 'invalid'), "Invalid threshold type");

t.end();
});
48 changes: 48 additions & 0 deletions packages/turf-intersect-by-area-percentage/test/in/polygons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "id": "poly1" },
"geometry": {
"type": "Polygon",
"coordinates": [[[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]]
}
},
{
"type": "Feature",
"properties": { "id": "poly2" },
"geometry": {
"type": "Polygon",
"coordinates": [[[0, 0], [20, 0], [20, 20], [0, 20], [0, 0]]]
}
},
{
"type": "Feature",
"properties": { "id": "poly3" },
"geometry": {
"type": "Polygon",
"coordinates": [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]]
}
},
{
"type": "Feature",
"properties": { "id": "polyNoOverlap" },
"geometry": {
"type": "Polygon",
"coordinates": [[[50, 50], [60, 50], [60, 60], [50, 60], [50, 50]]]
}
},
{
"type": "Feature",
"properties": { "id": "multiPoly1" },
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[[[5, 5], [15, 5], [15, 15], [5, 15], [5, 5]]],
[[[-15, -15], [-5, -15], [-5, -5], [-15, -5], [-15, -15]]]
]
}
}
]
}
10 changes: 10 additions & 0 deletions packages/turf-intersect-by-area-percentage/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.shared.json",
"compilerOptions": {
"outDir": "./dist/esm",
"rootDir": ".",
"composite": true
},
"include": ["*.ts", "*.d.ts", "./lib"],
"exclude": ["test.ts", "bench.ts", "./dist"]
}
Loading