Skip to content
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

Converted turf-standard-deviational-ellipse to Typescript #2649

Merged
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
54 changes: 26 additions & 28 deletions packages/turf-standard-deviational-ellipse/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<!-- This file is automatically generated. Please don't edit it directly. If you find an error, edit the source file of the module in question (likely index.js or index.ts), and re-run "yarn docs" from the root of the turf project. -->

Expand Down
7 changes: 4 additions & 3 deletions packages/turf-standard-deviational-ellipse/bench.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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))
Expand All @@ -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();
40 changes: 0 additions & 40 deletions packages/turf-standard-deviational-ellipse/index.d.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
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";
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<Polygon> {
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**
*
Expand All @@ -35,29 +59,36 @@ import { ellipse } from "@turf/ellipse";
* @returns {Feature<Polygon>} 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<Point>,
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].
Expand All @@ -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),
Expand All @@ -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<Polygon> = 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,
Expand All @@ -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;
5 changes: 4 additions & 1 deletion packages/turf-standard-deviational-ellipse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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"
}
}
Loading
Loading