From 4a4ac063284e63f19357d47e6c93fe98ae66bbcc Mon Sep 17 00:00:00 2001 From: James Beard Date: Sat, 17 May 2025 21:33:49 +1000 Subject: [PATCH 1/2] Converted clustersDbscan to use geokdbush rather than rbush. The former is better suited to static sets of points such as those processed in this module. --- packages/turf-clusters-dbscan/index.ts | 70 +++++----------- .../turf-clusters-dbscan/lib/rbush-export.ts | 7 -- packages/turf-clusters-dbscan/package.json | 6 +- .../turf-clusters-dbscan/test/in/fiji.geojson | 6 ++ pnpm-lock.yaml | 81 ++++++++++++++++--- 5 files changed, 99 insertions(+), 71 deletions(-) delete mode 100644 packages/turf-clusters-dbscan/lib/rbush-export.ts diff --git a/packages/turf-clusters-dbscan/index.ts b/packages/turf-clusters-dbscan/index.ts index ef87d5ec74..b003ff0006 100644 --- a/packages/turf-clusters-dbscan/index.ts +++ b/packages/turf-clusters-dbscan/index.ts @@ -1,8 +1,8 @@ import { GeoJsonProperties, FeatureCollection, Point } from "geojson"; import { clone } from "@turf/clone"; -import { distance } from "@turf/distance"; -import { degreesToRadians, lengthToDegrees, Units } from "@turf/helpers"; -import { rbush as RBush } from "./lib/rbush-export.js"; +import { Units } from "@turf/helpers"; +import KDBush from "kdbush"; +import * as geokdbush from "geokdbush"; /** * Point classification within the cluster. @@ -77,11 +77,13 @@ function clustersDbscan( // Defaults const minPoints = options.minPoints || 3; - // Calculate the distance in degrees for region queries - const latDistanceInDegrees = lengthToDegrees(maxDistance, options.units); - // Create a spatial index - var tree = new RBush(points.features.length); + const kdIndex = new KDBush(points.features.length); + // Index each point for spatial queries + for (const point of points.features) { + kdIndex.add(point.geometry.coordinates[0], point.geometry.coordinates[1]); + } + kdIndex.finish(); // Keeps track of whether a point has been visited or not. var visited = points.features.map((_) => false); @@ -95,54 +97,22 @@ function clustersDbscan( // Keeps track of the clusterId for each point var clusterIds: number[] = points.features.map((_) => -1); - // Index each point for spatial queries - tree.load( - points.features.map((point, index) => { - var [x, y] = point.geometry.coordinates; - return { - minX: x, - minY: y, - maxX: x, - maxY: y, - index: index, - } as IndexedPoint; - }) - ); - // Function to find neighbors of a point within a given distance const regionQuery = (index: number): IndexedPoint[] => { const point = points.features[index]; const [x, y] = point.geometry.coordinates; - const minY = Math.max(y - latDistanceInDegrees, -90.0); - const maxY = Math.min(y + latDistanceInDegrees, 90.0); - - const lonDistanceInDegrees = (function () { - // Handle the case where the bounding box crosses the poles - if (minY < 0 && maxY > 0) { - return latDistanceInDegrees; - } - if (Math.abs(minY) < Math.abs(maxY)) { - return latDistanceInDegrees / Math.cos(degreesToRadians(maxY)); - } else { - return latDistanceInDegrees / Math.cos(degreesToRadians(minY)); - } - })(); - - const minX = Math.max(x - lonDistanceInDegrees, -360.0); - const maxX = Math.min(x + lonDistanceInDegrees, 360.0); - - // Calculate the bounding box for the region query - const bbox = { minX, minY, maxX, maxY }; - return (tree.search(bbox) as ReadonlyArray).filter( - (neighbor) => { - const neighborIndex = neighbor.index; - const neighborPoint = points.features[neighborIndex]; - const distanceInKm = distance(point, neighborPoint, { - units: "kilometers", - }); - return distanceInKm <= maxDistance; - } + return ( + geokdbush + // @ts-expect-error - until https://github.com/mourner/geokdbush/issues/20 is resolved + .around(kdIndex, x, y, undefined, maxDistance) + .map((id) => ({ + minX: points.features[id].geometry.coordinates[0], + minY: points.features[id].geometry.coordinates[1], + maxX: points.features[id].geometry.coordinates[0], + maxY: points.features[id].geometry.coordinates[1], + index: id, + })) ); }; diff --git a/packages/turf-clusters-dbscan/lib/rbush-export.ts b/packages/turf-clusters-dbscan/lib/rbush-export.ts deleted file mode 100644 index 49463b8ab5..0000000000 --- a/packages/turf-clusters-dbscan/lib/rbush-export.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Get around problems with moduleResolution node16 and some older libraries. -// Manifests as "This expression is not callable ... has no call signatures" -// https://stackoverflow.com/a/74709714 - -import lib from "rbush"; - -export const rbush = lib as unknown as typeof lib.default; diff --git a/packages/turf-clusters-dbscan/package.json b/packages/turf-clusters-dbscan/package.json index 33a6d757aa..3e32267d87 100644 --- a/packages/turf-clusters-dbscan/package.json +++ b/packages/turf-clusters-dbscan/package.json @@ -63,7 +63,6 @@ "@turf/centroid": "workspace:*", "@turf/clusters": "workspace:*", "@types/benchmark": "^2.1.5", - "@types/rbush": "^3.0.2", "@types/tape": "^5.8.1", "benchmark": "^2.1.4", "chromatism": "^3.0.0", @@ -78,11 +77,12 @@ }, "dependencies": { "@turf/clone": "workspace:*", - "@turf/distance": "workspace:*", "@turf/helpers": "workspace:*", "@turf/meta": "workspace:*", "@types/geojson": "^7946.0.10", - "rbush": "^3.0.1", + "@types/geokdbush": "^1.1.5", + "geokdbush": "^2.0.1", + "kdbush": "^4.0.2", "tslib": "^2.8.1" } } diff --git a/packages/turf-clusters-dbscan/test/in/fiji.geojson b/packages/turf-clusters-dbscan/test/in/fiji.geojson index fa47af397b..2ae121be00 100644 --- a/packages/turf-clusters-dbscan/test/in/fiji.geojson +++ b/packages/turf-clusters-dbscan/test/in/fiji.geojson @@ -3,6 +3,7 @@ "features": [ { "type": "Feature", + "properties": {}, "geometry": { "type": "Point", "coordinates": [179.439697265625, -16.55196172197251] @@ -10,6 +11,7 @@ }, { "type": "Feature", + "properties": {}, "geometry": { "type": "Point", "coordinates": [179.01123046874997, -16.97274101999901] @@ -17,6 +19,7 @@ }, { "type": "Feature", + "properties": {}, "geometry": { "type": "Point", "coordinates": [179.505615234375, -17.035777250427184] @@ -24,6 +27,7 @@ }, { "type": "Feature", + "properties": {}, "geometry": { "type": "Point", "coordinates": [180.75805664062497, -16.41500926733237] @@ -31,6 +35,7 @@ }, { "type": "Feature", + "properties": {}, "geometry": { "type": "Point", "coordinates": [181.1865234375, -16.615137799987075] @@ -38,6 +43,7 @@ }, { "type": "Feature", + "properties": {}, "geometry": { "type": "Point", "coordinates": [181.03271484375, -16.277960306212513] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 203ee68483..0f5fbbd88e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2195,9 +2195,6 @@ importers: '@turf/clone': specifier: workspace:* version: link:../turf-clone - '@turf/distance': - specifier: workspace:* - version: link:../turf-distance '@turf/helpers': specifier: workspace:* version: link:../turf-helpers @@ -2207,9 +2204,15 @@ importers: '@types/geojson': specifier: ^7946.0.10 version: 7946.0.14 - rbush: - specifier: ^3.0.1 - version: 3.0.1 + '@types/geokdbush': + specifier: ^1.1.5 + version: 1.1.5 + geokdbush: + specifier: ^2.0.1 + version: 2.0.1 + kdbush: + specifier: ^4.0.2 + version: 4.0.2 tslib: specifier: ^2.8.1 version: 2.8.1 @@ -2223,9 +2226,6 @@ importers: '@types/benchmark': specifier: ^2.1.5 version: 2.1.5 - '@types/rbush': - specifier: ^3.0.2 - version: 3.0.3 '@types/tape': specifier: ^5.8.1 version: 5.8.1 @@ -2249,7 +2249,7 @@ importers: version: 5.9.0 tsup: specifier: ^8.4.0 - version: 8.4.0(postcss@8.5.3)(tsx@4.19.4)(typescript@5.8.3) + version: 8.4.0(postcss@8.5.3)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.7.1) tsx: specifier: ^4.19.4 version: 4.19.4 @@ -7881,12 +7881,18 @@ packages: '@types/geojson@7946.0.14': resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==} + '@types/geokdbush@1.1.5': + resolution: {integrity: sha512-jIsYnXY+RQ/YCyBqeEHxYN9mh+7PqKJUJUp84wLfZ7T2kqyVPNaXwZuvf1A2uQUkrvVqEbsG94ff8jH32AlLvA==} + '@types/hast@2.3.10': resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/kdbush@1.0.7': + resolution: {integrity: sha512-QM5iB8m/0mnGOjUKshErIZQ0LseyTieRSYc3yaOpmrRM0xbWiOuJUWlduJx+TPNK7/VFMWphUGwx3nus7eT1Wg==} + '@types/mdast@3.0.15': resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} @@ -9118,6 +9124,9 @@ packages: geojson-polygon-self-intersections@1.2.1: resolution: {integrity: sha512-/QM1b5u2d172qQVO//9CGRa49jEmclKEsYOQmWP9ooEjj63tBM51m2805xsbxkzlEELQ2REgTf700gUhhlegxA==} + geokdbush@2.0.1: + resolution: {integrity: sha512-0M8so1Qx6+jJ1xpirpCNrgUsWAzIcQ3LrLmh0KJPBYI3gH7vy70nY5zEEjSp9Tn0nBt6Q2Fh922oL08lfib4Zg==} + get-amd-module-type@6.0.1: resolution: {integrity: sha512-MtjsmYiCXcYDDrGqtNbeIYdAl85n+5mSv2r3FbzER/YV3ZILw4HNNIw34HuV5pyl0jzs6GFYU1VHVEefhgcNHQ==} engines: {node: '>=18'} @@ -9817,6 +9826,9 @@ packages: just-diff@6.0.2: resolution: {integrity: sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==} + kdbush@4.0.2: + resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -13441,12 +13453,18 @@ snapshots: '@types/geojson@7946.0.14': {} + '@types/geokdbush@1.1.5': + dependencies: + '@types/kdbush': 1.0.7 + '@types/hast@2.3.10': dependencies: '@types/unist': 2.0.10 '@types/json-schema@7.0.15': {} + '@types/kdbush@1.0.7': {} + '@types/mdast@3.0.15': dependencies: '@types/unist': 2.0.10 @@ -14759,7 +14777,7 @@ snapshots: reusify: 1.0.4 fdir@6.4.4(picomatch@4.0.2): - dependencies: + optionalDependencies: picomatch: 4.0.2 fecha@4.2.3: {} @@ -14897,6 +14915,10 @@ snapshots: dependencies: rbush: 2.0.2 + geokdbush@2.0.1: + dependencies: + tinyqueue: 2.0.3 + get-amd-module-type@6.0.1: dependencies: ast-module-types: 6.0.1 @@ -15602,6 +15624,8 @@ snapshots: just-diff@6.0.2: {} + kdbush@4.0.2: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -16841,6 +16865,14 @@ snapshots: postcss: 8.5.3 tsx: 4.19.4 + postcss-load-config@6.0.1(postcss@8.5.3)(tsx@4.19.4)(yaml@2.7.1): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.3 + tsx: 4.19.4 + yaml: 2.7.1 + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 @@ -17749,6 +17781,33 @@ snapshots: - tsx - yaml + tsup@8.4.0(postcss@8.5.3)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.7.1): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.3) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.0 + esbuild: 0.25.3 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.3)(tsx@4.19.4)(yaml@2.7.1) + resolve-from: 5.0.0 + rollup: 4.40.1 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.13 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.3 + typescript: 5.8.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsx@4.19.4: dependencies: esbuild: 0.25.3 From 862cf89696a0840189337d4e7e48fa24c03af294 Mon Sep 17 00:00:00 2001 From: James Beard Date: Sat, 17 May 2025 21:37:51 +1000 Subject: [PATCH 2/2] Added a TODO to re-enable some disabled runtime checks in clustersDbscan when we can. --- packages/turf-clusters-dbscan/index.ts | 3 +++ packages/turf-nearest-neighbor-analysis/README.md | 2 +- packages/turf-transform-translate/README.md | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/turf-clusters-dbscan/index.ts b/packages/turf-clusters-dbscan/index.ts index b003ff0006..6ab15101bd 100644 --- a/packages/turf-clusters-dbscan/index.ts +++ b/packages/turf-clusters-dbscan/index.ts @@ -66,6 +66,9 @@ function clustersDbscan( } = {} ): FeatureCollection { // Input validation being handled by Typescript + // TODO oops! No it isn't. Typescript doesn't do runtime checking. We should + // re-enable these checks, though will have to wait for a major version bump + // as more restrictive checks could break currently working code. // collectionOf(points, 'Point', 'points must consist of a FeatureCollection of only Points'); // if (maxDistance === null || maxDistance === undefined) throw new Error('maxDistance is required'); // if (!(Math.sign(maxDistance) > 0)) throw new Error('maxDistance is invalid'); diff --git a/packages/turf-nearest-neighbor-analysis/README.md b/packages/turf-nearest-neighbor-analysis/README.md index 0536682bd9..25121b22a5 100644 --- a/packages/turf-nearest-neighbor-analysis/README.md +++ b/packages/turf-nearest-neighbor-analysis/README.md @@ -30,7 +30,7 @@ Type: [object][1] ## nearestNeighborAnalysis -Nearest Neighbor Analysis calculates an index based the average distances +Nearest Neighbor Analysis calculates an index based on the average distances between points in the dataset, thereby providing inference as to whether the data is clustered, dispersed, or randomly distributed within the study area. diff --git a/packages/turf-transform-translate/README.md b/packages/turf-transform-translate/README.md index 846ebc0bc1..e7e2048b10 100644 --- a/packages/turf-transform-translate/README.md +++ b/packages/turf-transform-translate/README.md @@ -7,6 +7,9 @@ Moves any geojson Feature or Geometry of a specified distance along a Rhumb Line on the provided direction angle. +Note that this moves the points of your shape individually and can therefore change +the overall shape. How noticable this is depends on the distance and the used projection. + ### Parameters * `geojson` **([GeoJSON][1] | [GeometryCollection][2])** object to be translated