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

'use strict';

define(
  [
    'jquery',
    'underscore',
    'backbone',
    'collections/maps/AssetColors'
  ],
  function (
    $,
    _,
    Backbone,
    AssetColors
  ) {
    /**
     * @classdesc An AssetColorPalette Model represents a color scale that is mapped to
     * some attribute of a Map Asset. For vector assets, like 3D tilesets, this palette is
     * used to conditionally color features on a map. For any type of asset, it can be
     * used to generate a legend.
     * @classcategory Models/Maps
     * @class AssetColorPalette
     * @name AssetColorPalette
     * @extends Backbone.Model
     * @since 2.18.0
     * @constructor
    */
    var AssetColorPalette = Backbone.Model.extend(
      /** @lends AssetColorPalette.prototype */ {

        /**
         * The name of this type of model
         * @type {string}
        */
        type: 'AssetColorPalette',

        /**
         * Default attributes for AssetColorPalette models
         * @name AssetColorPalette#defaults
         * @type {Object}
         * @property {('categorical'|'continuous'|'classified')}
         * [paletteType='categorical'] Set to 'categorical', 'continuous', or
         * 'classified'. NOTE: Currently only categorical and continuous palettes are
         * supported.
         * - Categorical: the color conditions will be interpreted such that one color
         *   represents a single value (e.g. a discrete palette).
         * - Continuous: each color in the colors attribute will represent a point in a
         *   gradient. The point in the gradient will be associated with the number set
         *   with the color, and numbers in between points will be set to an interpolated
         *   color.
         * - Classified: the numbers set in the colors attribute will be interpreted as
         *   maximums. Continuous properties will be forced into discrete bins. 
         * @property {string} property The name (ID) of the property in the asset layer's
         * attribute table to color the vector data by (or for imagery data that does not
         * have an attribute table, just the name of the attribute that these colors map
         * to).
         * @property {string} [label = null] A user-friendly name to display instead of
         * the actual property name.
         * @property {AssetColors} [colors = new AssetColors()] The colors to use in the
         * color palette, along with the conditions associated with each color (i.e. the
         * properties of the feature that must be true to use the given color.) . The last
         * color in the collection will always be treated as the default color - any
         * feature that doesn't match the other colors will be colored with this color.
        */
        defaults: function () {
          return {
            paletteType: 'categorical',
            property: null,
            label: null,
            colors: new AssetColors()
          }
        },

        /**
         * The ColorPaletteConfig specifies a color scale that is mapped to some attribute
         * of a {@link MapAsset}. For vector assets, like 3D tilesets, this palette is
         * used to conditionally color features on a map. For any type of asset, including
         * imagery, it can be used to generate a legend. The ColorPaletteConfig is passed
         * to a {@link AssetColorPalette} model.
         * @typedef {Object} ColorPaletteConfig
         * @name MapConfig#ColorPaletteConfig
         * @property {('categorical'|'continuous'|'classified')}
         * [paletteType='categorical'] NOTE: Currently only categorical and continuous
         * palettes are supported.
         * - Categorical: the color conditions will be interpreted such that one color
         *   represents a single value (e.g. a discrete palette).
         * - Continuous: each color in the colors attribute will represent a point in a
         *   gradient. The point in the gradient will be associated with the number set
         *   with the color, and numbers in between points will be set to an interpolated
         *   color.
         * - Classified: the numbers set in the colors attribute will be interpreted as
         *   maximums. Continuous properties will be forced into discrete bins. 
         * @property {string} property The name (ID) of the property in the asset layer's
         * attribute table to color the vector data by (or for imagery data that does not
         * have an attribute table, just the name of the attribute that these colors
         * represent).
         * @property {string} [label] A user-friendly name to display instead of the
         * actual property name.
         * @property {MapConfig#ColorConfig[]} colors The colors to use in the color
         * palette, along with the conditions associated with each color (i.e. the
         * properties of the feature that must be true to use the given color). The array
         * of ColorConfig objects are passed to a {@link AssetColors} collection, which in
         * turn passes each ColorConfig to a {@link AssetColor} model.
         *
         * @example
         * {
         *    paletteType: 'categorical',
         *    property: 'landUse',
         *    label: 'Land Use in 2016',
         *    colors: [
         *      { value: "agriculture", color: "#FF5733" },
         *      { value: "park", color: "#33FF80" }
         *    ]
         * }
        */

        /**
         * Executed when a new AssetColorPalette model is created.
         * @param {MapConfig#ColorPaletteConfig} [paletteConfig] The initial values of the
         * attributes, which will be set on the model.
         */
        initialize: function (paletteConfig) {
          try {
            if (paletteConfig && paletteConfig.colors) {
              this.set('colors', new AssetColors(paletteConfig.colors))
            }
            // If a continuous palette has only 1 colour, then treat it as categorical
            if (this.get('paletteType') === 'continuous' && this.get('colors').length === 1) {
              this.set('paletteType', 'categorical')
            }
          }
          catch (error) {
            console.log(
              'There was an error initializing a AssetColorPalette model' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Given properties of a feature, returns the color associated with that feature.
         * @param {Object} properties The properties of the feature to get the color for.
         * (See the 'properties' attribute of {@link Feature#defaults}.)
         * @returns {AssetColor#Color} The color associated with the given set of
         * properties.
         */
        getColor: function (properties) {

          const colorPalette = this;

          // As a backup, use the default color
          const defaultColor = this.getDefaultColor();
          let color = defaultColor

          // The name of the property to conditionally color the features by
          const prop = colorPalette.get('property');
          // The value for the this property in the given properties
          const propValue = properties[prop];
          // Each palette type has different ways of getting the color
          const type = colorPalette.get('paletteType');
          // The collection of colors + conditions 
          const colors = colorPalette.get('colors');

          if (!colors || colors.length === 0) {
            // Skip the other if statements, use default color.
          } else if (colors.length === 1) {
            // If there's just 1 color, then return that color.
            color = colors.at(0).get('color');
          } else if (type === 'categorical') {
            // For a categorical color palette, the value of the feature property just
            // needs to match one of the values in the list of color conditions.
            // If it matches, then return the color associated with that value.
            const colorMatch = colors.findWhere({ value: propValue });
            if (colorMatch) {
              color = colorMatch.get('color');
            }
          } else if (type === 'classified') {
            // TODO: test
            
            // For a classified color palette, the value of the feature property needs to
            // be greater than or equal to the value of the color condition. Use a
            // sorted array.
            
            // const sortedColors = colors.toArray().sort(function (a, b) {
            //   return a.get('value') - b.get('value')
            // })
            // let i = 0;
            // while (i < sortedColors.length && propValue >= sortedColors[i].get('value')) {
            //   i++;
            // }
            // color = sortedColors[i].get('color');
          } else if (type === 'continuous') {
            // For a continuous color palette, the value of the feature property must
            // either match one of the values in the color palette, or be interpolated
            // between the values in the palette.
            const sortedColors = colors.toArray().sort(function (a, b) {
              return a.get('value') - b.get('value')
            })
            let i = 0;
            while (i < sortedColors.length && propValue >= sortedColors[i].get('value')) {
              i++;
            }
            if (i === 0) {
              color = sortedColors[0].get('color');
            }
            else if (i === sortedColors.length) {
              color = sortedColors[i - 1].get('color');
            }
            else {
              const percent = (propValue - sortedColors[i - 1].get('value')) /
                (sortedColors[i].get('value') - sortedColors[i - 1].get('value'));
              color = colorPalette.interpolate(sortedColors[i - 1].get('color'), sortedColors[i].get('color'), percent)
            }
          }
          return color
        },

        /**
         * Given two colors, returns a color that is a linear interpolation between the
         * two colors.
         * @param {AssetColor#Color} color1 The first color.
         * @param {AssetColor#Color} color2 The second color.
         * @param {number} fraction The percentage of the way between the two colors, 0-1.
         * @returns {AssetColor#Color} The interpolated color.
         */
        interpolate: function (color1, color2, fraction) {
          const red = color1.red + fraction * (color2.red - color1.red)
          const green = color1.green + fraction * (color2.green - color1.green)
          const blue = color1.blue + fraction * (color2.blue - color1.blue)
          return {
            red: red,
            green: green,
            blue: blue
          }
        },

        /**
         * Gets the default color for the color palette, returns it as an object of RGB
         * intestines between 0 and 1.
         * @returns {AssetColor#Color} The default color for the palette.
         */
        getDefaultColor: function () {
          return this.get('colors').getDefaultColor().get('color');
        },

        // /**
        //  * 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 AssetColorPalette attributes
        //  */
        // parse: function (input) {

        //   try {

        //     var modelJSON = {};

        //     return modelJSON

        //   }
        //   catch (error) {
        //     console.log(
        //       'There was an error parsing a AssetColorPalette 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 {

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

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

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

      });

    return AssetColorPalette;

  }
);