diff --git a/packages/turf-clusters-dbscan/index.ts b/packages/turf-clusters-dbscan/index.ts index 930c77e893..4467356024 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. @@ -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'); @@ -77,11 +80,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 +100,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 9cfe067c4e..d1f6cd1310 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 @@ -7878,12 +7878,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==} @@ -9115,6 +9121,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'} @@ -9814,6 +9823,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==} @@ -13438,12 +13450,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 @@ -14894,6 +14912,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 @@ -15599,6 +15621,8 @@ snapshots: just-diff@6.0.2: {} + kdbush@4.0.2: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1