Home Reference Source Repository

src/controls/TimeAxis.js

import L from 'leaflet'

import {$$, fromTemplate, inject, HTML} from './utils.js'
import {EventMixin} from '../util/EventMixin.js'

const DEFAULT_TEMPLATE_ID = 'template-coverage-timeaxis'
const DEFAULT_TEMPLATE = `<template id="${DEFAULT_TEMPLATE_ID}">
<div class="leaflet-coverage-control form-inline" style="clear:none">
  <strong class="title">Time</strong><br>
  <div class="form-group">
    <select name="date" class="date form-control"></select>
  </div>
  <div class="form-group">
    <select name="time" class="time form-control"></select>
  </div>
</div>
</template>`

/**
 * The `change` event, signalling that a different time entry has been selected.
 * 
 * @typedef {L.Event} TimeAxis#change
 * @property {Date} time The time that has been selected.
 */

/**
 * Displays a simple date/time picker for a coverage data layer by grouping
 * time steps into dates and times.
 * 
 * @example <caption>Coverage data layer</caption>
 * new C.TimeAxis(covLayer).addTo(map)
 * // Selecting a date/time automatically sets the 'time' property in the layer.
 * // Similarly, when the layer fires an 'axisChange' event with {axis: 'time'}
 * // the control reflects that change.
 * 
 * @example <caption>Fake layer</caption>
 * var times = ['2000-01-01T00:00:00Z','2000-01-01T05:00:00Z'].map(s => new Date(s))
 * var fakeLayer = {
 *   timeSlices: times,
 *   time: times[1] // select the second time step initially
 * }
 * var timeAxis = new C.TimeAxis(fakeLayer).addTo(map)
 * 
 * // change the time and trigger a manual update
 * fakeLayer.time = times[0]
 * timeAxis.update()
 * 
 * @extends {L.Control}
 * @extends {EventMixin}
 * 
 * @emits {TimeAxis#change} when a different time entry has been selected
 */
export class TimeAxis extends EventMixin(L.Control) {
  
  /**
   * Creates a time axis control.
   * 
   * @param {object} covLayer 
   *   The coverage data layer, or any object with `timeSlices` and `time` properties.
   *   If the object has `on`/`off` methods, then the control will
   *   listen for `axisChange` events with `{axis: 'time'}` and update itself automatically.
   *   If the layer fires a `remove` event, then the control will remove itself from the map.
   * @param {object} [options] Control options.
   * @param {string} [options.position='topleft'] The initial position of the control (see Leaflet docs).
   * @param {string} [options.title='Time'] The label to show above the date/time picker.
   * @param {string} [options.templateId] Element ID of an alternative HTML `<template>` element to use.
   */
  constructor (covLayer, options = {}) {
    super({position: options.position || 'topleft'})
    this._templateId = options.templateId || DEFAULT_TEMPLATE_ID
    this._title = options.title || 'Time'
    this._covLayer = covLayer

    if (!options.templateId && document.getElementById(DEFAULT_TEMPLATE_ID) === null) {
      inject(DEFAULT_TEMPLATE)
    } 

    if (covLayer.on) {
      this._remove = () => this.remove()
      covLayer.on('remove', this._remove)
      
      this._axisListener = e => {
        if (e.axis === 'time') this.update()
      }
    }
    
    let timeSlices = this._covLayer.timeSlices
    let dateMap = new Map() // UTC timestamp (representing the date only) -> array of Date objects
    for (let t of timeSlices) {
      let dateTimestamp = new Date(Date.UTC(t.getUTCFullYear(), t.getUTCMonth(), t.getUTCDate())).getTime()
      if (!dateMap.has(dateTimestamp)) {
        dateMap.set(dateTimestamp, [])
      }
      dateMap.get(dateTimestamp).push(t)
    }
    this._dateMap = dateMap
  }
  
  /**
   * @ignore
   */
  onAdd (map) {
    this._map = map
    
    if (this._covLayer.on) {
      this._covLayer.on('axisChange', this._axisListener)
    }
    
    let el = fromTemplate(this._templateId)
    this._el = el
    L.DomEvent.disableClickPropagation(el)
    
    if (this._title) {
      $$('.title', el).innerHTML = this._title
    }
    
    for (let dateTimestamp of this._dateMap.keys()) {
      let dateStr = getUTCDateString(dateTimestamp)
      $$('.date', el).appendChild(HTML(`<option value="${dateStr}">${dateStr}</option>`))
    }
    $$('.date', el).disabled = this._dateMap.size === 1
    
    $$('.date', el).addEventListener('change', event => {
      let dateTimestamp = getUTCTimestampDateOnly(event.target.value)
      let timeSlice = this._dateMap.get(dateTimestamp)[0]
      this._covLayer.time = timeSlice
      this._initTimeSelect(dateTimestamp)
      this.fire('change', {time: timeSlice})
    })
    $$('.time', el).addEventListener('change', event => {
      let dateStr = $$('.date', el).value
      let timeStr = event.target.value
      let time = new Date(dateStr + 'T' + timeStr)
      this._covLayer.time = time
      this.fire('change', {time: time})
    })
    
    this.update()
    
    return el
  }
  
  /**
   * @ignore
   */
  onRemove () {
    if (this._covLayer.off) {
      this._covLayer.off('remove', this._remove)
      this._covLayer.off('axisChange', this._axisListener)
    }
  }
  
  /**
   * Triggers a manual update of the date/time picker based on the
   * `time` property of the layer.
   * 
   * Useful if the supplied coverage data layer is not a real layer
   * and won't fire the necessary events for automatic updates.
   */
  update () {
    let covTime = this._covLayer.time
    if (!covTime) return
    let el = this._el
    // selects the date set in the cov layer, populates the time select, and selects the time
    let dateTimestamp = getUTCTimestampDateOnly(covTime.toISOString())
    let dateStr = getUTCDateString(dateTimestamp)
    $$('.date', el).value = dateStr 
    
    this._initTimeSelect(dateTimestamp)
    
    let timeStr = covTime.toISOString().substr(11)
    $$('.time', el).value = timeStr
  }
  
  _initTimeSelect (dateTimestamp) {
    let el = this._el
    let timeSelect = $$('.time', el)
    timeSelect.innerHTML = ''
    let times = this._dateMap.get(dateTimestamp)
    for (let timeSlice of times) {
      let timeStr = timeSlice.toISOString().substr(11)
      timeSelect.appendChild(HTML(`<option value="${timeStr}">${timeStr}</option>`))
    }
    timeSelect.disabled = times.length === 1
  }
    
}

function getUTCTimestampDateOnly (dateStr) {
  let year = parseInt(dateStr.substr(0, 4))
  let month = parseInt(dateStr.substr(5, 2))
  let day = parseInt(dateStr.substr(8, 2))
  return Date.UTC(year, month-1, day)
}

function getUTCDateString (timestamp) {
  let iso = new Date(timestamp).toISOString()
  let date = iso.substr(0, 10)
  return date
}