Source: src/js/models/maps/assets/CesiumGeohash.js

"use strict";

define([
  "jquery",
  "underscore",
  "backbone",
  "cesium",
  "models/maps/assets/CesiumVectorData",
  "models/maps/AssetColorPalette",
  "models/maps/AssetColor",
  "models/maps/Geohash",
  "collections/maps/Geohashes",
], function (
  $,
  _,
  Backbone,
  Cesium,
  CesiumVectorData,
  AssetColorPalette,
  AssetColor,
  Geohash,
  Geohashes,
) {
  /**
   * @classdesc A Geohash Model represents a geohash layer in a map.
   * @classcategory Models/Maps/Assets
   * @class CesiumGeohash
   * @name CesiumGeohash
   * @extends CesiumVectorData
   * @since 2.18.0
   * @constructor
   */
  return CesiumVectorData.extend(
    /** @lends Geohash.prototype */ {
      /**
       * The name of this type of model. This will be updated to
       * 'CesiumVectorData' upon initialization.
       * @type {string}
       */
      type: "CesiumGeohash",

      /**
       * Default attributes for Geohash models
       * @name CesiumGeohash#defaults
       * @type {Object}
       * @extends CesiumVectorData#defaults
       * @property {'CesiumGeohash'} type The format of the data.
       * @property {string} label The label for the layer.
       * @property {Geohashes} geohashes The collection of geohashes to display
       * on the map.
       * @property {number} opacity The opacity of the layer.
       * @property {AssetColorPalette} colorPalette The color palette for the
       * layer.
       * @property {AssetColor} outlineColor The outline color for the layer.
       * @property {AssetColor} highlightColor The color to use for features
       * that are selected/highlighted.
       * @property {boolean} showLabels Whether to show labels for the layer.
       * @property {number} maxGeoHashes The maximum number of geohashes to
       * render at a time for performance reasons. If the number of geohashes
       * exceeds this number, then the precision will be decreased until the
       * number of geohashes is less than or equal to this number.
       */
      defaults: function () {
        return Object.assign(CesiumVectorData.prototype.defaults(), {
          type: "CZMLDataSource",
          label: "Dataset counts",
          geohashes: new Geohashes(),
          opacity: 0.8,
          colorPalette: new AssetColorPalette({
            paletteType: "continuous",
            property: "count",
            colors: [
              {
                value: 0,
                color: "#FFFFFF00",
              },
              {
                value: 1,
                color: "#1BFAC44C",
              },
              {
                value: "max",
                color: "#1BFA8FFF",
              },
            ],
          }),
          outlineColor: new AssetColor({
            color: "#DFFAFAED",
          }),
          highlightColor: new AssetColor({
            color: "#f3e227",
          }),
          showLabels: true,
          maxGeoHashes: 900,
        });
      },

      /**
       * Executed when a new CesiumGeohash model is created.
       * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of
       * the attributes, which will be set on the model.
       */
      initialize: function (assetConfig) {
        try {
          if (this.get("showLabels")) {
            this.set("type", "CzmlDataSource");
          } else {
            this.set("type", "GeoJsonDataSource");
          }
          this.startListening();
          CesiumVectorData.prototype.initialize.call(this, assetConfig);
        } catch (error) {
          console.log("Error initializing a CesiumVectorData model", error);
        }
      },

      /**
       * Get the property that we want the geohashes to display, e.g. count.
       * @returns {string} The property of interest.
       * @since 2.25.0
       */
      getPropertyOfInterest: function () {
        return this.get("colorPalette")?.get("property");
      },

      /**
       * For the property of interest (e.g. count) Get the min and max values
       * from the geohashes collection and update the color palette.
       * @since 2.25.0
       *
       */
      updateColorRangeValues: function () {
        const colorPalette = this.get("colorPalette");
        const geohashes = this.get("geohashes");
        if (!geohashes || !colorPalette) {
          return;
        }
        const vals = geohashes.getAttr(this.getPropertyOfInterest());
        if (!vals || vals.length === 0) {
          return;
        }
        colorPalette.set("minVal", Math.min(...vals));
        colorPalette.set("maxVal", Math.max(...vals));
      },

      /**
       * Get the associated precision level for the current camera height.
       * Required that a mapModel be set on the model. If one is not set, then
       * the minimum precision from the geohash collection will be returned.
       * @returns {number} The precision level.
       * @since 2.25.0
       */
      getPrecision: function () {
        const limit = this.get("maxGeoHashes");
        const geohashes = this.get("geohashes");
        const area = this.getViewExtent().getArea();
        return geohashes.getMaxPrecision(area, limit);
      },

      /**
       * Replace the collection of geohashes to display on the map with a new
       * set.
       * @param {Geohash[]|Object[]} geohashes The new set of geohash models to
       * display or attributes for the new geohash models.
       * @since 2.25.0
       */
      replaceGeohashes: function (geohashes) {
        this.get("geohashes").reset(geohashes);
      },

      /**
       * Stop the model from listening to itself for changes.
       * @since 2.25.0
       */
      stopListeners: function () {
        this.stopListening(this.get("geohashes"), "add remove update reset");
      },

      /**
       * Update and re-render the geohashes when the collection of geohashes
       * changes.
       * @since 2.25.0
       */
      startListening: function () {
        try {
          this.stopListeners();
          this.listenTo(
            this.get("geohashes"),
            "add remove update reset",
            function () {
              this.updateColorRangeValues();
              this.createCesiumModel(true);
            },
          );
        } catch (error) {
          console.log("Failed to set listeners in CesiumGeohash", error);
        }
      },

      /**
       * Get the geohash collection and optionally reduce it to only those
       * geohashes that are within the current map extent, and to no more than
       * the specified amount.
       * @param {boolean} [limitToExtent=true] Whether to limit the geohashes
       * to those that are within the current map extent.
       * @returns {Geohashes} The geohashes to display.
       */
      getGeohashes: function (limitToExtent = true) {
        let geohashes = this.get("geohashes");
        if (limitToExtent) {
          geohashes = this.getGeohashesForExtent();
        }
        return geohashes;
      },

      /**
       * Get the geohashes that are currently in the map's extent.
       * @returns {Geohashes} The geohashes in the current extent.
       * @since 2.25.0
       */
      getGeohashesForExtent: function () {
        const bounds = this.getViewExtent()?.clone();
        if (!bounds) return this.get("geohashes");
        return this.get("geohashes")?.getSubsetByBounds(bounds);
      },

      /**
       * Get the current map extent from the Map Interaction model.
       * @returns {GeoBoundingBox} The current map extent
       */
      getViewExtent: function () {
        return this.get("mapModel")?.get("interactions")?.get("viewExtent");
      },

      /**
       * Returns the GeoJSON representation of the geohashes.
       * @param {Boolean} [limitToExtent = true] - Set to false to return the
       * GeoJSON for all geohashes, not just those in the current extent.
       * @returns {Object} The GeoJSON representation of the geohashes.
       * @since 2.25.0
       */
      getGeoJSON: function (limitToExtent = true) {
        const geohashes = this.getGeohashes(limitToExtent);
        return geohashes.toGeoJSON();
      },

      /**
       * Returns the CZML representation of the geohashes.
       * @param {Boolean} [limitToExtent = true] - Set to false to return the
       * CZML for all geohashes, not just those in the current extent.
       * @returns {Object} The CZML representation of the geohashes.
       * @since 2.25.0
       */
      getCZML: function (limitToExtent = true) {
        const geohashes = this.getGeohashes(limitToExtent);
        const label = this.getPropertyOfInterest();
        return geohashes.toCZML(label);
      },

      /**
       * Create the Cesium model for the geohashes.
       * @param {Boolean} [recreate = false] - Set to true to recreate the
       * Cesium model.
       */
      createCesiumModel: function (recreate = false) {
        try {
          const model = this;
          // If there is no map model, wait for it to be set so that we can
          // limit the geohashes to the current extent. Otherwise, too many
          // geohashes will be rendered.
          if (!model.get("mapModel")) {
            model.listenToOnce(model, "change:mapModel", function () {
              model.createCesiumModel(recreate);
            });
            return;
          }
          // Set the GeoJSON representing geohashes on the model
          const cesiumOptions = this.getCesiumOptions();
          const type = model.get("type");
          const data =
            type === "GeoJsonDataSource" ? this.getGeoJSON() : this.getCZML();
          cesiumOptions["data"] = data;
          cesiumOptions["height"] = 0;
          model.set("cesiumOptions", cesiumOptions);
          // Create the model like a regular vector data source
          CesiumVectorData.prototype.createCesiumModel.call(this, recreate);
        } catch (e) {
          console.log("Error creating a CesiumGeohash model. ", e);
        }
      },

      /**
       * Find the geohash Entity on the map and add it to the selected
       * features.
       * @param {string} geohash The geohash to select.
       * @since 2.25.0
       */
      selectGeohashes: function (geohashes) {
        try {
          const toSelect = [
            ...new Set(
              geohashes.map((geohash) => {
                const parent =
                  this.get("geohashes").getContainingGeohash(geohash);
                return parent?.get("hashString");
              }, this),
            ),
          ];
          const entities = this.get("cesiumModel").entities.values;
          const selected = entities.filter((entity) => {
            const hashString = this.getPropertiesFromFeature(entity).hashString;
            return toSelect.includes(hashString);
          });
          const featureAttrs = selected.map((feature) => {
            return this.getFeatureAttributes(feature);
          });
          this.get("mapModel").selectFeatures(featureAttrs);
        } catch (e) {
          console.log("Error selecting geohashes", e);
        }
      },
    },
  );
});