UNPKG

@spatial/standard-deviational-ellipse

Version:

turf standard-deviational-ellipse module

136 lines (121 loc) 5.64 kB
import { coordAll, featureEach } from '@spatial/meta'; import { getCoords } from '@spatial/invariant'; import { featureCollection, isObject, isNumber } from '@spatial/helpers'; import centerMean from '@spatial/center-mean'; import pointsWithinPolygon from '@spatial/points-within-polygon'; import ellipse from '@spatial/ellipse'; /** * Takes a {@link FeatureCollection} 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/) * * **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}. * * • 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}. * * * @name standardDeviationalEllipse * @param {FeatureCollection<Point>} points GeoJSON points * @param {Object} [options={}] Optional parameters * @param {string} [options.weight] the property name used to weight the center * @param {number} [options.steps=64] number of steps for the polygon * @param {Object} [options.properties={}] properties to pass to the resulting 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); * * //addToMap * var addToMap = [points, sdEllipse]; * */ function standardDeviationalEllipse(points, options) { // Optional params options = options || {}; if (!isObject(options)) throw new Error('options is invalid'); 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: const numberOfFeatures = coordAll(points).length; const meanCenter = centerMean(points, {weight: weightTerm}); // Calculate angle of rotation: // [X, Y] = mean center of all [x, y]. // theta = arctan( (A + B) / C ) // A = sum((x - X)^2) - sum((y - Y)^2) // B = sqrt(A^2 + 4(sum((x - X)(y - Y))^2)) // C = 2(sum((x - X)(y - Y))) let xDeviationSquaredSum = 0; let yDeviationSquaredSum = 0; let xyDeviationSum = 0; featureEach(points, (point) => { const weight = point.properties[weightTerm] || 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; }); 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)) let sigmaXsum = 0; let sigmaYsum = 0; let weightsum = 0; featureEach(points, (point) => { const weight = point.properties[weightTerm] || 1; const deviation = getDeviations(getCoords(point), getCoords(meanCenter)); sigmaXsum += Math.pow((deviation.x * Math.cos(theta)) - (deviation.y * Math.sin(theta)), 2) * weight; sigmaYsum += Math.pow((deviation.x * Math.sin(theta)) + (deviation.y * Math.cos(theta)), 2) * weight; weightsum += weight; }); const sigmaX = Math.sqrt(2 * sigmaXsum / weightsum); const sigmaY = Math.sqrt(2 * sigmaYsum / weightsum); const theEllipse = ellipse(meanCenter, sigmaX, sigmaY, {units: 'degrees', angle: thetaDeg, steps, properties}); const pointsWithinEllipse = pointsWithinPolygon(points, featureCollection([theEllipse])); const standardDeviationalEllipseProperties = { meanCenterCoordinates: getCoords(meanCenter), semiMajorAxis: sigmaX, semiMinorAxis: sigmaY, numberOfFeatures, angle: thetaDeg, percentageWithinEllipse: 100 * coordAll(pointsWithinEllipse).length / numberOfFeatures }; theEllipse.properties.standardDeviationalEllipse = standardDeviationalEllipseProperties; return theEllipse; } /** * 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] * @returns {Object} { x: n, y: m } */ function getDeviations(coordinates, center) { return { x: coordinates[0] - center[0], y: coordinates[1] - center[1] }; } export default standardDeviationalEllipse;