# Source: src/js/views/maps/LegendView.js

    'use strict';

    define(
      [
        'jquery',
        'underscore',
        'backbone',
        'd3',
        'models/maps/AssetColorPalette',
        'text!templates/maps/legend.html',
      ],
      function (
        $,
        _,
        Backbone,
        d3,
        AssetColorPalette,
        Template
      ) {

        /**
        * @class LegendView
        * @classdesc Creates a legend for a given Map Asset (Work In Progress). Currently
        * supports making 'preview' legends for CesiumImagery assets and Cesium3DTileset
        * assets (only for color palettes that are type 'categorical'). Eventually, will
        * support full-sized legend for these, and other assets, and all types of color
        * palettes (including 'continuous' and 'classified')
        * @classcategory Views/Maps
        * @name LegendView
        * @extends Backbone.View
        * @screenshot views/maps/LegendView.png
        * @since 2.18.0
        * @constructs
        */
        var LegendView = Backbone.View.extend(
          /** @lends LegendView.prototype */{

            /**
            * The type of View this is
            * @type {string}
            */
            type: 'LegendView',

            /**
            * The HTML classes to use for this view's element
            * @type {string}
            */
            className: 'map-legend',

            /**
            * The MapAsset model that this view uses - currently supports CesiumImagery and
            * Cesium3DTileset models.
            * @type {MapAsset}
            */
            model: null,

            /**
             * The primary HTML template for this view
             * @type {Underscore.template}
             */
            template: _.template(Template),

            /**
            * The events this view will listen to and the associated function to call.
            * @type {Object}
            */
            events: {
              // 'event selector': 'function',
            },

            /**
             * Which type of legend to show? Can be set to either 'full' for a complete legend
             * with labels, title, and all color coding, or 'preview' for just a small
             * thumbnail of the colors used in the full legend.
             * @type {string}
             */
            mode: 'preview',

            /**
             * For vector preview legends, the relative dimensions to use. The SVG's
             * dimensions are set with a viewBox property only, so the height and width
             * represent an aspect ratio rather than absolute size.
             * @type {Object}
             * @property {number} previewSvgDimensions.width - The width of the entire SVG
             * @property {number} previewSvgDimensions.height - The height of the entire SVG
             * @property {number} squareSpacing - Maximum spacing between each of the squares
             * in the preview legend. Squares will be spaced 20% closed than this when the
             * legend is not hovered over.
             */
            previewSvgDimensions: {
              width: 160,
              height: 45,
              squareSpacing: 20
            },

            /**
             * Classes that are used to identify, or that are added to, the HTML elements that
             * comprise this view.
             * @type {Object}
             * @property {string} preview Additional class to add to legend that are the
             * preview/thumbnail version
             * @property {string} previewSVG The SVG element that holds the shapes with all
             * the legend colours in the preview legend.
             * @property {string} previewImg The image element that represents a thumbnail of
             * image layers, in preview legends
             * @property {string} tooltip Class added to tooltips used in preview legends
             */
            classes: {
              preview: 'map-legend--preview',
              previewSVG: 'map-legend__svg--preview',
              previewImg: 'map-legend__img--preview',
              tooltip: 'map-tooltip',
            },

            /**
            * Executed when a new LegendView is created
            * @param {Object} [options] - A literal object with options to pass to the view
            */
            initialize: function (options) {

              try {
                // Get all the options and apply them to this view
                if (typeof options == 'object') {
                  for (const [key, value] of Object.entries(options)) {
                    this[key] = value;
                  }
                }
              } catch (e) {
                console.log('A LegendView failed to initialize. Error message: ' + e);
              }

            },

            /**
            * Renders this view
            * @return {LegendView} Returns the rendered view element
            */
            render: function () {

              try {

                // Save a reference to this view
                var view = this;

                // The color palette maps colors to attributes of the map asset
                let colorPalette = null;
                // For color palettes,
                let paletteType = null;
                const mode = this.mode;

                // Insert the template into the view
                this.$el.html(this.template({}));

                // Ensure the view's main element has the given class name
                this.el.classList.add(this.className);

                // Add a modifier class if this is a preview of a legend
                if (mode === 'preview') {
                  this.el.classList.add(this.classes.preview);
                }

                // Check for a color palette model in the Map Asset model. Even imagery layers
                // may have a color palette configured, specifically to use to create a
                // legend.
                for (const attr in this.model.attributes) {
                  if (this.model.attributes[attr] instanceof AssetColorPalette) {
                    colorPalette = this.model.get(attr);
                    paletteType = colorPalette.get('paletteType')
                  }
                }

                if (mode === 'preview') {
                  // For categorical vector color palettes, in preview mode
                  if (colorPalette && paletteType === 'categorical') {
                    this.renderCategoricalPreviewLegend(colorPalette)
                  } else if (colorPalette && paletteType === 'continuous') {
                    this.renderContinuousPreviewLegend(colorPalette)
                  }
                  // For imagery layers that do not have a color palette, in preview mode
                  else if (typeof this.model.getThumbnail === 'function') {
                    if (!this.model.get('thumbnail')) {
                      this.listenToOnce(this.model, 'change:thumbnail', function () {
                        this.renderImagePreviewLegend(this.model.get('thumbnail'))
                      })
                    } else {
                      this.renderImagePreviewLegend(this.model.get('thumbnail'))
                    }
                  }
                }
                // TODO:
                // - preview classified legend
                // - full legends with labels, title, etc.

                return this

              }
              catch (error) {
                console.log(
                  'There was an error rendering a Legend View' +
                  '. Error details: ' + error
                );
              }
            },

            /**
             * Inserts a thumbnail in image into this view
             * @param {string} thumbnailURL A url to use for the src property of the thumbnail
             * image
             */
            renderImagePreviewLegend: function (thumbnailURL) {
              try {
                const img = new Image()
                img.src = thumbnailURL
                img.classList.add(this.classes.previewImg)
                this.el.append(img)
              }
              catch (error) {
                console.log(
                  'There was an error rendering an image preview legend in a LegendView' +
                  '. Error details: ' + error
                );
              }
            },

            /**
             * Creates a preview legend for categorical color palettes and inserts it into the
             * view
             * @param {AssetColorPalette} colorPalette - The AssetColorPalette that maps
             * feature attributes to colors, used to create the legend
             */
            renderCategoricalPreviewLegend: function (colorPalette) {
              try {
                if (!colorPalette) {
                  return
                }
                const view = this
                // Data to use in d3
                let data = colorPalette.get('colors').toJSON().reverse();
                // The max width of the SVG, to be reduced if there are few colours
                let width = this.previewSvgDimensions.width
                // The height of the SVG
                const height = this.previewSvgDimensions.height
                // Height and width of the square is the height of the SVG, leaving some room
                // for shadow to show
                const squareSize = height * 0.92
                // Maximum spacing between squares. When not hovered, the squares will be
                // spaced 80% of this value.
                let squareSpacing = this.previewSvgDimensions.squareSpacing
                // The maximum number of squares that can fit on the SVG without any spilling
                // over
                const maxNumSquares = Math.floor(((width - squareSize) / squareSpacing) + 1)

                // If there are more colors than fit in the max width of the SVG space, only
                // show the first n squares that will fit
                if (data.length > maxNumSquares) {
                  data = data.slice(0, maxNumSquares);
                }
                // Add index to data for sorting later (also works as unique ID)
                data.forEach(function (d, i) {
                  d.i = i;
                });

                // Don't create an SVG that is wider than it need to be.
                width = squareSize + ((data.length - 1) * squareSpacing)

                // SVG element
                const svg = this.createSVG({
                  dropshadowFilter: true,
                  width: width,
                  height: height,
                })

                // Add the preview class and dropshadow to the SVG
                svg.classed(this.classes.previewSVG, true)
                svg.style('filter', 'url(#dropshadow)')

                // Calculates the placement of the square along x-axis, when SVG is hovered
                // and when it's not
                function getSquareX(i, hovered) {
                  const multiplier = hovered ? 1 : 0.8;
                  return ((width - squareSize) - (i * (squareSpacing * multiplier)))
                }

                // Draw the legend (d3)
                const legendSquares = svg.selectAll('rect')
                  .data(data)
                  .enter()
                  .append('rect')
                  .attr('x', function (d, i) { return getSquareX(i, false) })
                  .attr('height', squareSize)
                  .attr('width', squareSize)
                  .attr('rx', (squareSize * 0.1))
                  .style('fill', function (d) {
                    return `rgb(${d.color.red * 255},${d.color.green * 255},${d.color.blue * 255})`
                  })
                  .style('filter', 'url(#dropshadow)')

                // For legend with multiple colours, show a tooltip with the value/label when
                // the user hovers over a square. Also bring that square to the fore-front of
                // the legend when hovered. Only when MapAsset is visible though.
                if (data.length > 1) {

                  // Space the squares further apart when they are hovered over
                  svg
                    .on('mouseenter', function () {
                      if (view.model.get('visible')) {
                        legendSquares
                          .transition()
                          .duration(250)
                          .attr('x', function (d, i) { return getSquareX(i, true) })
                      }
                    })
                    .on('mouseleave', function () {
                      legendSquares
                        .transition()
                        .duration(200)
                        .attr('x', function (d, i) { return getSquareX(i, false) })
                    })

                  legendSquares.on('mouseenter', function (d) {
                    if (view.model.get('visible')) {
                      // Bring the hovered element to the front, while keeping other
                      // legendSquares in order
                      legendSquares.sort((a, b) => d3.ascending(a.i, b.i));
                      this.parentNode.appendChild(this)
                      // Show tooltip
                      if (d.label || d.value || d.value === 0) {
                        $(this).tooltip({
                          placement: 'bottom',
                          trigger: 'manual',
                          title: d.label || d.value,
                          container: view.$el,
                          animation: false,
                          template: '<div class="tooltip ' + view.classes.tooltip + '"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
                        }).tooltip('show')
                      }
                    }
                  })
                    // Hide tooltip and return squares to regular z-ordering
                    .on('mouseleave', function (d) {
                      $(this).tooltip('destroy');
                      legendSquares.sort((a, b) => d3.ascending(a.i, b.i));
                    })
                }
              }
              catch (error) {
                console.log(
                  'There was an error creating a categorical legend preview in a LegendView' +
                  '. Error details: ' + error
                );
              }
            },

            /**
             * Creates a preview legend for continuous color palettes and inserts it into the
             * view
             * @param {AssetColorPalette} colorPalette - The AssetColorPalette that maps
             * feature attributes to colors, used to create the legend
             */
            renderContinuousPreviewLegend: function (colorPalette) {
              try {
                if (!colorPalette) {
                  return
                }
                const view = this
                // Data to use in d3
                let data = colorPalette.get('colors').toJSON();
                // The max width of the SVG
                let width = this.previewSvgDimensions.width
                // The height of the SVG
                const height = this.previewSvgDimensions.height
                // Height of the gradient rectangle, leaving some room for the drop shadow
                const gradientHeight = height * 0.92

                // A unique ID for the gradient
                const gradientId = 'gradient-' + view.cid;

                // Calculate the rounding precision we should use based on the
                // range of the data. This determines how each value in the legend
                // is displayed in the tooltip on mouseover. See the
                // rect.on('mousemove'... function, below
                data = data.sort((a, b) => a.value - b.value);
                const min = data[0].value
                const max = data[data.length - 1].value
                const range = max - min
                let roundingConstant = 10 // Allow 1 decimal place by default
                if (range < 0.0001 || range > 100000) {
                  roundingConstant = null // Will use scientific notation
                } else if (range < 0.001) {
                  roundingConstant = 100000 // Allow 5 decimal places
                } else if (range < 0.01) {
                  roundingConstant = 10000 // Allow 4 decimal places
                } else if (range < 0.1) {
                  roundingConstant = 1000 // Allow 3 decimal places
                } else if (range < 1) {
                  roundingConstant = 100 // Allow 2 decimal places
                } else if (range > 100) {
                  roundingConstant = 1 // No decimal places
                }

                // SVG element
                const svg = this.createSVG({
                  dropshadowFilter: false,
                  width: width,
                  height: height,
                })

                // Add the preview class and dropshadow to the SVG
                svg.classed(this.classes.previewSVG, true)
                svg.style('filter', 'url(#dropshadow)')

                // Create a gradient using the data
                const gradient = svg.append('defs')
                  .append('linearGradient')
                  .attr('id', gradientId)
                  .attr('x1', '0%')
                  .attr('y1', '0%')

                var getOffset = function (d, data) {
                  return (d.value - min) / (range) * 100 + '%'
                }
                var getStopColor = function (d) {
                  const r = d.color.red * 255
                  const g = d.color.green * 255
                  const b = d.color.blue * 255
                  return `rgb(${r},${g},${b})`
                }

                // Add the gradient stops
                data.forEach(function (d, i) {
                  gradient.append('stop')
                    // offset should be relative to the value in the data
                    .attr('offset', getOffset(d, data))
                    .attr('stop-color', getStopColor(d))
                })

                // Create the rectangle
                const rect = svg.append('rect')
                  .attr('x', 0)
                  .attr('y', 0)
                  .attr('width', width)
                  .attr('height', gradientHeight)
                  .attr('rx', (gradientHeight * 0.1))
                  .style('fill', 'url(#' + gradientId + ')')

                // Create a proxy element to attach the tooltip to, so that we can move the
                // tooltip to follow the mouse (by moving the proxy element to follow the mouse)
                const proxyEl = svg.append('rect').attr('y', gradientHeight)

                rect.on('mousemove', function () {
                  if (view.model.get('visible')) {
                    // Get the coordinates of the mouse relative to the rectangle
                    let xMouse = d3.mouse(this)[0];
                    if (xMouse < 0) {
                      xMouse = 0;
                    }
                    if (xMouse > width) {
                      xMouse = width;
                    }
                    // Get the relative position of the mouse to the gradient
                    const relativePosition = xMouse / width;
                    // Get the value at the relative position by interpolating the data
                    let value = d3.interpolate(data[0].value, data[data.length - 1].value)(relativePosition);
                    // Show tooltip with the value
                    if (value || value === 0) {
                      // Round or show in scientific notation
                      if (roundingConstant) {
                        value = (Math.round(value * roundingConstant) / roundingConstant).toString()
                      } else {
                        value = value.toExponential(2).toString()
                      }
                      // Move the proxy element to follow the mouse
                      proxyEl.attr('x', xMouse)
                      // Attach the tooltip to the proxy element. Tooltip needs to be
                      // refreshed every time the mouse moves
                      $(proxyEl).tooltip('destroy');
                      $(proxyEl).tooltip({
                        placement: 'bottom',
                        trigger: 'manual',
                        title: value,
                        container: view.$el,
                        animation: false,
                        template: '<div class="tooltip ' + view.classes.tooltip + '"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
                      }).tooltip('show')
                    }
                  }
                })
                  // Hide tooltip
                  .on('mouseleave', function () {
                    $(proxyEl).tooltip('destroy');
                  })

              }
              catch (error) {
                console.log(
                  'There was an error rendering a continuous preview legend in a LegendView' +
                  '. Error details: ' + error
                );
              }
            },

            /**
             * Creates an SVG element and inserts it into the view
             * @param {object} options Used to configure parts of the SVG
             * @property {boolean} options.dropshadowFilter Set to true to create a filter
             * element that creates a dropshadow behind any element it is applied to. It can
             * be added to child elements of the SVG by setting a `filter: url(#dropshadow);`
             * style rule on the child.
             * @property {number} options.height The relative height of the SVG (for the
             * viewBox property)
             * @property {number} options.width The relative width of the SVG (for the viewBox
             * property)
             * @returns {SVG} Returns the SVG element that is in the view
             */
            createSVG: function (options = {}) {
              try {
                // Create an SVG to hold legend elements
                const container = this.el;
                const width = options.width;
                const height = options.height;

                const svg = d3.select(container)
                  .append('svg')
                  .attr('preserveAspectRatio', 'xMidYMid')
                  .attr('viewBox', [0, 0, width, height]);

                if (options.dropshadowFilter) {

                  const filterText =
                  `<filter id="dropshadow" height="110%">
                    <feGaussianBlur in="SourceAlpha" stdDeviation="2"/> <!-- stdDeviation is how much to blur -->
                    <feOffset dx="1" dy="1" result="offsetblur"/> <!-- how much to offset -->
                    <feComponentTransfer>
                      <feFuncA type="linear" slope="0.7"/> <!-- slope is the opacity of the shadow -->
                    </feComponentTransfer>
                    <feMerge>
                      <feMergeNode/> <!-- this contains the offset blurred image -->
                      <feMergeNode in="SourceGraphic"/> <!-- this contains the element that the filter is applied to -->
                    </feMerge>
                  </filter>`

                  const filterEl = new DOMParser().parseFromString(
                    '<svg xmlns="http://www.w3.org/2000/svg">' + filterText + '</svg>',
                    'application/xml'
                  ).documentElement.firstChild

                  svg.node().appendChild(document.importNode(filterEl, true))
                }

                return svg
              }
              catch (error) {
                console.log(
                  'There was an error creating an SVG in a LegendView' +
                  '. Error details: ' + error
                );
              }
            }

          }
        );

        return LegendView;

      }
    );