Source: geographiclib-geodesic/src/PolygonArea.js

  1. /*
  2. * PolygonArea.js
  3. * Transcription of PolygonArea.[ch]pp into JavaScript.
  4. *
  5. * See the documentation for the C++ class. The conversion is a literal
  6. * conversion from C++.
  7. *
  8. * The algorithms are derived in
  9. *
  10. * Charles F. F. Karney,
  11. * Algorithms for geodesics, J. Geodesy 87, 43-55 (2013);
  12. * https://doi.org/10.1007/s00190-012-0578-z
  13. * Addenda: https://geographiclib.sourceforge.io/geod-addenda.html
  14. *
  15. * Copyright (c) Charles Karney (2011-2022) <karney@alum.mit.edu> and licensed
  16. * under the MIT/X11 License. For more information, see
  17. * https://geographiclib.sourceforge.io/
  18. */
  19. // Load AFTER geodesic/Math.js and geodesic/Geodesic.js
  20. (function(
  21. /**
  22. * @exports geodesic/PolygonArea
  23. * @description Compute the area of geodesic polygons via the
  24. * {@link module:geodesic/PolygonArea.PolygonArea PolygonArea}
  25. * class.
  26. */
  27. p, g, m, a) {
  28. "use strict";
  29. var transit, transitdirect, AreaReduceA, AreaReduceB;
  30. transit = function(lon1, lon2) {
  31. // Return 1 or -1 if crossing prime meridian in east or west direction.
  32. // Otherwise return zero. longitude = +/-0 considered to be positive.
  33. // This is (should be?) compatible with transitdirect which computes
  34. // exactly the parity of
  35. // int(floor((lon1 + lon12) / 360)) - int(floor(lon1 / 360)))
  36. var lon12 = m.AngDiff(lon1, lon2).d;
  37. lon1 = m.AngNormalize(lon1);
  38. lon2 = m.AngNormalize(lon2);
  39. // edge case lon1 = 180, lon2 = 360->0, lon12 = 180 to give 1
  40. return lon12 > 0 &&
  41. // lon12 > 0 && lon1 > 0 && lon2 == 0 implies lon1 == 180
  42. ((lon1 < 0 && lon2 >= 0) || (lon1 > 0 && lon2 === 0)) ? 1 :
  43. // non edge case lon1 = -180, lon2 = -360->-0, lon12 = -180
  44. (lon12 < 0 && lon1 >= 0 && lon2 < 0 ? -1 : 0);
  45. };
  46. // an alternate version of transit to deal with longitudes in the direct
  47. // problem.
  48. transitdirect = function(lon1, lon2) {
  49. // We want to compute exactly
  50. // int(floor(lon2 / 360)) - int(floor(lon1 / 360))
  51. lon1 = lon1 % 720; lon2 = lon2 % 720;
  52. return ((0 <= lon2 && lon2 < 360) || lon2 < -360 ? 0 : 1) -
  53. ((0 <= lon1 && lon1 < 360) || lon1 < -360 ? 0 : 1 );
  54. };
  55. // Reduce Accumulator area
  56. AreaReduceA = function(area, area0, crossings, reverse, sign) {
  57. area.Remainder(area0);
  58. if (crossings & 1)
  59. area.Add( (area.Sum() < 0 ? 1 : -1) * area0/2 );
  60. // area is with the clockwise sense. If !reverse convert to
  61. // counter-clockwise convention.
  62. if (!reverse)
  63. area.Negate();
  64. // If sign put area in (-area0/2, area0/2], else put area in [0, area0)
  65. if (sign) {
  66. if (area.Sum() > area0/2)
  67. area.Add( -area0 );
  68. else if (area.Sum() <= -area0/2)
  69. area.Add( +area0 );
  70. } else {
  71. if (area.Sum() >= area0)
  72. area.Add( -area0 );
  73. else if (area.Sum() < 0)
  74. area.Add( +area0 );
  75. }
  76. return 0 + area.Sum();
  77. };
  78. // Reduce double area
  79. AreaReduceB = function(area, area0, crossings, reverse, sign) {
  80. area = m.remainder(area, area0);
  81. if (crossings & 1)
  82. area += (area < 0 ? 1 : -1) * area0/2;
  83. // area is with the clockwise sense. If !reverse convert to
  84. // counter-clockwise convention.
  85. if (!reverse)
  86. area *= -1;
  87. // If sign put area in (-area0/2, area0/2], else put area in [0, area0)
  88. if (sign) {
  89. if (area > area0/2)
  90. area -= area0;
  91. else if (area <= -area0/2)
  92. area += area0;
  93. } else {
  94. if (area >= area0)
  95. area -= area0;
  96. else if (area < 0)
  97. area += area0;
  98. }
  99. return 0 + area;
  100. };
  101. /**
  102. * @class
  103. * @property {number} a the equatorial radius (meters).
  104. * @property {number} f the flattening.
  105. * @property {bool} polyline whether the PolygonArea object describes a
  106. * polyline or a polygon.
  107. * @property {number} num the number of vertices so far.
  108. * @property {number} lat the current latitude (degrees).
  109. * @property {number} lon the current longitude (degrees).
  110. * @summary Initialize a PolygonArea object.
  111. * @classdesc Computes the area and perimeter of a geodesic polygon.
  112. * This object is usually instantiated by
  113. * {@link module:geodesic/Geodesic.Geodesic#Polygon Geodesic.Polygon}.
  114. * @param {object} geod a {@link module:geodesic/Geodesic.Geodesic
  115. * Geodesic} object.
  116. * @param {bool} [polyline = false] if true the new PolygonArea object
  117. * describes a polyline instead of a polygon.
  118. */
  119. p.PolygonArea = function(geod, polyline) {
  120. this._geod = geod;
  121. this.a = this._geod.a;
  122. this.f = this._geod.f;
  123. this._area0 = 4 * Math.PI * geod._c2;
  124. this.polyline = !polyline ? false : polyline;
  125. this._mask = g.LATITUDE | g.LONGITUDE | g.DISTANCE |
  126. (this.polyline ? g.NONE : g.AREA | g.LONG_UNROLL);
  127. if (!this.polyline)
  128. this._areasum = new a.Accumulator(0);
  129. this._perimetersum = new a.Accumulator(0);
  130. this.Clear();
  131. };
  132. /**
  133. * @summary Clear the PolygonArea object, setting the number of vertices to
  134. * 0.
  135. */
  136. p.PolygonArea.prototype.Clear = function() {
  137. this.num = 0;
  138. this._crossings = 0;
  139. if (!this.polyline)
  140. this._areasum.Set(0);
  141. this._perimetersum.Set(0);
  142. this._lat0 = this._lon0 = this.lat = this.lon = NaN;
  143. };
  144. /**
  145. * @summary Add the next vertex to the polygon.
  146. * @param {number} lat the latitude of the point (degrees).
  147. * @param {number} lon the longitude of the point (degrees).
  148. * @description This adds an edge from the current vertex to the new vertex.
  149. */
  150. p.PolygonArea.prototype.AddPoint = function(lat, lon) {
  151. var t;
  152. if (this.num === 0) {
  153. this._lat0 = this.lat = lat;
  154. this._lon0 = this.lon = lon;
  155. } else {
  156. t = this._geod.Inverse(this.lat, this.lon, lat, lon, this._mask);
  157. this._perimetersum.Add(t.s12);
  158. if (!this.polyline) {
  159. this._areasum.Add(t.S12);
  160. this._crossings += transit(this.lon, lon);
  161. }
  162. this.lat = lat;
  163. this.lon = lon;
  164. }
  165. ++this.num;
  166. };
  167. /**
  168. * @summary Add the next edge to the polygon.
  169. * @param {number} azi the azimuth at the current the point (degrees).
  170. * @param {number} s the length of the edge (meters).
  171. * @description This specifies the new vertex in terms of the edge from the
  172. * current vertex.
  173. */
  174. p.PolygonArea.prototype.AddEdge = function(azi, s) {
  175. var t;
  176. if (this.num) {
  177. t = this._geod.Direct(this.lat, this.lon, azi, s, this._mask);
  178. this._perimetersum.Add(s);
  179. if (!this.polyline) {
  180. this._areasum.Add(t.S12);
  181. this._crossings += transitdirect(this.lon, t.lon2);
  182. }
  183. this.lat = t.lat2;
  184. this.lon = t.lon2;
  185. }
  186. ++this.num;
  187. };
  188. /**
  189. * @summary Compute the perimeter and area of the polygon.
  190. * @param {bool} reverse if true then clockwise (instead of
  191. * counter-clockwise) traversal counts as a positive area.
  192. * @param {bool} sign if true then return a signed result for the area if the
  193. * polygon is traversed in the "wrong" direction instead of returning the
  194. * area for the rest of the earth.
  195. * @return {object} r where r.number is the number of vertices, r.perimeter
  196. * is the perimeter (meters), and r.area (only returned if polyline is
  197. * false) is the area (meters<sup>2</sup>).
  198. * @description Arbitrarily complex polygons are allowed. In the case of
  199. * self-intersecting polygons the area is accumulated "algebraically",
  200. * e.g., the areas of the 2 loops in a figure-8 polygon will partially
  201. * cancel. If the object is a polygon (and not a polyline), the perimeter
  202. * includes the length of a final edge connecting the current point to the
  203. * initial point. If the object is a polyline, then area is nan. More
  204. * points can be added to the polygon after this call.
  205. */
  206. p.PolygonArea.prototype.Compute = function(reverse, sign) {
  207. var vals = {number: this.num}, t, tempsum;
  208. if (this.num < 2) {
  209. vals.perimeter = 0;
  210. if (!this.polyline)
  211. vals.area = 0;
  212. return vals;
  213. }
  214. if (this.polyline) {
  215. vals.perimeter = this._perimetersum.Sum();
  216. return vals;
  217. }
  218. t = this._geod.Inverse(this.lat, this.lon, this._lat0, this._lon0,
  219. this._mask);
  220. vals.perimeter = this._perimetersum.Sum(t.s12);
  221. tempsum = new a.Accumulator(this._areasum);
  222. tempsum.Add(t.S12);
  223. vals.area = AreaReduceA(tempsum, this._area0,
  224. this._crossings + transit(this.lon, this._lon0),
  225. reverse, sign);
  226. return vals;
  227. };
  228. /**
  229. * @summary Compute the perimeter and area of the polygon with a tentative
  230. * new vertex.
  231. * @param {number} lat the latitude of the point (degrees).
  232. * @param {number} lon the longitude of the point (degrees).
  233. * @param {bool} reverse if true then clockwise (instead of
  234. * counter-clockwise) traversal counts as a positive area.
  235. * @param {bool} sign if true then return a signed result for the area if the
  236. * polygon is traversed in the "wrong" direction instead of returning the
  237. * area for the rest of the earth.
  238. * @return {object} r where r.number is the number of vertices, r.perimeter
  239. * is the perimeter (meters), and r.area (only returned if polyline is
  240. * false) is the area (meters<sup>2</sup>).
  241. * @description A new vertex is *not* added to the polygon.
  242. */
  243. p.PolygonArea.prototype.TestPoint = function(lat, lon, reverse, sign) {
  244. var vals = {number: this.num + 1}, t, tempsum, crossings, i;
  245. if (this.num === 0) {
  246. vals.perimeter = 0;
  247. if (!this.polyline)
  248. vals.area = 0;
  249. return vals;
  250. }
  251. vals.perimeter = this._perimetersum.Sum();
  252. tempsum = this.polyline ? 0 : this._areasum.Sum();
  253. crossings = this._crossings;
  254. for (i = 0; i < (this.polyline ? 1 : 2); ++i) {
  255. t = this._geod.Inverse(
  256. i === 0 ? this.lat : lat, i === 0 ? this.lon : lon,
  257. i !== 0 ? this._lat0 : lat, i !== 0 ? this._lon0 : lon,
  258. this._mask);
  259. vals.perimeter += t.s12;
  260. if (!this.polyline) {
  261. tempsum += t.S12;
  262. crossings += transit(i === 0 ? this.lon : lon,
  263. i !== 0 ? this._lon0 : lon);
  264. }
  265. }
  266. if (this.polyline)
  267. return vals;
  268. vals.area = AreaReduceB(tempsum, this._area0, crossings, reverse, sign);
  269. return vals;
  270. };
  271. /**
  272. * @summary Compute the perimeter and area of the polygon with a tentative
  273. * new edge.
  274. * @param {number} azi the azimuth of the edge (degrees).
  275. * @param {number} s the length of the edge (meters).
  276. * @param {bool} reverse if true then clockwise (instead of
  277. * counter-clockwise) traversal counts as a positive area.
  278. * @param {bool} sign if true then return a signed result for the area if the
  279. * polygon is traversed in the "wrong" direction instead of returning the
  280. * area for the rest of the earth.
  281. * @return {object} r where r.number is the number of vertices, r.perimeter
  282. * is the perimeter (meters), and r.area (only returned if polyline is
  283. * false) is the area (meters<sup>2</sup>).
  284. * @description A new vertex is *not* added to the polygon.
  285. */
  286. p.PolygonArea.prototype.TestEdge = function(azi, s, reverse, sign) {
  287. var vals = {number: this.num ? this.num + 1 : 0}, t, tempsum, crossings;
  288. if (this.num === 0)
  289. return vals;
  290. vals.perimeter = this._perimetersum.Sum() + s;
  291. if (this.polyline)
  292. return vals;
  293. tempsum = this._areasum.Sum();
  294. crossings = this._crossings;
  295. t = this._geod.Direct(this.lat, this.lon, azi, s, this._mask);
  296. tempsum += t.S12;
  297. crossings += transitdirect(this.lon, t.lon2);
  298. crossings += transit(t.lon2, this._lon0);
  299. t = this._geod.Inverse(t.lat2, t.lon2, this._lat0, this._lon0, this._mask);
  300. vals.perimeter += t.s12;
  301. tempsum += t.S12;
  302. vals.area = AreaReduceB(tempsum, this._area0, crossings, reverse, sign);
  303. return vals;
  304. };
  305. })(geodesic.PolygonArea, geodesic.Geodesic,
  306. geodesic.Math, geodesic.Accumulator);