diff --git a/packages/turf-standard-deviational-ellipse/README.md b/packages/turf-standard-deviational-ellipse/README.md index 14891014f2..28983314cc 100644 --- a/packages/turf-standard-deviational-ellipse/README.md +++ b/packages/turf-standard-deviational-ellipse/README.md @@ -4,70 +4,68 @@ ## standardDeviationalEllipse -Takes a [FeatureCollection][1] and returns a standard deviational ellipse, +Takes a collection of features and returns a standard deviational ellipse, also known as a “directional distribution.” The standard deviational ellipse aims to show the direction and the distribution of a dataset by drawing an ellipse that contains about one standard deviation’s worth (~ 70%) of the data. -This module mirrors the functionality of [Directional Distribution][2] -in ArcGIS and the [QGIS Standard Deviational Ellipse Plugin][3] +This module mirrors the functionality of [Directional Distribution][1] +in ArcGIS and the [QGIS Standard Deviational Ellipse Plugin][2] **Bibliography** • Robert S. Yuill, “The Standard Deviational Ellipse; An Updated Tool for Spatial Description,” *Geografiska Annaler* 53, no. 1 (1971): 28–39, -doi:{@link [https://doi.org/10.2307/490885|10.2307/490885}][4]. +doi:{@link [https://doi.org/10.2307/490885|10.2307/490885}][3]. • Paul Hanly Furfey, “A Note on Lefever’s “Standard Deviational Ellipse,” *American Journal of Sociology* 33, no. 1 (1927): 94—98, -doi:{@link [https://doi.org/10.1086/214336|10.1086/214336}][5]. +doi:{@link [https://doi.org/10.1086/214336|10.1086/214336}][4]. ### Parameters -* `points` **[FeatureCollection][6]<[Point][7]>** GeoJSON points -* `options` **[Object][8]** Optional parameters (optional, default `{}`) +* `points` **[FeatureCollection][5]<[Point][6]>** GeoJSON points +* `options` **[Object][7]** Optional parameters (optional, default `{}`) - * `options.weight` **[string][9]?** the property name used to weight the center - * `options.steps` **[number][10]** number of steps for the polygon (optional, default `64`) - * `options.properties` **[Object][8]** properties to pass to the resulting ellipse (optional, default `{}`) + * `options.weight` **[string][8]?** the property name used to weight the center + * `options.steps` **[number][9]** number of steps for the polygon (optional, default `64`) + * `options.properties` **[Object][7]** properties to pass to the resulting ellipse (optional, default `{}`) ### Examples ```javascript -var bbox = [-74, 40.72, -73.98, 40.74]; -var points = turf.randomPoint(400, {bbox: bbox}); -var sdEllipse = turf.standardDeviationalEllipse(points); +const bbox = [-74, 40.72, -73.98, 40.74]; +const points = turf.randomPoint(400, {bbox: bbox}); +const sdEllipse = turf.standardDeviationalEllipse(points); //addToMap -var addToMap = [points, sdEllipse]; +const addToMap = [points, sdEllipse]; ``` -Returns **[Feature][11]<[Polygon][12]>** an elliptical Polygon that includes approximately 1 SD of the dataset within it. +Returns **[Feature][10]<[Polygon][11]>** an elliptical Polygon that includes approximately 1 SD of the dataset within it. -[1]: https://tools.ietf.org/html/rfc7946#section-3.3 +[1]: http://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-statistics-toolbox/directional-distribution.htm -[2]: http://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-statistics-toolbox/directional-distribution.htm +[2]: http://arken.nmbu.no/~havatv/gis/qgisplugins/SDEllipse/ -[3]: http://arken.nmbu.no/~havatv/gis/qgisplugins/SDEllipse/ +[3]: https://doi.org/10.2307/490885|10.2307/490885} -[4]: https://doi.org/10.2307/490885|10.2307/490885} +[4]: https://doi.org/10.1086/214336|10.1086/214336} -[5]: https://doi.org/10.1086/214336|10.1086/214336} +[5]: https://tools.ietf.org/html/rfc7946#section-3.3 -[6]: https://tools.ietf.org/html/rfc7946#section-3.3 +[6]: https://tools.ietf.org/html/rfc7946#section-3.1.2 -[7]: https://tools.ietf.org/html/rfc7946#section-3.1.2 +[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object -[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object +[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String -[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String +[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number -[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number +[10]: https://tools.ietf.org/html/rfc7946#section-3.2 -[11]: https://tools.ietf.org/html/rfc7946#section-3.2 - -[12]: https://tools.ietf.org/html/rfc7946#section-3.1.6 +[11]: https://tools.ietf.org/html/rfc7946#section-3.1.6 diff --git a/packages/turf-standard-deviational-ellipse/bench.ts b/packages/turf-standard-deviational-ellipse/bench.ts index e46a44c467..6b2cf87fe3 100644 --- a/packages/turf-standard-deviational-ellipse/bench.ts +++ b/packages/turf-standard-deviational-ellipse/bench.ts @@ -1,6 +1,7 @@ +import { BBox } from "geojson"; import { randomPoint } from "@turf/random"; import { standardDeviationalEllipse } from "./index.js"; -import Benchmark from "benchmark"; +import Benchmark, { Event } from "benchmark"; /** * Benchmark Results @@ -10,7 +11,7 @@ import Benchmark from "benchmark"; * turf-standard-deviational-ellipse - 600 points x 574 ops/sec ±1.14% (92 runs sampled) */ const suite = new Benchmark.Suite("turf-standard-deviational-ellipse"); -const properties = { bbox: [-10, -10, 10, 10] }; +const properties: { bbox: BBox } = { bbox: [-10, -10, 10, 10] }; suite .add("turf-standard-deviational-ellipse - 150 points", () => standardDeviationalEllipse(randomPoint(150, properties)) @@ -21,5 +22,5 @@ suite .add("turf-standard-deviational-ellipse - 600 points", () => standardDeviationalEllipse(randomPoint(600, properties)) ) - .on("cycle", (e) => console.log(String(e.target))) + .on("cycle", (e: Event) => console.log(String(e.target))) .run(); diff --git a/packages/turf-standard-deviational-ellipse/index.d.ts b/packages/turf-standard-deviational-ellipse/index.d.ts deleted file mode 100644 index 0f4a02618a..0000000000 --- a/packages/turf-standard-deviational-ellipse/index.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - FeatureCollection, - Feature, - Position, - Polygon, - GeoJsonProperties, - Point, -} from "geojson"; - -/** - * http://turfjs.org/docs/#standarddeviational-ellipse - */ - -declare interface SDEProps { - meanCenterCoordinates: Position; - semiMajorAxis: number; - semiMinorAxis: number; - numberOfFeatures: number; - angle: number; - percentageWithinEllipse: number; -} - -declare interface StandardDeviationalEllipse extends Feature { - properties: { - standardDeviationalEllipse: SDEProps; - [key: string]: any; - }; -} - -declare function standardDeviationalEllipse( - points: FeatureCollection, - options?: { - properties?: GeoJsonProperties; - weight?: string; - steps?: number; - } -): StandardDeviationalEllipse; - -export { SDEProps, StandardDeviationalEllipse, standardDeviationalEllipse }; -export default standardDeviationalEllipse; diff --git a/packages/turf-standard-deviational-ellipse/index.js b/packages/turf-standard-deviational-ellipse/index.ts similarity index 53% rename from packages/turf-standard-deviational-ellipse/index.js rename to packages/turf-standard-deviational-ellipse/index.ts index efa5b8544b..e73f4072e6 100644 --- a/packages/turf-standard-deviational-ellipse/index.js +++ b/packages/turf-standard-deviational-ellipse/index.ts @@ -1,3 +1,11 @@ +import { + FeatureCollection, + Feature, + Position, + Polygon, + GeoJsonProperties, + Point, +} from "geojson"; import { coordAll, featureEach } from "@turf/meta"; import { getCoords } from "@turf/invariant"; import { featureCollection, isObject, isNumber } from "@turf/helpers"; @@ -5,15 +13,31 @@ import { centerMean } from "@turf/center-mean"; import { pointsWithinPolygon } from "@turf/points-within-polygon"; import { ellipse } from "@turf/ellipse"; +declare interface SDEProps { + meanCenterCoordinates: Position; + semiMajorAxis: number; + semiMinorAxis: number; + numberOfFeatures: number; + angle: number; + percentageWithinEllipse: number; +} + +declare interface StandardDeviationalEllipse extends Feature { + properties: { + standardDeviationalEllipse: SDEProps; + [key: string]: any; + } | null; +} + /** - * Takes a {@link FeatureCollection} and returns a standard deviational ellipse, + * Takes a collection of features and returns a standard deviational ellipse, * also known as a “directional distribution.” The standard deviational ellipse * aims to show the direction and the distribution of a dataset by drawing * an ellipse that contains about one standard deviation’s worth (~ 70%) of the * data. * - * This module mirrors the functionality of [Directional Distribution](http://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-statistics-toolbox/directional-distribution.htm) - * in ArcGIS and the [QGIS Standard Deviational Ellipse Plugin](http://arken.nmbu.no/~havatv/gis/qgisplugins/SDEllipse/) + * This module mirrors the functionality of {@link http://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-statistics-toolbox/directional-distribution.htm|Directional Distribution} + * in ArcGIS and the {@link http://arken.nmbu.no/~havatv/gis/qgisplugins/SDEllipse/|QGIS Standard Deviational Ellipse Plugin} * * **Bibliography** * @@ -35,29 +59,36 @@ import { ellipse } from "@turf/ellipse"; * @returns {Feature} an elliptical Polygon that includes approximately 1 SD of the dataset within it. * @example * - * var bbox = [-74, 40.72, -73.98, 40.74]; - * var points = turf.randomPoint(400, {bbox: bbox}); - * var sdEllipse = turf.standardDeviationalEllipse(points); + * const bbox = [-74, 40.72, -73.98, 40.74]; + * const points = turf.randomPoint(400, {bbox: bbox}); + * const sdEllipse = turf.standardDeviationalEllipse(points); * * //addToMap - * var addToMap = [points, sdEllipse]; + * const addToMap = [points, sdEllipse]; * */ -function standardDeviationalEllipse(points, options) { +function standardDeviationalEllipse( + points: FeatureCollection, + options?: { + properties?: GeoJsonProperties; + weight?: string; + steps?: number; + } +): StandardDeviationalEllipse { // Optional params options = options || {}; if (!isObject(options)) throw new Error("options is invalid"); - var steps = options.steps || 64; - var weightTerm = options.weight; - var properties = options.properties || {}; + const steps = options.steps || 64; + const weightTerm = options.weight; + const properties = options.properties || {}; // Validation: if (!isNumber(steps)) throw new Error("steps must be a number"); if (!isObject(properties)) throw new Error("properties must be a number"); // Calculate mean center & number of features: - var numberOfFeatures = coordAll(points).length; - var meanCenter = centerMean(points, { weight: weightTerm }); + const numberOfFeatures = coordAll(points).length; + const meanCenter = centerMean(points, { weight: weightTerm }); // Calculate angle of rotation: // [X, Y] = mean center of all [x, y]. @@ -66,33 +97,35 @@ function standardDeviationalEllipse(points, options) { // B = sqrt(A^2 + 4(sum((x - X)(y - Y))^2)) // C = 2(sum((x - X)(y - Y))) - var xDeviationSquaredSum = 0; - var yDeviationSquaredSum = 0; - var xyDeviationSum = 0; + let xDeviationSquaredSum = 0; + let yDeviationSquaredSum = 0; + let xyDeviationSum = 0; featureEach(points, function (point) { - var weight = point.properties[weightTerm] || 1; - var deviation = getDeviations(getCoords(point), getCoords(meanCenter)); + // weightTerm or point.properties might be undefined, hence this check. + const weight = weightTerm ? point.properties?.[weightTerm] || 1 : 1; + const deviation = getDeviations(getCoords(point), getCoords(meanCenter)); xDeviationSquaredSum += Math.pow(deviation.x, 2) * weight; yDeviationSquaredSum += Math.pow(deviation.y, 2) * weight; xyDeviationSum += deviation.x * deviation.y * weight; }); - var bigA = xDeviationSquaredSum - yDeviationSquaredSum; - var bigB = Math.sqrt(Math.pow(bigA, 2) + 4 * Math.pow(xyDeviationSum, 2)); - var bigC = 2 * xyDeviationSum; - var theta = Math.atan((bigA + bigB) / bigC); - var thetaDeg = (theta * 180) / Math.PI; + const bigA = xDeviationSquaredSum - yDeviationSquaredSum; + const bigB = Math.sqrt(Math.pow(bigA, 2) + 4 * Math.pow(xyDeviationSum, 2)); + const bigC = 2 * xyDeviationSum; + const theta = Math.atan((bigA + bigB) / bigC); + const thetaDeg = (theta * 180) / Math.PI; // Calculate axes: // sigmaX = sqrt((1 / n - 2) * sum((((x - X) * cos(theta)) - ((y - Y) * sin(theta)))^2)) // sigmaY = sqrt((1 / n - 2) * sum((((x - X) * sin(theta)) - ((y - Y) * cos(theta)))^2)) - var sigmaXsum = 0; - var sigmaYsum = 0; - var weightsum = 0; + let sigmaXsum = 0; + let sigmaYsum = 0; + let weightsum = 0; featureEach(points, function (point) { - var weight = point.properties[weightTerm] || 1; - var deviation = getDeviations(getCoords(point), getCoords(meanCenter)); + // weightTerm or point.properties might be undefined, hence this check. + const weight = weightTerm ? point.properties?.[weightTerm] || 1 : 1; + const deviation = getDeviations(getCoords(point), getCoords(meanCenter)); sigmaXsum += Math.pow( deviation.x * Math.cos(theta) - deviation.y * Math.sin(theta), @@ -106,20 +139,20 @@ function standardDeviationalEllipse(points, options) { weightsum += weight; }); - var sigmaX = Math.sqrt((2 * sigmaXsum) / weightsum); - var sigmaY = Math.sqrt((2 * sigmaYsum) / weightsum); + const sigmaX = Math.sqrt((2 * sigmaXsum) / weightsum); + const sigmaY = Math.sqrt((2 * sigmaYsum) / weightsum); - var theEllipse = ellipse(meanCenter, sigmaX, sigmaY, { + const theEllipse: Feature = ellipse(meanCenter, sigmaX, sigmaY, { units: "degrees", angle: thetaDeg, steps: steps, properties: properties, }); - var pointsWithinEllipse = pointsWithinPolygon( + const pointsWithinEllipse = pointsWithinPolygon( points, featureCollection([theEllipse]) ); - var standardDeviationalEllipseProperties = { + const standardDeviationalEllipseProperties = { meanCenterCoordinates: getCoords(meanCenter), semiMajorAxis: sigmaX, semiMinorAxis: sigmaY, @@ -128,26 +161,30 @@ function standardDeviationalEllipse(points, options) { percentageWithinEllipse: (100 * coordAll(pointsWithinEllipse).length) / numberOfFeatures, }; + // Make sure properties object exists. + theEllipse.properties = theEllipse.properties ?? {}; theEllipse.properties.standardDeviationalEllipse = standardDeviationalEllipseProperties; - return theEllipse; + // We have added the StandardDeviationalEllipse specific properties, so + // confirm this to Typescript with a cast. + return theEllipse as StandardDeviationalEllipse; } /** * Get x_i - X and y_i - Y * * @private - * @param {Array} coordinates Array of [x_i, y_i] - * @param {Array} center Array of [X, Y] + * @param {Position} coordinates Array of [x_i, y_i] + * @param {Position} center Array of [X, Y] * @returns {Object} { x: n, y: m } */ -function getDeviations(coordinates, center) { +function getDeviations(coordinates: Position, center: Position) { return { x: coordinates[0] - center[0], y: coordinates[1] - center[1], }; } -export { standardDeviationalEllipse }; +export { standardDeviationalEllipse, SDEProps, StandardDeviationalEllipse }; export default standardDeviationalEllipse; diff --git a/packages/turf-standard-deviational-ellipse/package.json b/packages/turf-standard-deviational-ellipse/package.json index 7b483a864a..2c072bb5d4 100644 --- a/packages/turf-standard-deviational-ellipse/package.json +++ b/packages/turf-standard-deviational-ellipse/package.json @@ -65,6 +65,7 @@ "tape": "^5.7.2", "tsup": "^8.0.1", "tsx": "^4.6.2", + "typescript": "^5.2.2", "write-json-file": "^5.0.0" }, "dependencies": { @@ -73,6 +74,8 @@ "@turf/helpers": "workspace:^", "@turf/invariant": "workspace:^", "@turf/meta": "workspace:^", - "@turf/points-within-polygon": "workspace:^" + "@turf/points-within-polygon": "workspace:^", + "@types/geojson": "7946.0.8", + "tslib": "^2.6.2" } } diff --git a/packages/turf-standard-deviational-ellipse/test.ts b/packages/turf-standard-deviational-ellipse/test.ts index 84b5c6f1c6..c0073590b3 100644 --- a/packages/turf-standard-deviational-ellipse/test.ts +++ b/packages/turf-standard-deviational-ellipse/test.ts @@ -1,3 +1,4 @@ +import { Feature, FeatureCollection, Point } from "geojson"; import test from "tape"; import { glob } from "glob"; import path from "path"; @@ -15,7 +16,10 @@ test("turf-standard-deviational-ellipse", (t) => { .forEach((filepath) => { // Define params const { name } = path.parse(filepath); - const geojson = loadJsonFileSync(filepath); + const geojson = loadJsonFileSync(filepath) as FeatureCollection & { + options: any; + esriEllipse: any; + }; // Non-standard FeatureCollection options used for testing. const options = geojson.options; // Optional: ESRI Polygon in GeoJSON test/in to compare results const esriEllipse = geojson.esriEllipse; @@ -38,7 +42,12 @@ test("turf-standard-deviational-ellipse", (t) => { t.end(); }); -function colorize(feature, stroke = "#0A0", fill = "#FFF", opacity = 0) { +function colorize( + feature: Feature, + stroke = "#0A0", + fill = "#FFF", + opacity = 0 +) { const properties = { fill, stroke, @@ -46,6 +55,8 @@ function colorize(feature, stroke = "#0A0", fill = "#FFF", opacity = 0) { "stroke-opacity": 1, "fill-opacity": opacity, }; + // Make sure feature properties is defined. + feature.properties = feature.properties ?? {}; Object.assign(feature.properties, properties); return feature; } diff --git a/packages/turf-standard-deviational-ellipse/types.ts b/packages/turf-standard-deviational-ellipse/types.ts index 8f06c60a36..3b7760de92 100644 --- a/packages/turf-standard-deviational-ellipse/types.ts +++ b/packages/turf-standard-deviational-ellipse/types.ts @@ -8,4 +8,7 @@ const pts = points([ const stdEllipse = standardDeviationalEllipse(pts); // Access custom properties -stdEllipse.properties.standardDeviationalEllipse.meanCenterCoordinates; +// It's correct to use optional chaining here ?. as even though the function +// *always* adds properties, that does not change the overall optional nature of +// the containing properties attribute. +stdEllipse.properties?.standardDeviationalEllipse.meanCenterCoordinates; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df6c801033..2e12d33568 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5594,6 +5594,12 @@ importers: '@turf/points-within-polygon': specifier: workspace:^ version: link:../turf-points-within-polygon + '@types/geojson': + specifier: 7946.0.8 + version: 7946.0.8 + tslib: + specifier: ^2.6.2 + version: 2.6.2 devDependencies: '@turf/random': specifier: workspace:^ @@ -5625,6 +5631,9 @@ importers: tsx: specifier: ^4.6.2 version: 4.6.2 + typescript: + specifier: ^5.2.2 + version: 5.3.3 write-json-file: specifier: ^5.0.0 version: 5.0.0