Source: src/js/models/maps/GeoBoundingBox.js

"use strict";

define(["backbone"], function (Backbone) {
  /**
   * @class GeoBoundingBox
   * @classdesc The GeoBoundingBox model stores the geographical boundaries for
   * an area on the Earth's surface. It includes the northernmost, southernmost,
   * easternmost, and westernmost latitudes and longitudes, as well as an
   * optional height parameter.
   * @classcategory Models/Maps
   * @name GeoBoundingBox
   * @since 2.27.0
   * @extends Backbone.Model
   */
  var GeoBoundingBox = Backbone.Model.extend(
    /** @lends GeoBoundingBox.prototype */ {
      /**
       * The type of model this is.
       * @type {String}
       */
      type: "GeoBoundingBox",

      /**
       * Overrides the default Backbone.Model.defaults() function to specify
       * default attributes for the GeoBoundingBox.
       * @property {number|null} north The northernmost latitude of the bounding
       * box. Should be a number between -90 and 90.
       * @property {number|null} south The southernmost latitude of the bounding
       * box. Should be a number between -90 and 90.
       * @property {number|null} east The easternmost longitude of the bounding
       * box. Should be a number between -180 and 180.
       * @property {number|null} west The westernmost longitude of the bounding
       * box. Should be a number between -180 and 180.
       * @property {number|null} [height] The height of the camera above the
       * bounding box. Represented in meters above sea level. This attribute is
       * optional and can be null.
       */
      defaults: function () {
        return {
          north: null,
          south: null,
          east: null,
          west: null,
          height: null,
        };
      },

      // /**
      //  * Run when a new GeoBoundingBox is created.
      //  * @param {Object} attrs - An object specifying configuration options for
      //  * the GeoBoundingBox. If any config option is not specified, the default
      //  * will be used instead (see {@link GeoBoundingBox#defaults}).
      //  */
      // initialize: function (attrs, options) {
      //   try {
      //     // ...
      //   } catch (e) {
      //     console.log("Error initializing a GeoBoundingBox model", e);
      //   }
      // },

      /**
       * Splits the given bounding box if it crosses the prime meridian.
       * Returns one or two new GeoBoundingBox models.
       * @returns {GeoBoundingBox[]} An array of GeoBoundingBox models. One if
       * the bounding box does not cross the prime meridian, two if it does.
       */
      split: function () {
        const { north, south, east, west } = this.getCoords();
        if (east < west) {
          return [
            new GeoBoundingBox({ north, south, east: 180, west }),
            new GeoBoundingBox({ north, south, east, west: -180 }),
          ];
        } else {
          return [this.clone()];
        }
      },

      /**
       * Get the area of this bounding box in degrees.
       * @returns {Number} The area of the bounding box in degrees. Will return
       * the globe's area if the bounding box is invalid.
       */
      getArea: function () {
        if (!this.isValid()) {
          console.warn("Invalid bounding box, returning globe area");
          return 360 * 180;
        }
        const { north, south, east, west } = this.attributes;
        // Account for cases where east < west, due to the bounds crossing the
        // prime meridian
        const lonDiff = east < west ? 360 - (west - east) : east - west;
        const latDiff = north - south;
        return Math.abs(latDiff * lonDiff);
      },

      /**
       * Return the four sides of the bounding box as an array.
       * @returns {Object} An object with the northernmost, southernmost,
       * easternmost, and westernmost coordinates of the bounding box.
       */
      getCoords: function () {
        return {
          north: this.get("north"),
          south: this.get("south"),
          east: this.get("east"),
          west: this.get("west"),
        };
      },

      /**
       * Check if the bounding box covers the entire Earth.
       * @returns {Boolean} True if the bounding box covers the entire Earth,
       * false otherwise.
       */
      coversEarth: function () {
        const { north, south, east, west } = this.getCoords();
        return north >= 90 && south <= -90 && east >= 180 && west <= -180;
      },

      /**
       * Check if another bounding box is fully contained within this bounding
       * box.
       * @param {Number} n - The northernmost latitude of the bounding box.
       * @param {Number} e - The easternmost longitude of the bounding box.
       * @param {Number} s - The southernmost latitude of the bounding box.
       * @param {Number} w - The westernmost longitude of the bounding box.
       * @returns {Boolean} True if the other bounding box is fully contained
       * within this bounding box, false otherwise.
       */
      boundsAreFullyContained: function (n, e, s, w) {
        const { north, south, east, west } = this.getCoords();
        return s >= south && w >= west && n <= north && e <= east;
      },

      /**
       * Check if another bounding box is fully outside of this bounding box.
       * @param {Number} n - The northernmost latitude of the bounding box.
       * @param {Number} e - The easternmost longitude of the bounding box.
       * @param {Number} s - The southernmost latitude of the bounding box.
       * @param {Number} w - The westernmost longitude of the bounding box.
       * @returns {Boolean} True if the other bounding box is fully outside this
       * bounding box, false otherwise.
       */
      boundsAreFullyOutside: function (n, e, s, w) {
        const { north, south, east, west } = this.getCoords();
        return n < south || s > north || e < west || w > east;
      },

      /**
       * Validate the model attributes
       * @param {Object} attrs - The model's attributes
       */
      validate: function (attrs, options) {
        const bounds = attrs;
        const isValid =
          bounds &&
          typeof bounds.north === "number" &&
          typeof bounds.south === "number" &&
          typeof bounds.east === "number" &&
          typeof bounds.west === "number" &&
          bounds.north <= 90 &&
          bounds.north >= -90 &&
          bounds.south >= -90 &&
          bounds.south <= 90 &&
          bounds.east <= 180 &&
          bounds.east >= -180 &&
          bounds.west >= -180 &&
          bounds.west <= 180;
        if (!isValid) {
          return (
            "Bounds must include a number between -90 and 90 for north " +
            "and south, and between -180 and 180 for east and west."
          );
        }
      },
    },
  );

  return GeoBoundingBox;
});