Source: BoundingBox.js

'use strict'

var GeoJSONBounds = require('geojson-bounds')
const haversine = require('haversine')

/* global L:false */

/**
 * create bounding box from input
 * @class
 * @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.
 * @example
 * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
 */
function BoundingBox (bounds) {
  var k

  if (bounds === null || typeof bounds === 'undefined') {
    this.minlat = -90
    this.minlon = -180
    this.maxlat = +90
    this.maxlon = +180
    return
  }

  // Leaflet.latLngBounds detected!
  if (typeof bounds.getSouthWest === 'function') {
    var sw = bounds.getSouthWest().wrap()
    var ne = bounds.getNorthEast().wrap()

    bounds = {
      minlat: sw.lat,
      minlon: sw.lng,
      maxlat: ne.lat,
      maxlon: ne.lng
    }
  }

  // GeoJSON detected
  if (bounds.type === 'Feature') {
    let boxes

    if (bounds.geometry.type === 'GeometryCollection') {
      boxes = bounds.geometry.geometries.map(
        geometry => {
          let b = new BoundingBox({ type: 'Feature', geometry })
          return [ b.minlon, b.minlat, b.maxlon, b.maxlat ]
        }
      )
    } else if ([ 'MultiPoint', 'MultiPolygon', 'MultiLineString' ].includes(bounds.geometry.type)) {
      boxes = bounds.geometry.coordinates.map(
        geom => GeoJSONBounds.extent({ type: 'Feature', geometry: { type: bounds.geometry.type.substr(5), coordinates: geom } })
      )
    } else {
      boxes = [ GeoJSONBounds.extent(bounds) ]
    }

    let b = boxes.shift()

    this.minlat = b[1]
    this.minlon = b[0]
    this.maxlat = b[3]
    this.maxlon = b[2]

    boxes.forEach(b => this.extend({
      minlat: b[1],
      minlon: b[0],
      maxlat: b[3],
      maxlon: b[2]
    }))

    this._wrap()

    return
  }

  if ('bounds' in bounds) {
    bounds = bounds.bounds
  }

  if (bounds.lat) {
    this.minlat = bounds.lat
    this.maxlat = bounds.lat
  }

  if (bounds.lon) {
    this.minlon = bounds.lon
    this.maxlon = bounds.lon
  }

  if (Array.isArray(bounds)) {
    this.minlat = bounds[0]
    this.maxlat = bounds[0]
    this.minlon = bounds[1]
    this.maxlon = bounds[1]
  }

  // e.g. L.latLng object
  if (bounds.lng) {
    this.minlon = bounds.lng
    this.maxlon = bounds.lng
  }

  var props = ['minlon', 'minlat', 'maxlon', 'maxlat']
  for (var i = 0; i < props.length; i++) {
    k = props[i]
    if (k in bounds) {
      this[k] = bounds[k]
    }
  }

  this._wrap()
}

BoundingBox.prototype.wrapMaxLon = function () {
  return (this.minlon > this.maxlon) ? this.maxlon + 360 : this.maxlon
}

BoundingBox.prototype.wrapMinLon = function () {
  return (this.minlon > this.maxlon) ? this.minlon - 360 : this.minlon
}

BoundingBox.prototype._wrap = function () {
  if (this.minlon < -180 || this.minlon > 180) {
    this.minlon = (this.minlon + 180) % 360 - 180
  }
  if (this.maxlon < -180 || this.maxlon > 180) {
    this.maxlon = (this.maxlon + 180) % 360 - 180
  }

  return this
}

/**
  * Checks whether the other bounding box intersects (shares any portion of space) the current object.
 * @param {BoundingBox} other Other boundingbox to check for
 * @return {boolean} true if the bounding boxes intersect
 * @example
 * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
 * var bbox2 = new BoundingBox({ lat: 48.5, lon: 16.267 })
 * console.log(bbox.intersects(bbox2)) // true
 */
BoundingBox.prototype.intersects = function (other) {
  if (!(other instanceof BoundingBox)) {
    other = new BoundingBox(other)
  }

  if (other.maxlat < this.minlat) {
    return false
  }

  if (other.minlat > this.maxlat) {
    return false
  }

  if (other.wrapMaxLon() < this.wrapMinLon()) {
    return false
  }

  if (other.wrapMinLon() > this.wrapMaxLon()) {
    return false
  }

  return true
}

/**
 * Checks whether the current object is fully within the other bounding box.
 * @param {BoundingBox} other Other boundingbox to check for
 * @return {boolean} true if the bounding boxes is within other
 * @example
 * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
 * var bbox2 = new BoundingBox({ lat: 48.5, lon: 16.267 })
 * console.log(bbox2.within(bbox)) // true
 */
BoundingBox.prototype.within = function (other) {
  if (!(other instanceof BoundingBox)) {
    other = new BoundingBox(other)
  }

  if (other.maxlat < this.maxlat) {
    return false
  }

  if (other.minlat > this.minlat) {
    return false
  }

  if (other.wrapMaxLon() < this.wrapMaxLon()) {
    return false
  }

  if (other.wrapMinLon() > this.wrapMinLon()) {
    return false
  }

  return true
}

BoundingBox.prototype.toTile = function () {
  return new BoundingBox({
    minlat: Math.floor(this.minlat * 10) / 10,
    minlon: Math.floor(this.minlon * 10) / 10,
    maxlat: Math.ceil(this.maxlat * 10) / 10,
    maxlon: Math.ceil(this.maxlon * 10) / 10
  })
}

/**
 * return the bounding box as lon-lat string, e.g. '179.5,55,-179.5,56'
 * @return {string}
 * @example
 * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
 * console.log(bbox.toLonLatString()) // '16.23,48.123,16.367,49.012'
 */
BoundingBox.prototype.toLonLatString = function () {
  return this.minlon + ',' +
         this.minlat + ',' +
         this.maxlon + ',' +
         this.maxlat
}

/**
 * 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.
 * @return {string}
 * @example
 * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
 * console.log(bbox.toBBoxString()) // '16.23,48.123,16.367,49.012'
 */
BoundingBox.prototype.toBBoxString = BoundingBox.prototype.toLonLatString

/**
 * return the bounding box as lon-lat string, e.g. '55,179.5,56,-179.5'. Useful e.g. for Overpass API requests.
 * @return {string}
 * @example
 * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
 * console.log(bbox.toLatLonString()) // '48.123,16.23,49.012,16.367'
 */
BoundingBox.prototype.toLatLonString = function () {
  return this.minlat + ',' +
         this.minlon + ',' +
         this.maxlat + ',' +
         this.maxlon
}

/**
 * return the diagonal length (length of hypothenuse).
 * @return {number}
 * @example
 * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
 * console.log(bbox.diagonalLength()) // 0.8994943023721748
 */
BoundingBox.prototype.diagonalLength = function () {
  var dlat = this.maxlat - this.minlat
  var dlon = this.wrapMaxLon() - this.minlon

  return Math.sqrt(dlat * dlat + dlon * dlon)
}

/**
 * return the diagonal distance (using the haversine function). See https://github.com/njj/haversine for further details.
 * @param {object} [options] Options
 * @param {string} [options.unit=km] Unit of measurement applied to result ('km', 'mile', 'meter', 'nmi')
 * @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.
 * @return {number}
 * @example
 * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
 * console.log(bbox.diagonalDistance({ unit: 'm' })) // 99.36491328576697
 */
BoundingBox.prototype.diagonalDistance = function (options = {}) {
  return haversine(
    { latitude: this.minlat, longitude: this.minlon },
    { latitude: this.maxlat, longitude: this.maxlon },
    options
  )
}

/**
 * Returns the center point of the bounding box as { lat, lon }
 * @return {object}
 * @example
 * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
 * console.log(bbox.getCenter()) // { lat: 48.567499999999995, lon: 16.2985 }
 */
BoundingBox.prototype.getCenter = function () {
  var dlat = this.maxlat - this.minlat
  var dlon = this.wrapMaxLon() - this.minlon
  let lon = this.minlon + dlon / 2
  if (lon < -180 || lon > 180) {
    lon = (lon + 180) % 360 - 180
  }

  return {
    lat: this.minlat + dlat / 2,
    lon
  }
}

/**
 * get Northern boundary (latitude)
 * @param {number}
 */
BoundingBox.prototype.getNorth = function () {
  return this.maxlat
}

/**
 * get Southern boundary (latitude)
 * @param {number}
 */
BoundingBox.prototype.getSouth = function () {
  return this.minlat
}

/**
 * get Eastern boundary (longitude)
 * @param {number}
 */
BoundingBox.prototype.getEast = function () {
  return this.maxlon
}

/**
 * get Western boundary (longitude)
 * @param {number}
 */
BoundingBox.prototype.getWest = function () {
  return this.minlon
}

/**
 * extends current boundary by the other boundary
 * @param {BoundingBox} other
 * @example
 * var bbox1 = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
 * var bbox2 = new BoundingBox({ minlat: 48.000, minlon: 16.23, maxlat: 49.012, maxlon: 16.789 })
 * bbox1.extend(bbox2)
 * console.log(bbox1.bounds) // { minlat: 48, minlon: 16.23, maxlat: 49.012, maxlon: 16.789 }
 */
BoundingBox.prototype.extend = function (other) {
  if (!(other instanceof BoundingBox)) {
    other = new BoundingBox(other)
  }

  if (other.minlat < this.minlat) {
    this.minlat = other.minlat
  }

  if (other.maxlat > this.maxlat) {
    this.maxlat = other.maxlat
  }

  // does bounds intersect with other bounds in longitude?
  for (let shift = -360; shift <= 360; shift += 360) {
    if (other.wrapMaxLon() + shift > this.minlon && other.minlon + shift < this.wrapMaxLon()) {
      this.minlon = Math.min(this.minlon, other.minlon + shift)
      this.maxlon = Math.max(this.wrapMaxLon(), other.wrapMaxLon() + shift)

      this._wrap()
      return
    }
  }

  let min1 = Math.min(this.minlon, other.minlon)
  let min2 = Math.max(this.minlon, other.minlon)
  let max1 = Math.max(this.wrapMaxLon(), other.wrapMaxLon())
  let max2 = Math.min(this.wrapMaxLon(), other.wrapMaxLon())

  if (max1 - min1 < max2 - min2 + 360) {
    this.minlon = min1
    this.maxlon = max1
  } else {
    this.minlon = min2
    this.maxlon = max2
  }

  this._wrap()
}

/**
 * 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).
 * @return {object}
 * @example
 * var bbox = new BoundingBox({ minlat: 48.123, minlon: 16.23, maxlat: 49.012, maxlon: 16.367 })
 * bbox.toGeoJSON()
 * // {
 * //   "type": "Feature",
 * //   "properties": {},
 * //   "geometry": {
 * //     "type": "Polygon",
 * //     "coordinates": [
 * //       [
 * //         [ 16.23, 48.123 ],
 * //         [ 16.367, 48.123 ],
 * //         [ 16.367, 49.012 ],
 * //         [ 16.23, 49.012 ],
 * //         [ 16.23, 48.123 ]
 * //       ]
 * //     ]
 * //   }
 * // }
 */
BoundingBox.prototype.toGeoJSON = function () {
  if (this.minlon > this.maxlon) {
    return {
      type: 'Feature',
      properties: {},
      geometry: {
        'type': 'MultiPolygon',
        'coordinates': [
          [[
            [ this.minlon, this.minlat ],
            [ 180, this.minlat ],
            [ 180, this.maxlat ],
            [ this.minlon, this.maxlat ],
            [ this.minlon, this.minlat ]
          ]],
          [[
            [ -180, this.minlat ],
            [ this.maxlon, this.minlat ],
            [ this.maxlon, this.maxlat ],
            [ -180, this.maxlat ],
            [ -180, this.minlat ]
          ]]
        ]
      }
    }
  }

  return {
    type: 'Feature',
    properties: {},
    geometry: {
      'type': 'Polygon',
      'coordinates': [[
        [ this.minlon, this.minlat ],
        [ this.maxlon, this.minlat ],
        [ this.maxlon, this.maxlat ],
        [ this.minlon, this.maxlat ],
        [ this.minlon, this.minlat ]
      ]]
    }
  }
}

/**
 * Returns the bounding box as L.latLngBounds object. Leaflet must be included separately!
 * @param {object} [options] Options.
 * @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).
 */
BoundingBox.prototype.toLeaflet = function (options = {}) {
  if (!('shiftWorld' in options)) {
    options.shiftWorld = [ 0, 0 ]
  }

  return L.latLngBounds(
    L.latLng(this.minlat, this.minlon + (this.minlon < 0 ? options.shiftWorld[0] : options.shiftWorld[1])),
    L.latLng(this.maxlat, this.maxlon + (this.maxlon < 0 ? options.shiftWorld[0] : options.shiftWorld[1]))
  )
}

if (typeof module !== 'undefined' && module.exports) {
  module.exports = BoundingBox
}
if (typeof window !== 'undefined') {
  window.BoundingBox = BoundingBox
}