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

'use strict';

define(
  [
    'jquery',
    'underscore',
    'backbone',
    'models/portals/PortalImage',
    'models/maps/AssetColorPalette',
    MetacatUI.root + '/components/dayjs.min.js'
  ],
  function (
    $,
    _,
    Backbone,
    PortalImage,
    AssetColorPalette,
    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
     * @extends Backbone.Model
     * @since 2.18.0
     * @constructor
    */
    var 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 = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M1.2 13.7 3.6 3.5l7.2 8.7zM5 3.3c2 .3 3.5.4 4.4.4H13l3 6.3-4 1.8-7-8.5Zm9.1.6 2.8 6 4.5-2.2a23 23 0 0 1-.7-4c0-.8-1.2-.7-2-.7l-4.6.9ZM1 15l11-1.9c1.2-.3 2.7-1 4.2-2l2.5 5.2c-1.2.3-3.6.4-7.3.4-2.8 0-7.3 0-9.4-.8a2 2 0 0 1-1-.9Zm16.4-4.2 4-1.8 1.3 5.5c0 .4.1 1-.4 1.2l-2.5.5-2.4-5.4ZM1.7 17l-.5 2.6c0 .7.2 1 1 1.3a64.1 64.1 0 0 0 19.8 0c.4-.3 1-.5.7-1.6l-.5-2.3a52.6 52.6 0 0 1-20.6 0Z"/></svg>']
         * 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 {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 {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 {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 {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.
        */
        defaults: function () {
          return {
            type: '',
            label: '',
            icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M1.2 13.7 3.6 3.5l7.2 8.7zM5 3.3c2 .3 3.5.4 4.4.4H13l3 6.3-4 1.8-7-8.5Zm9.1.6 2.8 6 4.5-2.2a23 23 0 0 1-.7-4c0-.8-1.2-.7-2-.7l-4.6.9ZM1 15l11-1.9c1.2-.3 2.7-1 4.2-2l2.5 5.2c-1.2.3-3.6.4-7.3.4-2.8 0-7.3 0-9.4-.8a2 2 0 0 1-1-.9Zm16.4-4.2 4-1.8 1.3 5.5c0 .4.1 1-.4 1.2l-2.5.5-2.4-5.4ZM1.7 17l-.5 2.6c0 .7.2 1 1 1.3a64.1 64.1 0 0 0 19.8 0c.4-.3 1-.5.7-1.6l-.5-2.3a52.6 52.6 0 0 1-20.6 0Z"/></svg>',
            description: '',
            attribution: '',
            moreInfoLink: '',
            downloadLink: '',
            id: '',
            selected: false,
            opacity: 1,
            visible: true,
            colorPalette: null,
            customProperties: {},
            featureTemplate: {},
            notification: {},
            status: null,
            statusDetails: null
          }
        },

        /**
         * 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')} 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 {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.
        */

        /**
         * 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}
         * @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 {Object.<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: function (assetConfig) {
          try {

            const model = this;

            if (!assetConfig || typeof assetConfig !== 'object') {
              assetConfig = {}
            }

            // Set the color palette
            if (assetConfig.colorPalette) {
              this.set('colorPalette', new AssetColorPalette(assetConfig.colorPalette))
            }

            // The map asset cannot be visible on the map if there was an error loading
            // the asset
            this.listenTo(this, 'change:status', function (model, status) {
              if (status === 'error') {
                this.set('visible', false)
              }
            })
            this.listenTo(this, 'change:visible', function (model, visible) {
              if (this.get('status') === 'error') {
                this.set('visible', false)
              }
            })

            // Fetch the icon, if there is one
            if (assetConfig.icon) {
              if (model.isSVG(assetConfig.icon)) {
                model.updateIcon(assetConfig.icon)
              } else {
                // If the string is not an SVG then assume it is a PID and try to fetch
                // the SVG file. fetchIcon will update the icon when complete.
                model.fetchIcon(assetConfig.icon)
              }
            }

            // Update the style of the asset to highlight the selected features when
            // features from this asset are selected in the map.
            if (typeof this.updateAppearance === 'function') {

              const setSelectFeaturesListeners = function () {

                const mapModel = this.get('mapModel')
                if (!mapModel) { return }
                const selectedFeatures = mapModel.get('selectedFeatures')

                this.stopListening(selectedFeatures, 'update');
                this.listenTo(selectedFeatures, 'update', this.updateAppearance)

                this.stopListening(mapModel, 'change:selectedFeatures')
                this.listenTo(mapModel, 'change:selectedFeatures', function () {
                  this.updateAppearance()
                  setSelectFeaturesListeners()
                })

              }
              setSelectFeaturesListeners.call(this)
              this.listenTo(this, 'change:mapModel', setSelectFeaturesListeners)
              this.listenTo(this, 'change:mapModel', setSelectFeaturesListeners)
            }
          }
          catch (error) {
            console.log(
              'There was an error initializing a MapAsset model' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Given a feature object from a Feature model, checks if it is part of the
         * selectedFeatures collection. See featureObject property from
         * {@link Feature#defaults}.
         * @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: function (feature) {
          const map = this.get('mapModel')
          if (!map) { return false }
          return map.get('selectedFeatures').containsFeature(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: function (properties) {
          try {

            const model = this;
            const customProperties = model.get('customProperties');
            const formattedProperties = {};

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

            if (!properties || typeof properties !== 'object') {
              properties = {}
            }

            if (customProperties) {
              _.each(customProperties, function (config, key) {
                let formattedValue = '';
                if (config.type === 'date') {
                  formattedValue = model.formatDateProperty(config, properties)
                  // 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, properties)
                }
                formattedProperties[key] = formattedValue;
              });
            }
            // merge the properties with the formatted properties
            return Object.assign(properties, formattedProperties);
          } catch (error) {
            console.log(
              'There was an error adding custom properties. Returning properties ' +
              'unchanged. Error details: ' +
              error
            );
            return properties
          }
        },

        /**
         * 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: function (config, properties) {
          try {
            if (!properties) {
              properties = {}
            }
            let formattedDate = ''
            if (!config || !config.format) {
              return formattedDate;
            }
            const value = properties[config.property];
            if (value) {
              formattedDate = dayjs(value).format(config.format);
            }
            return formattedDate;
          }
          catch (error) {
            console.log(
              'There was an error formatting a date for a Feature model' +
              '. Error details: ' + error
            );
            return '';
          }
        },

        /**
         * 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
         * @param {Object} properties key-value pairs representing existing properties in
         * a Feature
         * @returns {string} The new string for the given Feature property
         */
        formatStringProperty: function (config, properties) {
          try {
            if (!properties) {
              properties = {}
            }
            let formattedString = ''
            if (!config || !config.value) {
              return formattedString;
            }
            formattedString = config.value;
            return formattedString;
          }
          catch (error) {
            console.log(
              'There was an error formatting a string for a Feature model' +
              '. Error details: ' + error
            );
            return '';
          }
        },

        // 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: function (icon) {
          const model = this
          try {
            model.sanitizeIcon(icon, function (sanitizedIcon) {
              model.set('icon', sanitizedIcon)
              model.set('iconStatus', 'success')
            })
          }
          catch (error) {
            console.log(
              'There was an error updating an icon in a MapAsset model' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Simple test to see if a string is an SVG
         * @param {string} str The string to check
         * @returns {Boolean} Returns true if the string starts with `<svg` and ends with
         * `</svg>`, regardless of case
         */
        isSVG: function (str) {
          const strLower = str.toLowerCase()
          return strLower.startsWith('<svg') && strLower.endsWith('</svg>')
        },

        /**
         * Fetches an SVG given a pid, sanitizes it, then updates the model's icon
         * attribute with the new and SVG string (after sanitizing it)
         * @param {string} pid
         */
        fetchIcon: function (pid) {
          const model = this
          try {
            model.set('iconStatus', 'fetching')

            // Use the portal image model to get the correct baseURL for an image
            const imageURL = new PortalImage({
              identifier: pid
            }).get('imageURL')

            fetch(imageURL)
              .then(function (response) {
                return response.text();
              })
              .then(function (data) {
                if (model.isSVG(data)) {
                  model.updateIcon(data)
                }
              })
              .catch(function (response) {
                model.set('iconStatus', 'error')
              });
          }
          catch (error) {
            console.log(
              'Failed to fetch an icon for a MapAsset' +
              '. Error details: ' + error
            );
            model.set('iconStatus', 'error')
          }
        },

        /**
         * Takes an SVG string and returns it with only the allowed tags and attributes
         * @param {string} icon The SVG icon string to sanitize
         * @param {function} callback Function to call once the icon has been sanitized.
         * Will pass the sanitized icon string.
         */
        sanitizeIcon: function (icon, callback) {
          try {
            // Use the showdown xss filter to sanitize the SVG string
            require(['showdown', 'showdownXssFilter'], function (showdown, showdownXss) {
              var converter = new showdown.Converter({
                extensions: ['xssfilter']
              });
              let sanitizedIcon = converter.makeHtml(icon);
              // Remove the <p></p> tags that showdown wraps the string in
              sanitizedIcon = sanitizedIcon.replace(/^(<p>)/, '')
              sanitizedIcon = sanitizedIcon.replace(/(<\/p>)$/, '')
              // Call the callback
              if (callback && typeof callback === 'function') {
                callback(sanitizedIcon)
              }
            })
          }
          catch (error) {
            console.log(
              'There was an error sanitizing an SVG icon in a MapAsset model' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Resets the Map Asset's status and statusDetails attributes to their default
         * values.
         * @since X.X.X
         */
        resetStatus: function () {
          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: function () {
          const model = this;
          return new Promise(function (resolve, reject) {
            if (model.get('status') === 'ready') {
              resolve(model)
              return
            }
            model.stopListening(model, 'change:status')
            model.listenTo(model, 'change:status', function () {
              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: function (properties) {
          try {
            const model = this
            const colorPalette = model.get('colorPalette')
            if (colorPalette) {
              return colorPalette.getColor(properties)
            } else {
              return new AssetColorPalette().getDefaultColor()
            }
          }
          catch (error) {
            console.log(
              'There was an error getting a color for a MapAsset model' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * 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: function (properties) {
          const model = this
          const filters = model.get('filters')
          if (filters && filters.length) {
            return filters.featureIsVisible(properties)
          } else {
            return true
          }
        },

        /**
         * 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: function () {
          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: function () {
          // 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)
          }
        },

        // /**
        //  * Parses the given input into a JSON object to be set on the model.
        //  *
        //  * @param {TODO} input - The raw response object
        //  * @return {TODO} - The JSON object of all the MapAsset attributes
        //  */
        // parse: function (input) {

        //   try {

        //     var modelJSON = {};

        //     return modelJSON

        //   }
        //   catch (error) {
        //     console.log(
        //       'There was an error parsing a MapAsset model' +
        //       '. Error details: ' + error
        //     );
        //   }

        // },

        // /**
        //  * Overrides the default Backbone.Model.validate.function() to check if this if
        //  * the values set on this model are valid.
        //  *
        //  * @param {Object} [attrs] - A literal object of model attributes to validate.
        //  * @param {Object} [options] - A literal object of options for this validation
        //  * process
        //  *
        //  * @return {Object} - Returns a literal object with the invalid attributes and
        //  * their corresponding error message, if there are any. If there are no errors,
        //  * returns nothing.
        //  */
        // validate: function (attrs, options) {
        //   try {
        //     // Required attributes: type, url, label, description (all strings)
        //   }
        //   catch (error) {
        //     console.log(
        //       'There was an error validating a MapAsset model' +
        //       '. Error details: ' + error
        //     );
        //   }
        // },

        // /**
        //  * Creates a string using the values set on this model's attributes.
        //  * @return {string} The MapAsset string
        //  */
        // serialize: function () {
        //   try {
        //     var serializedMapAsset = '';

        //     return serializedMapAsset;
        //   }
        //   catch (error) {
        //     console.log(
        //       'There was an error serializing a MapAsset model' +
        //       '. Error details: ' + error
        //     );
        //   }
        // },

      });

    return MapAsset;

  }
);