Home Reference Source Repository

src/controls/ContinuousLegend.js

import L from 'leaflet'

import {inject, fromTemplate, $$, $} from './utils.js'
import {getLanguageTag, getLanguageString, stringifyUnit} from 'covutils'

const DEFAULT_TEMPLATE_ID = 'template-coverage-parameter-continuous-legend'
const DEFAULT_TEMPLATE = `<template id="${DEFAULT_TEMPLATE_ID}">
  <div class="leaflet-coverage-control legend continuous-legend">
    <div style="margin-bottom:3px" class="legend-title-container">
      <strong class="legend-title"></strong>
    </div>
    <div style="display: inline-block; height: 144px; float:left">
      <span style="height: 136px; width: 18px; display: block; margin-top: 9px;" class="legend-palette"></span>
    </div>
    <div style="display: inline-block; float:left; height:153px">
      <table style="height: 100%;">
        <tr><td style="vertical-align:top"><span class="legend-max"></span> <span class="legend-uom"></span></td></tr>
        <tr><td><span class="legend-current"></span></td></tr>
        <tr><td style="vertical-align:bottom"><span class="legend-min"></span> <span class="legend-uom"></span></td></tr>
      </table>
    </div>
  </div>
</template>`

/**
 * Displays a continuous legend for the parameter displayed by the given
 * coverage data layer.
 * 
 * Note that this class should only be used if the palette is continuous
 * by nature, typically having at least 100-200 color steps.
 * If there are only a few color steps (e.g. 10), then this class
 * will still show a continuous legend due to its rendering technique
 * (CSS gradient based).
 * 
 * @example <caption>Coverage data layer</caption>
 * new C.ContinuousLegend(covLayer).addTo(map)
 * // changing the palette of the layer automatically updates the legend 
 * covLayer.palette = C.linearPalette(['blue', 'red'])
 * 
 * @example <caption>Fake layer</caption>
 * var fakeLayer = {
 *   parameter: {
 *     observedProperty: {
 *       label: { en: 'Temperature' }
 *     },
 *     unit: {
 *       symbol: { value: 'K' },
 *       label: { en: 'Kelvin' }
 *     }
 *   },
 *   palette: linearPalette(['#FFFFFF', '#000000']),
 *   paletteExtent: [0, 10]
 * }
 * var legend = new C.ContinuousLegend(fakeLayer).addTo(map)
 * 
 * // change the palette and trigger a manual update
 * fakeLayer.palette = C.linearPalette(['blue', 'red'])
 * legend.update()
 * 
 * @extends {L.Control}
 */
export class ContinuousLegend extends L.Control {
  
  /**
   * Creates a continuous legend control.
   * 
   * @param {object} covLayer 
   *   The coverage data layer, or any object with <code>palette</code>,
   *   <code>paletteExtent</code>, and <code>parameter</code> properties.
   *   If the object has <code>on</code>/<code>off</code> methods, then the legend will
   *   listen for <code>"paletteChange"</code> and <code>"paletteExtentChange"</code>
   *   events and update itself automatically.
   *   If the layer fires a <code>"remove"</code> event, then the legend will remove itself
   *   from the map. 
   * @param {object} [options] Legend options.
   * @param {string} [options.position='bottomright'] The initial position of the control (see Leaflet docs).
   * @param {string} [options.language] A language tag, indicating the preferred language to use for labels.
   * @param {string} [options.templateId] Uses the HTML element with the given id as template.
   */
  constructor (covLayer, options = {}) {
    super({position: options.position || 'bottomright'})
    this._covLayer = covLayer
    this._templateId = options.templateId || DEFAULT_TEMPLATE_ID
    this._language = options.language
    
    if (!options.templateId && document.getElementById(DEFAULT_TEMPLATE_ID) === null) {
      inject(DEFAULT_TEMPLATE)
    }   

    if (covLayer.on) {
      this._remove = () => this.remove()
      this._update = () => this._doUpdate(false)
      covLayer.on('remove', this._remove)
    }
  }
  
  /**
   * Triggers a manual update of the legend.
   * 
   * Useful if the supplied coverage data layer is not a real layer
   * and won't fire the necessary events for automatic updates.
   */
  update () {
    this._doUpdate(true)
  }
  
  _doUpdate (fullUpdate) {
    let el = this._el
    
    if (fullUpdate) {
      let param = this._covLayer.parameter
      // if requested language doesn't exist, use the returned one for all other labels
      let language = getLanguageTag(param.observedProperty.label, this._language) 
      let title = getLanguageString(param.observedProperty.label, language)
      let unit = stringifyUnit(param.unit, language)
       $$('.legend-title', el).innerHTML = title
       $('.legend-uom', el).forEach(u => u.innerHTML = unit)        
    }
    
    let palette = this._covLayer.palette
    let [low,high] = this._covLayer.paletteExtent
    
    $$('.legend-min', el).innerHTML = low.toFixed(2)
    $$('.legend-max', el).innerHTML = high.toFixed(2)

    let gradient = ''
    for (let i = 0; i < palette.steps; i++) {
      if (i > 0) gradient += ','
      gradient += 'rgb(' + palette.red[i] + ',' + palette.green[i] + ',' + palette.blue[i] + ')'
    }
    
    $$('.legend-palette', el).style.background = 'transparent linear-gradient(to top, ' + gradient + ') repeat scroll 0% 0%'
  }
  
  /**
   * @override
   * @ignore
   */
  onAdd (map) {
    this._map = map
    
    if (this._covLayer.on) {
      this._covLayer.on('paletteChange', this._update)
      this._covLayer.on('paletteExtentChange', this._update)
    }
    
    this._el = fromTemplate(this._templateId)
    this.update()
    return this._el
  }
  
  /**
   * @override
   * @ignore
   */
  onRemove () {
    if (this._covLayer.off) {
      this._covLayer.off('remove', this._remove)
      this._covLayer.off('paletteChange', this._update)
      this._covLayer.off('paletteExtentChange', this._update)
    }
  }
  
}