Source: src/js/collections/maps/GeoPoints.js

"use strict";

define(["backbone", "models/maps/GeoPoint"], function (Backbone, GeoPoint) {
  /**
   * @class GeoPoints
   * @classdesc A group of ordered geographic points.
   * @class GeoPoints
   * @classcategory Collections/Maps
   * @extends Backbone.Collection
   * @since 2.27.0
   * @constructor
   */
  var GeoPoints = Backbone.Collection.extend(
    /** @lends GeoPoints.prototype */ {
      /**
       * The class/model that this collection contains.
       * @type {Backbone.Model}
       */
      model: GeoPoint,

      /**
       * Given a point in various formats, format it such that it can be used to
       * add to this collection.
       * @param {Array|Object|GeoPoint} point - Accepted formats are:
       *   - An array of the form [longitude, latitude], with an optional third
       *     element for height
       *   - An object with a "longitude" and "latitude" property, and
       *     optionally a "height" property
       *   - A GeoPoint model
       * @returns {Object|GeoPoint} Returns an object with "longitude" and
       * "latitude" properties, and optionally a "height" property, or a
       * GeoPoint model.
       */
      formatPoint: function (point) {
        let attributes = {};
        if (Array.isArray(point) && point.length > 1) {
          attributes.longitude = point[0];
          attributes.latitude = point[1];
          if (point[2]) {
            attributes.height = point[2];
          }
        } else if (
          point instanceof GeoPoint ||
          (point.latitude && point.longitude)
        ) {
          attributes = point;
        }
        return attributes;
      },

      /**
       * Add a point to the collection. Use this rather than the Backbone add
       * method to allow for different formats of points to be added.
       * @param {Array|Object|GeoPoint} point - See {@link formatPoint} for
       * accepted formats.
       * @returns {GeoPoint} Returns the GeoPoint model that was added.
       */
      addPoint: function (point) {
        point = this.formatPoint(point);
        return this.add(point);
      },

      /**
       * Remove a specific point from the collection. Use this rather than the
       * Backbone remove method to allow for different formats of points to be
       * removed.
       * @param {Array|Object|GeoPoint|Number} indexOrPoint - The index of the
       * point to remove, or the point itself. See {@link formatPoint} for
       * accepted formats.
       * @returns {GeoPoint} Returns the GeoPoint model that was removed.
       */
      removePoint(indexOrPoint) {
        if (typeof indexOrPoint === "number") {
          return this.removePointByIndex(indexOrPoint);
        } else if (Array.isArray(indexOrPoint)) {
          return this.removePointByAttr(indexOrPoint);
        } else {
          // try just removing the point
          return this.remove(indexOrPoint);
        }
      },

      /**
       * Remove a point from the collection based on its attributes.
       * @param {Array|Object|GeoPoint} point - Any format supported by
       * {@link formatPoint} is accepted.
       * @returns {GeoPoint} Returns the GeoPoint model that was removed.
       */
      removePointByAttr: function (point) {
        point = this.formatPoint(point);
        const model = this.findWhere(point);
        return this.remove(model);
      },

      /**
       * Remove a point from the collection based on its index.
       * @param {Number} index - The index of the point to remove.
       * @returns {GeoPoint} Returns the GeoPoint model that was removed.
       */
      removePointByIndex: function (index) {
        if (index < 0 || index >= this.length) {
          console.warn("Index out of bounds, GeoPoint not removed.");
          return;
        }
        const model = this.at(index);
        return this.remove(model);
      },

      /**
       * Convert the collection to a GeoJSON object. The output can be the
       * series of points as Point features, the points connected as a
       * LineString feature, or the points connected and closed as a Polygon.
       *
       * Note: For a "Polygon" geometry type, when there's only one point in the
       * collection, the output will be a "Point". If there are only two points,
       * the output will be a "LineString", unless `forceAsPolygon` is set to
       * true.
       *
       * @param {String} geometryType - The type of geometry to create. Can be
       * "Point", "LineString", or "Polygon".
       * @param {Boolean} [forceAsPolygon=false] - Set to true to enforce the
       * output as a polygon for the "Polygon" geometry type, regardless of the
       * number of points in the collection.
       * @returns {Object} Returns a GeoJSON object of type "Point",
       * "LineString", or "Polygon".
       */
      toGeoJson: function (geometryType, forceAsPolygon = false) {
        if (!forceAsPolygon && geometryType === "Polygon" && this.length < 3) {
          geometryType = this.length === 1 ? "Point" : "LineString";
        }
        return {
          type: "FeatureCollection",
          features: this.toGeoJsonFeatures(geometryType),
        };
      },

      // TODO: Move this to a CZML model, use in GeoHash/es

      /**
       * Get the header object for a CZML document.
       * @returns {Object} Returns a CZML header object.
       */
      getCZMLHeader: function () {
        return {
          id: "document",
          version: "1.0",
          name: "GeoPoints",
        };
      },

      /**
       * Convert the collection to a CZML document.
       * @param {String} geometryType - The type of geometry to create.
       * @param {Boolean} [forceAsPolygon=false] - Set to true to enforce the
       * output as a polygon for the "Polygon" geometry type, regardless of the
       * number of points in the collection.
       * @returns {Object[]} Returns an array of CZML objects.
       */
      toCzml: function (geometryType, forceAsPolygon = false) {
        if (!forceAsPolygon && geometryType === "Polygon" && this.length < 3) {
          geometryType = this.length === 1 ? "Point" : "LineString";
        }
        let czml = [this.getCZMLHeader()];
        switch (geometryType) {
          case "Point":
            czml = czml.concat(this.toCZMLPoints());
            break;
          case "LineString":
            czml.push(this.getCZMLLineString());
            break;
          case "Polygon":
            czml.push(this.getCZMLPolygon());
            break;
          default:
            break;
        }
        return czml;
      },

      /**
       * Convert the collection to an array of CZML point objects.
       * @returns {Object[]} Returns an array of CZML point objects.
       */
      toCZMLPoints: function () {
        return this.models.map((model) => {
          return model.toCZML();
        });
      },

      /**
       * Convert the collection to a CZML polygon object.
       * @returns {Object} Returns a CZML polygon object.
       */
      getCZMLPolygon: function () {
        const coords = this.toECEFArray();
        return {
          id: this.cid,
          name: "Polygon",
          polygon: {
            positions: {
              cartesian: coords,
            },
          },
        };
      },

      /**
       * Convert the collection to a CZML line string object.
       * @returns {Object} Returns a CZML line string object.
       */
      getCZMLLineString: function () {
        const coords = this.toECEFArray();
        return {
          id: this.cid,
          name: "LineString",
          polyline: {
            positions: {
              cartesian: coords,
            },
          },
        };
      },

      /**
       * Convert the collection to a GeoJSON object. The output can be the
       * series of points as Point features, the points connected as a
       * LineString feature, or the points connected and closed as a Polygon.
       * @param {"Point"|"LineString"|"Polygon"} geometryType - The type of
       * geometry to create.
       * @returns {Object[]} Returns an array of GeoJSON features.
       */
      toGeoJsonFeatures: function (geometryType) {
        switch (geometryType) {
          case "Point":
            return this.toGeoJsonPointFeatures();
          case "LineString":
            return [this.toGeoJsonLineStringFeature()];
          case "Polygon":
            return [this.toGeoJsonPolygonFeature()];
          default:
            return [];
        }
      },

      /**
       * Convert the collection to an array of GeoJSON point features.
       * @returns {Object[]} Returns an array of GeoJSON point features.
       */
      toGeoJsonPointFeatures: function () {
        return this.models.map((model) => {
          return model.toGeoJsonFeature();
        });
      },

      /**
       * Convert the collection to a GeoJSON LineString feature.
       * @returns {Object} Returns a GeoJSON LineString feature.
       */
      toGeoJsonLineStringFeature: function () {
        return {
          type: "Feature",
          geometry: {
            type: "LineString",
            coordinates: this.to2DArray(),
          },
          properties: {},
        };
      },

      /**
       * Convert the collection to a GeoJSON Polygon feature. The polygon will
       * be closed if it isn't already.
       * @returns {Object} Returns a GeoJSON Polygon feature.
       */
      toGeoJsonPolygonFeature: function () {
        const coordinates = this.to2DArray();
        // Make sure the polygon is closed
        if (coordinates[0] != coordinates[coordinates.length - 1]) {
          coordinates.push(coordinates[0]);
        }
        return {
          type: "Feature",
          geometry: {
            type: "Polygon",
            coordinates: [coordinates],
          },
          properties: {},
        };
      },

      /**
       * Convert the collection to an array of arrays, where each sub-array
       * contains the longitude and latitude of a point.
       * @returns {Array[]} Returns an array of arrays.
       */
      to2DArray: function () {
        return this.models.map((model) => {
          return model.to2DArray();
        });
      },

      /**
       * Convert the collection to a cartesian array, where each every three
       * elements represents the x, y, and z coordinates of a vertex, e.g.
       * [x1, y1, z1, x2, y2, z2, ...].
       * @returns {Array} Returns an array of numbers.
       */
      toECEFArray: function () {
        return this.models.flatMap((model) => {
          return model.toECEFArray();
        });
      },

      /**
       * Convert the collection to an array of coordinates in the format
       * native to the map widget. For Cesium, this is an array of
       * Cartesian3 objects in ECEF coordinates.
       * @returns {Array} An array of coordinates that can be used by the map
       * widget.
       */
      asMapWidgetCoords: function () {
        return this.models.map((model) => {
          return model.get("mapWidgetCoords");
        });
      },
    },
  );

  return GeoPoints;
});