index.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import { coordAll, featureEach } from '@turf/meta';
  2. import { getCoords } from '@turf/invariant';
  3. import { isObject, isNumber, featureCollection } from '@turf/helpers';
  4. import centerMean from '@turf/center-mean';
  5. import pointsWithinPolygon from '@turf/points-within-polygon';
  6. import ellipse from '@turf/ellipse';
  7. /**
  8. * Takes a {@link FeatureCollection} and returns a standard deviational ellipse,
  9. * also known as a “directional distribution.” The standard deviational ellipse
  10. * aims to show the direction and the distribution of a dataset by drawing
  11. * an ellipse that contains about one standard deviation’s worth (~ 70%) of the
  12. * data.
  13. *
  14. * This module mirrors the functionality of [Directional Distribution](http://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-statistics-toolbox/directional-distribution.htm)
  15. * in ArcGIS and the [QGIS Standard Deviational Ellipse Plugin](http://arken.nmbu.no/~havatv/gis/qgisplugins/SDEllipse/)
  16. *
  17. * **Bibliography**
  18. *
  19. * • Robert S. Yuill, “The Standard Deviational Ellipse; An Updated Tool for
  20. * Spatial Description,” _Geografiska Annaler_ 53, no. 1 (1971): 28–39,
  21. * doi:{@link https://doi.org/10.2307/490885|10.2307/490885}.
  22. *
  23. * • Paul Hanly Furfey, “A Note on Lefever’s “Standard Deviational Ellipse,”
  24. * _American Journal of Sociology_ 33, no. 1 (1927): 94—98,
  25. * doi:{@link https://doi.org/10.1086/214336|10.1086/214336}.
  26. *
  27. *
  28. * @name standardDeviationalEllipse
  29. * @param {FeatureCollection<Point>} points GeoJSON points
  30. * @param {Object} [options={}] Optional parameters
  31. * @param {string} [options.weight] the property name used to weight the center
  32. * @param {number} [options.steps=64] number of steps for the polygon
  33. * @param {Object} [options.properties={}] properties to pass to the resulting ellipse
  34. * @returns {Feature<Polygon>} an elliptical Polygon that includes approximately 1 SD of the dataset within it.
  35. * @example
  36. *
  37. * var bbox = [-74, 40.72, -73.98, 40.74];
  38. * var points = turf.randomPoint(400, {bbox: bbox});
  39. * var sdEllipse = turf.standardDeviationalEllipse(points);
  40. *
  41. * //addToMap
  42. * var addToMap = [points, sdEllipse];
  43. *
  44. */
  45. function standardDeviationalEllipse(points, options) {
  46. // Optional params
  47. options = options || {};
  48. if (!isObject(options)) throw new Error("options is invalid");
  49. var steps = options.steps || 64;
  50. var weightTerm = options.weight;
  51. var properties = options.properties || {};
  52. // Validation:
  53. if (!isNumber(steps)) throw new Error("steps must be a number");
  54. if (!isObject(properties)) throw new Error("properties must be a number");
  55. // Calculate mean center & number of features:
  56. var numberOfFeatures = coordAll(points).length;
  57. var meanCenter = centerMean(points, { weight: weightTerm });
  58. // Calculate angle of rotation:
  59. // [X, Y] = mean center of all [x, y].
  60. // theta = arctan( (A + B) / C )
  61. // A = sum((x - X)^2) - sum((y - Y)^2)
  62. // B = sqrt(A^2 + 4(sum((x - X)(y - Y))^2))
  63. // C = 2(sum((x - X)(y - Y)))
  64. var xDeviationSquaredSum = 0;
  65. var yDeviationSquaredSum = 0;
  66. var xyDeviationSum = 0;
  67. featureEach(points, function (point) {
  68. var weight = point.properties[weightTerm] || 1;
  69. var deviation = getDeviations(getCoords(point), getCoords(meanCenter));
  70. xDeviationSquaredSum += Math.pow(deviation.x, 2) * weight;
  71. yDeviationSquaredSum += Math.pow(deviation.y, 2) * weight;
  72. xyDeviationSum += deviation.x * deviation.y * weight;
  73. });
  74. var bigA = xDeviationSquaredSum - yDeviationSquaredSum;
  75. var bigB = Math.sqrt(Math.pow(bigA, 2) + 4 * Math.pow(xyDeviationSum, 2));
  76. var bigC = 2 * xyDeviationSum;
  77. var theta = Math.atan((bigA + bigB) / bigC);
  78. var thetaDeg = (theta * 180) / Math.PI;
  79. // Calculate axes:
  80. // sigmaX = sqrt((1 / n - 2) * sum((((x - X) * cos(theta)) - ((y - Y) * sin(theta)))^2))
  81. // sigmaY = sqrt((1 / n - 2) * sum((((x - X) * sin(theta)) - ((y - Y) * cos(theta)))^2))
  82. var sigmaXsum = 0;
  83. var sigmaYsum = 0;
  84. var weightsum = 0;
  85. featureEach(points, function (point) {
  86. var weight = point.properties[weightTerm] || 1;
  87. var deviation = getDeviations(getCoords(point), getCoords(meanCenter));
  88. sigmaXsum +=
  89. Math.pow(
  90. deviation.x * Math.cos(theta) - deviation.y * Math.sin(theta),
  91. 2
  92. ) * weight;
  93. sigmaYsum +=
  94. Math.pow(
  95. deviation.x * Math.sin(theta) + deviation.y * Math.cos(theta),
  96. 2
  97. ) * weight;
  98. weightsum += weight;
  99. });
  100. var sigmaX = Math.sqrt((2 * sigmaXsum) / weightsum);
  101. var sigmaY = Math.sqrt((2 * sigmaYsum) / weightsum);
  102. var theEllipse = ellipse(meanCenter, sigmaX, sigmaY, {
  103. units: "degrees",
  104. angle: thetaDeg,
  105. steps: steps,
  106. properties: properties,
  107. });
  108. var pointsWithinEllipse = pointsWithinPolygon(
  109. points,
  110. featureCollection([theEllipse])
  111. );
  112. var standardDeviationalEllipseProperties = {
  113. meanCenterCoordinates: getCoords(meanCenter),
  114. semiMajorAxis: sigmaX,
  115. semiMinorAxis: sigmaY,
  116. numberOfFeatures: numberOfFeatures,
  117. angle: thetaDeg,
  118. percentageWithinEllipse:
  119. (100 * coordAll(pointsWithinEllipse).length) / numberOfFeatures,
  120. };
  121. theEllipse.properties.standardDeviationalEllipse = standardDeviationalEllipseProperties;
  122. return theEllipse;
  123. }
  124. /**
  125. * Get x_i - X and y_i - Y
  126. *
  127. * @private
  128. * @param {Array} coordinates Array of [x_i, y_i]
  129. * @param {Array} center Array of [X, Y]
  130. * @returns {Object} { x: n, y: m }
  131. */
  132. function getDeviations(coordinates, center) {
  133. return {
  134. x: coordinates[0] - center[0],
  135. y: coordinates[1] - center[1],
  136. };
  137. }
  138. export default standardDeviationalEllipse;