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

"use strict";

define([
  "underscore",
  "backbone",
  "models/portals/PortalImage",
  "models/maps/AssetColorPalette",
  "common/IconUtilities",
  "common/SearchParams",
  `${MetacatUI.root}/components/dayjs.min.js`,
], (
  _,
  Backbone,
  PortalImage,
  AssetColorPalette,
  IconUtilities,
  SearchParams,
  dayjs,
) => {
  /**
   * @classdesc A MapAsset Model comprises information required to fetch source data for
   * some asset or resource that is displayed in a map, such as imagery (raster) tiles,
   * vector data, a 3D tileset, or a terrain model. This model also contains metadata
   * about the source data, like an attribution and a description. It represents the
   * most generic type of asset, and can be extended to support specific assets that are
   * compatible with a given map widget.
   * @classcategory Models/Maps/Assets
   * @class MapAsset
   * @name MapAsset
   * @augments Backbone.Model
   * @since 2.18.0
   * @class
   */
  const MapAsset = Backbone.Model.extend(
    /** @lends MapAsset.prototype */ {
      /**
       * The name of this type of model
       * @type {string}
       */
      type: "MapAsset",

      /**
       * Default attributes for MapAsset models
       * @name MapAsset#defaults
       * @type {object}
       * @property {('Cesium3DTileset'|'BingMapsImageryProvider'|'IonImageryProvider'|'WebMapTileServiceImageryProvider'|'TileMapServiceImageryProvider'|'CesiumTerrainProvider')} type
       * The format of the data. Must be one of the supported types.
       * @property {string} label A user friendly name for this asset, to be displayed
       * in a map.
       * @property {string} [icon] A PID for an SVG saved as a dataObject, or an SVG
       * string. The SVG will be used as an icon that will be displayed next to the label
       * in the layers list. It should be an SVG file that has no fills, borders, or styles
       * set on it (since the icon will be shaded dynamically by the maps CSS using a fill
       * attribute). It must use a viewbox property rather than a height and width. When
       * not specified, a layer icon is used as default.
       * @property {string} [description = ''] A brief description about the asset, e.g.
       * which area it covers, the resolution, etc.
       * @property {string} [attribution = ''] A credit or attribution to display along
       * with this map resource.
       * @property {string} [moreInfoLink = ''] A link to show in a map where a user can
       * find more information about this resource.
       * @property {string} [downloadLink = ''] A link to show in a map where a user can
       * go to download the source data.
       * @property {string} [id = ''] If this asset's data is archived in a DataONE
       * repository, the ID of the data package.
       * @property {boolean} [selected = false] Set to true when this asset has been
       * selected by the user in the layer list.
       * @property {number} [opacity = 1] A number between 0 and 1 indicating the
       * opacity of the layer on the map, with 0 representing fully transparent and 1
       * representing fully opaque. This applies to raster (imagery) and vector assets,
       * not to terrain assets.
       * @property {number} [saturation = 1] A number that indicates the saturation of
       * the layer on the map. Less than 1.0 reduces the saturation while greater than
       * 1.0 increases it. This applies to raster (imagery) only.
       * @property {boolean} [visible = true] Set to true if the layer is visible on the
       * map, false if it is hidden. This applies to raster (imagery) and vector assets,
       * not to terrain assets.
       * @property {boolean} [configuredVisibility = true] Tracks the original
       * visibility value according to the portal configuration and ignoring any
       * search query parameters in the URL which can affect the layer's initial
       * visibility.
       * @property {boolean} [originalVisibility = true] Tracks the original
       * visibility value according to search query parameters and portal
       * configuration.
       * @property {AssetColorPalette} [colorPalette] The color or colors mapped to
       * attributes of this asset. This applies to raster/imagery and vector assets. For
       * imagery, the colorPalette will be used to create a legend. For vector assets
       * (e.g. 3Dtilesets), it will also be used to style the features.
       * @property {MapConfig#FeatureTemplate} [featureTemplate] Configuration for
       * content and layout of the Feature Info panel - the panel that shows information
       * about a selected feature from a vector asset ({@link FeatureInfoView}).
       * @property {Cesium.Entity|Cesium.Cesium3DTilesetFeature} [featureType] For vector
       * and 3d tileset assets, the object type that cesium uses to represent features
       * from the asset. Null for imagery and terrain assets.
       * @property {MapConfig#CustomProperties} [customProperties] Configuration that
       * allows for the definition of custom feature properties, potentially based on
       * other properties. For example, a custom property could be a formatted version
       * of an existing date property.
       * @property {MapConfig#Notification} [notification] A custom badge and message to
       * display about the layer in the Layer list. For example, this could highlight
       * the layer if it is new, give a warning if they layer is under development, etc.
       * @property {'ready'|'error'|null} [status = null] Set to 'ready' when the
       * resource is loaded and ready to be rendered in a map view. Set to 'error' when
       * the asset is not supported, or there was a problem requesting the resource.
       * @property {string} [statusDetails = null] Any further details about the status,
       * especially when there was an error.
       * @property {boolean} [hideInLayerList = false] Set to true to hide this asset
       * from the layer list.
       */
      defaults() {
        return {
          type: "",
          label: "",
          icon: '<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><path d="m3.2 7.3 8.6 4.6a.5.5 0 0 0 .4 0l8.6-4.6a.4.4 0 0 0 0-.8L12.1 3a.5.5 0 0 0-.4 0L3.3 6.5a.4.4 0 0 0 0 .8Z"></path><path d="M20.7 10.7 19 9.9l-6.7 3.6a.5.5 0 0 1-.4 0L5 9.9l-1.8.8a.5.5 0 0 0 0 .8l8.5 5a.5.5 0 0 0 .5 0l8.5-5a.5.5 0 0 0 0-.8Z"></path><path d="m20.7 15.1-1.5-.7-7 3.8a.5.5 0 0 1-.4 0l-7-3.8-1.5.7a.5.5 0 0 0 0 .9l8.5 5a.5.5 0 0 0 .5 0l8.5-5a.5.5 0 0 0 0-.9Z"></path></svg>',
          description: "",
          attribution: "",
          moreInfoLink: "",
          downloadLink: "",
          selected: false,
          opacity: 1,
          saturation: 1,
          visible: true,
          configuredVisibility: true,
          originalVisibility: true,
          colorPalette: null,
          customProperties: {},
          featureTemplate: {},
          featureType: null,
          notification: {},
          status: null,
          statusDetails: null,
          hideInLayerList: false,
        };
      },

      /**
       * The source of a specific asset (i.e. layer or terrain data) to show on the map,
       * as well as metadata and display properties of the asset. Some properties listed
       * here do not apply to all asset types, but this is specified in the property
       * description.
       * @typedef {object} MapAssetConfig
       * @name MapConfig#MapAssetConfig
       * @property {('Cesium3DTileset'|'BingMapsImageryProvider'|'IonImageryProvider'|'WebMapTileServiceImageryProvider'|'WebMapServiceImageryProvider'|'TileMapServiceImageryProvider'|'NaturalEarthII'|'CesiumTerrainProvider'|'GeoJsonDataSource'|'USGSImageryTopo'|'OpenStreetMapImageryProvider')} type -
       * A string indicating the format of the data. Some of these types correspond
       * directly to Cesium classes. The NaturalEarthII type is a special imagery layer
       * that automatically sets the cesiumOptions to load the Natural Earth II imagery
       * that is shipped with Cesium/MetacatUI. If this type is set, then no other
       * cesiumOptions are required. The same is true for USGSImageryTopo, which pulls
       * imagery directly from USGS.
       * @property {(Cesium3DTileset#cesiumOptions|CesiumImagery#cesiumOptions|CesiumTerrain#cesiumOptions|CesiumVectorData#cesiumOptions)} [cesiumOptions] -
       * For MapAssets that are configured for Cesium, like
       * Cesium3DTilesets, an object with options to pass to the Cesium constructor
       * function that creates the Cesium model. Options are specific to each type of
       * asset. For details, see documentation for each of the types.
       * @property {string} label - A user friendly name for this asset, to be displayed
       * in a map.
       * @property {string} [icon] - A PID for an SVG saved as a dataObject, or an SVG
       * string. The SVG will be used as an icon that will be displayed next to the
       * label in the layers list. It should be an SVG file that has no fills, borders,
       * or styles set on it (since the icon will be shaded dynamically by the maps CSS
       * using a fill attribute). It must use a viewbox property rather than a height
       * and width.
       * @property {number} [opacity=1] - A number between 0 and 1 indicating the
       * opacity of the layer on the map, with 0 representing fully transparent and 1
       * representing fully opaque. This applies to raster (imagery) and vector assets,
       * not to terrain assets.
       * @property {number} [saturation=1] - A number that indicates the saturation of
       * the layer on the map. Less than 1.0 reduces the saturation while greater than
       * 1.0 increases it. This applies to raster (imagery) only.
       * @property {boolean} [visible=true] - Set to true if the layer is visible on the
       * map, false if it is hidden. This applies to raster (imagery) and vector assets,
       * not to terrain assets.
       * @property {string} [description] - A brief description about the asset, e.g.
       * which area it covers, the resolution, etc.
       * @property {string} [attribution] A credit or attribution to display along with
       * this asset.
       * @property {string} [moreInfoLink] A complete URL used to create a link to show
       * in a map where a user can find more information about this resource.
       * @property {string} [downloadLink]  A complete URL used to show a link in a map
       * where a user can go to download the source data.
       * @property {string} [id] If this asset's data is archived in a DataONE
       * repository, the ID of the data package.
       * @property {MapConfig#ColorPaletteConfig} [colorPalette] The color or colors
       * mapped to attributes of this asset. This applies to raster/imagery and vector
       * assets. For imagery, the colorPalette will be used to create a legend. For
       * vector assets (e.g. 3Dtilesets), it will also be used to style the features.
       * @property {MapConfig#FeatureTemplate} [featureTemplate] Configuration for the
       * content and layout of the Feature Info panel ({@link FeatureInfoView}) - the
       * panel that shows information about a selected feature from a vector asset. If
       * no feature template is set, then the default table layout is used.
       * @property {MapConfig#CustomProperties} [customProperties] Definitions of custom
       * properties of features, potentially based on existing properties. For example,
       * a custom property could be a formatted version of another date property. These
       * custom properties can be used in the filters, colorPalette, or featureTemplate.
       * So far, custom strings and formatted dates are supported. Eventually, the
       * custom properties may be expanded to support formatted numbers and booleans.
       * @property {MapConfig#VectorFilterConfig} [filters] - A set of conditions used
       * to show or hide specific features of this tileset.
       * @property {MapConfig#Notification} [notification] A custom badge and message to
       * display about the layer in the Layer list. For example, this could highlight
       * the layer if it is new, give a warning if they layer is under development, etc.
       * @property {boolean} [hideInLayerList] - Set to true to hide this asset from the
       * layer list.
       */

      /**
       * A feature template configures the format and content of information displayed
       * in the Feature Info panel ({@link FeatureInfoView}). The Feature Info panel is
       * displayed in a map when a user clicks on a vector feature in a map.
       * @typedef {object} FeatureTemplate
       * @name MapConfig#FeatureTemplate
       * @since 2.19.0
       * @property {'story'|'table'} [template='table'] The name/ID of the template to
       * use. This must match the name of one of the templates available in
       * {@link FeatureInfoView#contentTemplates}.
       * @property {string} [label] Sets which of the feature properties to use as the
       * title for the FeatureInfoView. The string must exactly match the key for a
       * property that exists in the feature.
       * @property {MapConfig#StoryTemplateOptions} [options] A list of key-value pairs
       * that map the template variable to a property/attribute of the the feature. Keys
       * are the template variable names and values are the names of properties in the
       * feature. Template variable names are specific to each template. Currently only
       * the 'story' template allows variables. These are specified in the
       * {@link FeatureInfoView#contentTemplates}.
       * @example
       * // Use the "story" template, which shows a secondary title, image, description,
       * // and link.
       * {
       *   "template": "story",
       *   "label": "title",
       *   "options": {
       *     "subtitle": "formattedDate",
       *     "description": "summary",
       *     "thumbnail": "imageSrc",
       *     "url": "newsLink",
       *     "urlText": "newsTitle",
       *   }
       * }
       * @example
       * // Use the default template (a table), but use the "forestName" attribute for
       * // the FeatureInfo panel label
       * {
       *   "label": "forestName"
       * }
       */

      /**
       * An object that maps template variable to feature properties for the "story"
       * template.
       * @typedef {object} StoryTemplateOptions
       * @name MapConfig#StoryTemplateOptions
       * @since 2.19.0
       * @property {string} subtitle The name of a feature property to use for a
       * secondary title in the template
       * @property {string} description The name of a feature property that contains a
       * brief summary or description of the feature; displayed as a paragraph.
       * @property {string} thumbnail The name of a feature property that contains a URL
       * for an image. Displayed as a thumbnail next to the description.
       * @property {string} url The name of a feature property with a URL to use to
       * create a link (e.g. to learn more information about the given feature)
       * @property {string} urlText The name of a feature property that has text to
       * display for the url. Defaults to 'Read More' if none is set.
       */

      /**
       * An object where the keys indicate the name/ID of the new custom property to
       * create, and the values are an object that defines the new property.
       * @typedef {{string: (MapConfig#CustomDateProperty|MapConfig#CustomStringProperty)}} CustomProperties
       * @name MapConfig#CustomProperties
       * @since 2.19.0
       * @example
       * {
       *   "year": {
       *     "type": "date",
       *     "property": "dateTime",
       *     "format": "YYYY",
       *   },
       *   "urlText": {
       *     "type": "string",
       *     "value": "Click here to learn more about this feature"
       *   }
       * }
       */

      /**
       * An object that defines a formatted date to use as a property in a feature. Used
       * in the {@link MapConfig#CustomProperties} object.
       * @typedef {object} CustomDateProperty
       * @name MapConfig#CustomDateProperty
       * @since 2.19.0
       * @property {'date'} type Must be set to 'date' to indicate that this is a custom
       * date property
       * @property {string} property The name/ID of the existing date property to format
       * @property {string} format A string that indicates the new format to use.
       * Follows the syntax used by Day.JS, see
       * {@link https://day.js.org/docs/en/display/format}
       */

      /**
       * An object that defines a custom string to use as a property in a feature. Used
       * in the {@link MapConfig#CustomProperties} object.
       * @typedef {object} CustomStringProperty
       * @name MapConfig#CustomStringProperty
       * @since 2.19.0
       * @property {'string'} type Must be set to 'string' to indicate that this is a
       * custom string property
       * @property {string} value The new string to use. So far only static strings are
       * available. In the future, templates that include other properties may be
       * supported.
       */

      /**
       * A notification displays a badge in the {@link LayerListView} and a message in
       * the {@link LayerDetailsView}. This is useful for indicating some special status
       * of the layer: "new", "under development", etc.
       * @typedef {object} Notification
       * @name MapConfig#Notification
       * @since 2.22.0
       * @property {'yellow'|'green'|'blue'|'contrast'} [style] - The badge and message
       * color. If none is set, then notification elements will be similar to the
       * background colour (subtle).
       * @property {string} badge - The text to display in the badge element next to the
       * layer label in the list. This badge should be as few characters as possible.
       * @property {string} message - A longer message to display explaining the status.
       */

      /**
       * Executed when a new MapAsset model is created.
       * @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
       * attributes, which will be set on the model.
       */
      initialize(assetConfig) {
        const model = this;

        const assetConfigCopy =
          !assetConfig || typeof assetConfig !== "object" ? {} : assetConfig;

        // Set the color palette
        if (assetConfigCopy.colorPalette) {
          this.set(
            "colorPalette",
            new AssetColorPalette(assetConfigCopy.colorPalette),
          );
        }

        // Fetch the icon, if there is one
        if (assetConfigCopy.icon) {
          try {
            if (IconUtilities.isSVG(assetConfigCopy.icon)) {
              model.updateIcon(assetConfigCopy.icon);
            } else {
              model.set("iconStatus", "fetching");
              // If the string is not an SVG then assume it is a PID and try to fetch
              // the SVG file.
              IconUtilities.fetchIcon(assetConfigCopy.icon).then((icon) =>
                model.updateIcon(icon),
              );
            }
          } catch (error) {
            model.set("iconStatus", "error");
          }
        }

        this.on("change:visible", () => {
          if (!this.get("mapModel")?.get("showShareUrl")) return;

          this.get("mapModel")
            .get("allLayers")
            .forEach((layer) => {
              const layerId = layer.get("layerId");
              if (layerId && layer.get("visible")) {
                SearchParams.addEnabledLayer(layerId);
              } else {
                SearchParams.removeEnabledLayer(layerId);
              }
            });
        });

        this.setListeners();
      },

      /**
       * Set an error status and message for this asset.
       * @param {object | string} error - An error object with a status code
       * attribute or or string with details about the error.
       * @since 2.27.0
       */
      setError(error) {
        // See https://cesium.com/learn/cesiumjs/ref-doc/RequestErrorEvent.html
        let details = error;
        // Write a helpful error message
        switch (error.statusCode) {
          case 404:
            details = "The resource was not found (error code 404).";
            break;
          case 500:
            details = "There was a server error (error code 500).";
            break;
          default:
        }
        this.set("status", "error");
        this.set("statusDetails", details);
      },

      /**
       * Set a ready status for this asset.
       * @since 2.27.0
       */
      setReady() {
        this.set("status", "ready");
        this.set("statusDetails", null);
      },

      /**
       * When the asset can't be loaded, hide it from the map and show an error.
       * @since 2.27.0
       */
      handleError() {
        this.set("visible", false);
        this.stopListening(this, "change:visible");
      },

      /**
       * Set all of the listeners for this model
       * @since 2.27.0
       */
      setListeners() {
        const status = this.get("status");
        if (status === "error") {
          this.handleError();
          return;
        }

        const vis = this.get("originalVisibility");
        if (typeof vis === "boolean") {
          this.set("visible", vis);
        }

        // The map asset cannot be visible on the map if there was an error
        // loading the asset
        this.stopListening(this, "change:status");
        this.listenTo(this, "change:status", this.setListeners);

        // Listen for changes to the cesiumOptions object
        this.stopListening(this, "change:cesiumOptions");
        this.listenTo(this, "change:cesiumOptions", () => {
          this.createCesiumModel(true);
        });

        this.listenToSelectedFeatures();
      },

      /**
       * Update the appearance of features from this asset when they are
       * selected or deselected in the map widget.
       * @since 2.27.0
       */
      listenToSelectedFeatures() {
        if (typeof this.updateAppearance !== "function") {
          return;
        }

        const mapModel = this.get("mapModel");
        if (!mapModel) {
          this.listenToOnce(
            this,
            "change:mapModel",
            this.listenToSelectedFeatures,
          );
          return;
        }

        const interactions = mapModel.get("interactions");

        if (!interactions) {
          this.listenToOnce(
            mapModel,
            "change:interactions",
            this.listenToSelectedFeatures,
          );
          return;
        }

        const selectedFeatures = mapModel.getSelectedFeatures();

        if (selectedFeatures) {
          this.stopListening(selectedFeatures, "update");
          this.listenTo(selectedFeatures, "update", this.updateAppearance);
        }

        // Reset the listeners if the selectedFeatures collection or the
        // interactions model is replaced
        this.listenToOnce(interactions, "change:selectedFeatures", () => {
          this.updateAppearance();
          this.listenToSelectedFeatures();
        });

        this.listenToOnce(mapModel, "change:interactions", () => {
          this.updateAppearance();
          this.listenToSelectedFeatures();
        });
      },

      /**
       * Get the asset config's cesiumOptions, if it has any. This will return
       * a copy of the cesiumOptions object, so that changes made to the
       * returned object will not affect the original cesiumOptions object.
       * @returns {object} A copy of the cesiumOptions object, or null if there
       * are no cesiumOptions.
       * @since 2.26.0
       */
      getCesiumOptions() {
        const cesiumOptions = this.get("cesiumOptions");
        if (!cesiumOptions) {
          return null;
        }
        return JSON.parse(JSON.stringify(cesiumOptions));
      },

      /**
       * Given a feature object from a Feature model, checks if it is part of the
       * selectedFeatures collection. See featureObject property from
       * {@link Feature#defaults}. For vector and 3d tile models only.
       * @param {*} feature - An object that a Map widget uses to represent this feature
       * in the map, e.g. a Cesium.Entity or a Cesium.Cesium3DTileFeature
       * @returns {boolean} Returns true if the given feature is part of the
       * selectedFeatures collection in this asset
       */
      featureIsSelected(feature) {
        const map = this.get("mapModel");
        if (!map) {
          return false;
        }
        const selectedFeatures = map.getSelectedFeatures();
        if (!selectedFeatures) {
          return false;
        }
        const isSelected = selectedFeatures.containsFeature(feature);
        return isSelected;
      },

      /**
       * Checks if a feature from the map (a Cesium object) is the type of
       * feature that this map asset model contains. For example, if a
       * Cesium3DTilesetFeature is passed to this function, this function
       * will return true if it is a Cesium3DTileset model, and false if it
       * is a CesiumVectorData model.
       * @param {Cesium.Cesium3DTilesetFeature|Cesium.Entity} feature feature to be checked.
       * @returns {boolean} true if the feature is an instance of the feature
       * type set on the asset model, false otherwise.
       * @since 2.25.0
       */
      usesFeatureType(feature) {
        const ft = this.get("featureType");
        if (!feature || !ft) return false;
        if ((!feature) instanceof ft) return false;
        return true;
      },

      /**
       * Given a feature object from a Feature model, checks if it is part of the
       * selectedFeatures collection. See featureObject property from
       * {@link Feature#defaults}. For vector and 3d tile models only.
       * @param {*} feature - An object that a Map widget uses to represent this feature
       * in the map, e.g. a Cesium.Entity or a Cesium.Cesium3DTileFeature
       * @returns {boolean} Returns true if the given feature is part of the
       * selectedFeatures collection in this asset
       * @since 2.25.0
       */
      containsFeature(feature) {
        if (!this.usesFeatureType(feature)) return false;
        if (!this.getCesiumModelFromFeature) return false;
        const cesiumModel = this.getCesiumModelFromFeature(feature);
        if (!cesiumModel) return false;
        if (this.get("cesiumModel") === cesiumModel) return true;
        return false;
      },

      /**
       * Given a feature object from a Feature model, returns the attributes
       * needed to create a Feature model. For vector and 3d tile models only.
       * @param {*} feature - An object that a Map widget uses to represent this feature
       * in the map, e.g. a Cesium.Entity or a Cesium.Cesium3DTileFeature
       * @returns {object} An object with properties, mapAsset, featureID, featureObject,
       * and label properties. Returns null if the feature is not the correct type
       * for this asset model.
       */
      getFeatureAttributes(feature) {
        if (!this.usesFeatureType(feature)) return null;
        if (!this.getCesiumModelFromFeature) return null;
        return {
          properties: this.getPropertiesFromFeature(feature),
          mapAsset: this,
          featureID: this.getIDFromFeature(feature),
          featureObject: feature,
          label: this.getLabelFromFeature(feature),
        };
      },

      /**
       * Given a set of properties from a Feature from this Map Asset model, add any
       * custom properties to the properties object and return it.
       * @since 2.19.0
       * @param {object} properties A set of key-value pairs representing the existing
       * properties of a feature from this asset.
       * @returns {object} The properties object with any custom properties added.
       */
      addCustomProperties(properties) {
        const model = this;
        const customProperties = model.get("customProperties");
        const formattedProperties = {};

        if (!customProperties || !Object.keys(customProperties).length) {
          return properties;
        }

        const propertiesCopy =
          properties && typeof properties === "object" ? properties : {};

        if (customProperties) {
          _.each(customProperties, (config, key) => {
            let formattedValue = "";
            if (config.type === "date") {
              formattedValue = model.formatDateProperty(config, propertiesCopy);
              // TODO: support formatted numbers and booleans...
              // } else if (config.type === 'number') {
              //   formattedValue = model.formatNumberProperty(config, properties)
              // } else if (config.type === 'boolean') {
              //   formattedValue = model.formatBooleanProperty(config, properties)
            } else {
              formattedValue = model.formatStringProperty(config);
            }
            formattedProperties[key] = formattedValue;
          });
        }
        // merge the properties with the formatted properties
        return Object.assign(propertiesCopy, formattedProperties);
      },

      /**
       * Given a definition for a new date property, and the properties that already
       * exist on a specific feature, returns a new string with the formatted date.
       * @since 2.19.0
       * @param {MapConfig#CustomDateProperty} config - An object that defines the new
       * date property to create
       * @param {object} properties key-value pairs representing existing properties in
       * a Feature
       * @returns {string} The value for the new date property, formatted as defined by
       * config, for the given feature
       */
      formatDateProperty(config, properties) {
        const propertiesCopy = properties || {};
        let formattedDate = "";
        if (!config || !config.format) {
          return formattedDate;
        }
        const value = propertiesCopy[config.property];
        if (value) {
          formattedDate = dayjs(value).format(config.format);
        }
        return formattedDate;
      },

      /**
       * For a given set of Feature properties and a definition for a new sting
       * property, returns the value of the custom property. Note that since only static
       * strings are supported so far, this function essentially just returns the value
       * of config.value. This function exists to allow support of dynamic strings in
       * the future (e.g. combining strings from existing properties)
       * @since 2.19.0
       * @param {MapConfig#CustomStringProperty} config The object the defines the new
       * custom property
       * @returns {string} The new string for the given Feature property
       */
      formatStringProperty(config) {
        let formattedString = "";
        if (!config || !config.value) {
          return formattedString;
        }
        formattedString = config.value;
        return formattedString;
      },

      // formatNumberProperty: function (config, properties) {
      //   try {
      //     if (!properties) {
      //       properties = {}
      //     }
      //     let formattedNumber = ''
      //     // TODO...
      //   }
      //   catch (error) {
      //     console.log(
      //       'There was an error formatting a number for a Feature model' +
      //       '. Error details: ' + error
      //     );
      //     return '';
      //   }
      // },

      // formatBooleanProperty: function (config, properties) {
      //   try {
      //     if (!properties) {
      //       properties = {}
      //     }
      //     let formattedBoolean = ''
      //     // TODO...
      //   }
      //   catch (error) {
      //     console.log(
      //       'There was an error formatting a boolean for a Feature model' +
      //       '. Error details: ' + error
      //     );
      //     return '';
      //   }
      // },

      /**
       * Sanitizes an SVG string and updates the model's 'icon' attribute the sanitized
       * string. Also sets the 'iconStatus' attribute to 'success'.
       * @param {string} icon An SVG string to use for the MapAsset icon
       */
      updateIcon(icon) {
        if (!icon) return;

        this.set("icon", IconUtilities.sanitizeIcon(icon));
        this.set("iconStatus", "success");
      },

      /**
       * Resets the Map Asset's status and statusDetails attributes to their default
       * values.
       * @since 2.21.0
       */
      resetStatus() {
        const defaults = this.defaults();
        this.set("status", defaults.status);
        this.set("statusDetails", defaults.statusDetails);
      },

      /**
       * Checks if the asset information has been fetched and is ready to use.
       * @returns {Promise} Returns a promise that resolves to this model when ready.
       */
      whenReady() {
        const model = this;
        return new Promise((resolve) => {
          if (model.get("status") === "ready") {
            resolve(model);
            return;
          }
          model.stopListening(model, "change:status");
          model.listenTo(model, "change:status", () => {
            if (model.get("status") === "ready") {
              model.stopListening(model, "change:status");
              resolve(model);
            }
          });
        });
      },

      /**
       * Given properties of a Feature model from this MapAsset, returns the color
       * associated with that feature.
       * @param {object} properties The properties of the feature to get the color for;
       * An object containing key-value mapping of property names to properties. (See
       * the 'properties' attribute of {@link Feature#defaults}.)
       * @returns {AssetColor#Color} The color associated with the given set of
       * properties.
       */
      getColor(properties) {
        const model = this;
        const colorPalette = model.get("colorPalette");
        return (
          colorPalette?.getColor(properties) ||
          new AssetColorPalette().getDefaultColor()
        );
      },

      /**
       * This function checks whether a feature from the MapAsset is visible on the map
       * based on the properties of the feature and the MapAsset's filter settings.
       * @param {object} properties The properties of the feature to be filtered. (See
       * the 'properties' attribute of {@link Feature#defaults}.)
       * @returns {boolean} Returns true if the feature passes all the filters, or if
       * there are no filters set for this MapAsset. Returns false if the feature fails
       * any of the filters.
       */
      featureIsVisible(properties) {
        const model = this;
        const filters = model.get("filters");
        if (filters && filters.length) {
          return filters.featureIsVisible(properties);
        }
        return true;
      },

      /**
       * Indicate that the map widget should navigate to a given target from
       * this MapAsset.
       * @param {MapConfig#CameraPosition} target The target to navigate to.
       * @since 2.27.0
       */
      zoomTo(target) {
        this.get("mapModel")?.zoomTo(target);
      },

      /**
       * Checks that the visible attribute is set to true and that the opacity attribute
       * is greater than zero. If both conditions are met, returns true.
       * @returns {boolean} Returns true if the MapAsset has opacity > 0 and is visible.
       */
      isVisible() {
        if (this.get("temporarilyHidden") === true) return false;
        return this.get("visible") && this.get("opacity") > 0;
      },

      /**
       * Make sure the layer is visible. Sets visibility to true if false, and sets
       * opacity to 0.5 if it's less than 0.05.
       */
      show() {
        // If the opacity is very low, set it to 50%
        if (this.get("opacity") < 0.05) {
          this.set("opacity", 0.5);
        }
        // Make sure the layer is visible
        if (this.get("visible") === false) {
          this.set("visible", true);
        }
      },
    },
  );

  return MapAsset;
});