Source: BoundingBox.js

  1. 'use strict'
  2. var GeoJSONBounds = require('geojson-bounds')
  3. const haversine = require('haversine')
  4. /* global L:false */
  5. /**
  6. * create bounding box from input
  7. * @class
  8. * @param {object|Leaflet.latLngBounds|GeoJSON} bounds Input boundary. Can be an object with { minlat, minlon, maxlat, maxlon } or { lat, lon } or { lat, lng } or [ N (lat), N (lon) ] a GeoJSON object or a Leaflet object (latLng or latLngBounds). The boundary will automatically be wrapped at longitude -180 / 180.
  9. * @example
  10. * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
  11. */
  12. function BoundingBox (bounds) {
  13. var k
  14. if (bounds === null || typeof bounds === 'undefined') {
  15. this.minlat = -90
  16. this.minlon = -180
  17. this.maxlat = +90
  18. this.maxlon = +180
  19. return
  20. }
  21. // Leaflet.latLngBounds detected!
  22. if (typeof bounds.getSouthWest === 'function') {
  23. var sw = bounds.getSouthWest().wrap()
  24. var ne = bounds.getNorthEast().wrap()
  25. bounds = {
  26. minlat: sw.lat,
  27. minlon: sw.lng,
  28. maxlat: ne.lat,
  29. maxlon: ne.lng
  30. }
  31. }
  32. // GeoJSON detected
  33. if (bounds.type === 'Feature') {
  34. let boxes
  35. if (bounds.geometry.type === 'GeometryCollection') {
  36. boxes = bounds.geometry.geometries.map(
  37. geometry => {
  38. let b = new BoundingBox({ type: 'Feature', geometry })
  39. return [ b.minlon, b.minlat, b.maxlon, b.maxlat ]
  40. }
  41. )
  42. } else if ([ 'MultiPoint', 'MultiPolygon', 'MultiLineString' ].includes(bounds.geometry.type)) {
  43. boxes = bounds.geometry.coordinates.map(
  44. geom => GeoJSONBounds.extent({ type: 'Feature', geometry: { type: bounds.geometry.type.substr(5), coordinates: geom } })
  45. )
  46. } else {
  47. boxes = [ GeoJSONBounds.extent(bounds) ]
  48. }
  49. let b = boxes.shift()
  50. this.minlat = b[1]
  51. this.minlon = b[0]
  52. this.maxlat = b[3]
  53. this.maxlon = b[2]
  54. boxes.forEach(b => this.extend({
  55. minlat: b[1],
  56. minlon: b[0],
  57. maxlat: b[3],
  58. maxlon: b[2]
  59. }))
  60. this._wrap()
  61. return
  62. }
  63. if ('bounds' in bounds) {
  64. bounds = bounds.bounds
  65. }
  66. if (bounds.lat) {
  67. this.minlat = bounds.lat
  68. this.maxlat = bounds.lat
  69. }
  70. if (bounds.lon) {
  71. this.minlon = bounds.lon
  72. this.maxlon = bounds.lon
  73. }
  74. if (Array.isArray(bounds)) {
  75. this.minlat = bounds[0]
  76. this.maxlat = bounds[0]
  77. this.minlon = bounds[1]
  78. this.maxlon = bounds[1]
  79. }
  80. // e.g. L.latLng object
  81. if (bounds.lng) {
  82. this.minlon = bounds.lng
  83. this.maxlon = bounds.lng
  84. }
  85. var props = ['minlon', 'minlat', 'maxlon', 'maxlat']
  86. for (var i = 0; i < props.length; i++) {
  87. k = props[i]
  88. if (k in bounds) {
  89. this[k] = bounds[k]
  90. }
  91. }
  92. this._wrap()
  93. }
  94. BoundingBox.prototype.wrapMaxLon = function () {
  95. return (this.minlon > this.maxlon) ? this.maxlon + 360 : this.maxlon
  96. }
  97. BoundingBox.prototype.wrapMinLon = function () {
  98. return (this.minlon > this.maxlon) ? this.minlon - 360 : this.minlon
  99. }
  100. BoundingBox.prototype._wrap = function () {
  101. if (this.minlon < -180 || this.minlon > 180) {
  102. this.minlon = (this.minlon + 180) % 360 - 180
  103. }
  104. if (this.maxlon < -180 || this.maxlon > 180) {
  105. this.maxlon = (this.maxlon + 180) % 360 - 180
  106. }
  107. return this
  108. }
  109. /**
  110. * Checks whether the other bounding box intersects (shares any portion of space) the current object.
  111. * @param {BoundingBox} other Other boundingbox to check for
  112. * @return {boolean} true if the bounding boxes intersect
  113. * @example
  114. * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
  115. * var bbox2 = new BoundingBox({ lat: 48.5, lon: 16.267 })
  116. * console.log(bbox.intersects(bbox2)) // true
  117. */
  118. BoundingBox.prototype.intersects = function (other) {
  119. if (!(other instanceof BoundingBox)) {
  120. other = new BoundingBox(other)
  121. }
  122. if (other.maxlat < this.minlat) {
  123. return false
  124. }
  125. if (other.minlat > this.maxlat) {
  126. return false
  127. }
  128. if (other.wrapMaxLon() < this.wrapMinLon()) {
  129. return false
  130. }
  131. if (other.wrapMinLon() > this.wrapMaxLon()) {
  132. return false
  133. }
  134. return true
  135. }
  136. /**
  137. * Checks whether the current object is fully within the other bounding box.
  138. * @param {BoundingBox} other Other boundingbox to check for
  139. * @return {boolean} true if the bounding boxes is within other
  140. * @example
  141. * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
  142. * var bbox2 = new BoundingBox({ lat: 48.5, lon: 16.267 })
  143. * console.log(bbox2.within(bbox)) // true
  144. */
  145. BoundingBox.prototype.within = function (other) {
  146. if (!(other instanceof BoundingBox)) {
  147. other = new BoundingBox(other)
  148. }
  149. if (other.maxlat < this.maxlat) {
  150. return false
  151. }
  152. if (other.minlat > this.minlat) {
  153. return false
  154. }
  155. if (other.wrapMaxLon() < this.wrapMaxLon()) {
  156. return false
  157. }
  158. if (other.wrapMinLon() > this.wrapMinLon()) {
  159. return false
  160. }
  161. return true
  162. }
  163. BoundingBox.prototype.toTile = function () {
  164. return new BoundingBox({
  165. minlat: Math.floor(this.minlat * 10) / 10,
  166. minlon: Math.floor(this.minlon * 10) / 10,
  167. maxlat: Math.ceil(this.maxlat * 10) / 10,
  168. maxlon: Math.ceil(this.maxlon * 10) / 10
  169. })
  170. }
  171. /**
  172. * return the bounding box as lon-lat string, e.g. '179.5,55,-179.5,56'
  173. * @return {string}
  174. * @example
  175. * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
  176. * console.log(bbox.toLonLatString()) // '16.23,48.123,16.367,49.012'
  177. */
  178. BoundingBox.prototype.toLonLatString = function () {
  179. return this.minlon + ',' +
  180. this.minlat + ',' +
  181. this.maxlon + ',' +
  182. this.maxlat
  183. }
  184. /**
  185. * return the bounding box as lon-lat string, e.g. '179.5,55,-179.5,56'. Useful for sending requests to web services that return geo data.
  186. * @return {string}
  187. * @example
  188. * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
  189. * console.log(bbox.toBBoxString()) // '16.23,48.123,16.367,49.012'
  190. */
  191. BoundingBox.prototype.toBBoxString = BoundingBox.prototype.toLonLatString
  192. /**
  193. * return the bounding box as lon-lat string, e.g. '55,179.5,56,-179.5'. Useful e.g. for Overpass API requests.
  194. * @return {string}
  195. * @example
  196. * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
  197. * console.log(bbox.toLatLonString()) // '48.123,16.23,49.012,16.367'
  198. */
  199. BoundingBox.prototype.toLatLonString = function () {
  200. return this.minlat + ',' +
  201. this.minlon + ',' +
  202. this.maxlat + ',' +
  203. this.maxlon
  204. }
  205. /**
  206. * return the diagonal length (length of hypothenuse).
  207. * @return {number}
  208. * @example
  209. * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
  210. * console.log(bbox.diagonalLength()) // 0.8994943023721748
  211. */
  212. BoundingBox.prototype.diagonalLength = function () {
  213. var dlat = this.maxlat - this.minlat
  214. var dlon = this.wrapMaxLon() - this.minlon
  215. return Math.sqrt(dlat * dlat + dlon * dlon)
  216. }
  217. /**
  218. * return the diagonal distance (using the haversine function). See https://github.com/njj/haversine for further details.
  219. * @param {object} [options] Options
  220. * @param {string} [options.unit=km] Unit of measurement applied to result ('km', 'mile', 'meter', 'nmi')
  221. * @param {number} [options.threshold] If passed, will result in library returning boolean value of whether or not the start and end points are within that supplied threshold.
  222. * @return {number}
  223. * @example
  224. * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
  225. * console.log(bbox.diagonalDistance({ unit: 'm' })) // 99.36491328576697
  226. */
  227. BoundingBox.prototype.diagonalDistance = function (options = {}) {
  228. return haversine(
  229. { latitude: this.minlat, longitude: this.minlon },
  230. { latitude: this.maxlat, longitude: this.maxlon },
  231. options
  232. )
  233. }
  234. /**
  235. * Returns the center point of the bounding box as { lat, lon }
  236. * @return {object}
  237. * @example
  238. * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
  239. * console.log(bbox.getCenter()) // { lat: 48.567499999999995, lon: 16.2985 }
  240. */
  241. BoundingBox.prototype.getCenter = function () {
  242. var dlat = this.maxlat - this.minlat
  243. var dlon = this.wrapMaxLon() - this.minlon
  244. let lon = this.minlon + dlon / 2
  245. if (lon < -180 || lon > 180) {
  246. lon = (lon + 180) % 360 - 180
  247. }
  248. return {
  249. lat: this.minlat + dlat / 2,
  250. lon
  251. }
  252. }
  253. /**
  254. * get Northern boundary (latitude)
  255. * @param {number}
  256. */
  257. BoundingBox.prototype.getNorth = function () {
  258. return this.maxlat
  259. }
  260. /**
  261. * get Southern boundary (latitude)
  262. * @param {number}
  263. */
  264. BoundingBox.prototype.getSouth = function () {
  265. return this.minlat
  266. }
  267. /**
  268. * get Eastern boundary (longitude)
  269. * @param {number}
  270. */
  271. BoundingBox.prototype.getEast = function () {
  272. return this.maxlon
  273. }
  274. /**
  275. * get Western boundary (longitude)
  276. * @param {number}
  277. */
  278. BoundingBox.prototype.getWest = function () {
  279. return this.minlon
  280. }
  281. /**
  282. * extends current boundary by the other boundary
  283. * @param {BoundingBox} other
  284. * @example
  285. * var bbox1 = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
  286. * var bbox2 = new BoundingBox({ minlat: 48.000, minlon: 16.23, maxlat: 49.012, maxlon: 16.789 })
  287. * bbox1.extend(bbox2)
  288. * console.log(bbox1.bounds) // { minlat: 48, minlon: 16.23, maxlat: 49.012, maxlon: 16.789 }
  289. */
  290. BoundingBox.prototype.extend = function (other) {
  291. if (!(other instanceof BoundingBox)) {
  292. other = new BoundingBox(other)
  293. }
  294. if (other.minlat < this.minlat) {
  295. this.minlat = other.minlat
  296. }
  297. if (other.maxlat > this.maxlat) {
  298. this.maxlat = other.maxlat
  299. }
  300. // does bounds intersect with other bounds in longitude?
  301. for (let shift = -360; shift <= 360; shift += 360) {
  302. if (other.wrapMaxLon() + shift > this.minlon && other.minlon + shift < this.wrapMaxLon()) {
  303. this.minlon = Math.min(this.minlon, other.minlon + shift)
  304. this.maxlon = Math.max(this.wrapMaxLon(), other.wrapMaxLon() + shift)
  305. this._wrap()
  306. return
  307. }
  308. }
  309. let min1 = Math.min(this.minlon, other.minlon)
  310. let min2 = Math.max(this.minlon, other.minlon)
  311. let max1 = Math.max(this.wrapMaxLon(), other.wrapMaxLon())
  312. let max2 = Math.min(this.wrapMaxLon(), other.wrapMaxLon())
  313. if (max1 - min1 < max2 - min2 + 360) {
  314. this.minlon = min1
  315. this.maxlon = max1
  316. } else {
  317. this.minlon = min2
  318. this.maxlon = max2
  319. }
  320. this._wrap()
  321. }
  322. /**
  323. * Returns the bounding box as GeoJSON feature. In case of bounding boxes crossing the antimeridian, this function will return a multipolygon with the parts on each side of the antimeridian (as specified in RFC 7946, section 3.1.9).
  324. * @return {object}
  325. * @example
  326. * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
  327. * bbox.toGeoJSON()
  328. * // {
  329. * // "type": "Feature",
  330. * // "properties": {},
  331. * // "geometry": {
  332. * // "type": "Polygon",
  333. * // "coordinates": [
  334. * // [
  335. * // [ 16.23, 48.123 ],
  336. * // [ 16.367, 48.123 ],
  337. * // [ 16.367, 49.012 ],
  338. * // [ 16.23, 49.012 ],
  339. * // [ 16.23, 48.123 ]
  340. * // ]
  341. * // ]
  342. * // }
  343. * // }
  344. */
  345. BoundingBox.prototype.toGeoJSON = function () {
  346. if (this.minlon > this.maxlon) {
  347. return {
  348. type: 'Feature',
  349. properties: {},
  350. geometry: {
  351. 'type': 'MultiPolygon',
  352. 'coordinates': [
  353. [[
  354. [ this.minlon, this.minlat ],
  355. [ 180, this.minlat ],
  356. [ 180, this.maxlat ],
  357. [ this.minlon, this.maxlat ],
  358. [ this.minlon, this.minlat ]
  359. ]],
  360. [[
  361. [ -180, this.minlat ],
  362. [ this.maxlon, this.minlat ],
  363. [ this.maxlon, this.maxlat ],
  364. [ -180, this.maxlat ],
  365. [ -180, this.minlat ]
  366. ]]
  367. ]
  368. }
  369. }
  370. }
  371. return {
  372. type: 'Feature',
  373. properties: {},
  374. geometry: {
  375. 'type': 'Polygon',
  376. 'coordinates': [[
  377. [ this.minlon, this.minlat ],
  378. [ this.maxlon, this.minlat ],
  379. [ this.maxlon, this.maxlat ],
  380. [ this.minlon, this.maxlat ],
  381. [ this.minlon, this.minlat ]
  382. ]]
  383. }
  384. }
  385. }
  386. /**
  387. * Returns the bounding box as L.latLngBounds object. Leaflet must be included separately!
  388. * @param {object} [options] Options.
  389. * @param {number[]} [options.shiftWorld=[0, 0]] Shift the world by the first value for the Western hemisphere (lon < 0) or the second value for the Eastern hemisphere (lon >= 0).
  390. */
  391. BoundingBox.prototype.toLeaflet = function (options = {}) {
  392. if (!('shiftWorld' in options)) {
  393. options.shiftWorld = [ 0, 0 ]
  394. }
  395. return L.latLngBounds(
  396. L.latLng(this.minlat, this.minlon + (this.minlon < 0 ? options.shiftWorld[0] : options.shiftWorld[1])),
  397. L.latLng(this.maxlat, this.maxlon + (this.maxlon < 0 ? options.shiftWorld[0] : options.shiftWorld[1]))
  398. )
  399. }
  400. if (typeof module !== 'undefined' && module.exports) {
  401. module.exports = BoundingBox
  402. }
  403. if (typeof window !== 'undefined') {
  404. window.BoundingBox = BoundingBox
  405. }