Home Reference Source

src/layerSelector/LayerSelector.js

import $ from 'jquery'
import { Control } from '../controls/Control'
import { ButtonBox } from '../html/ButtonBox'
import { ListenerOrganizerMixin } from '../ListenerOrganizerMixin'
import { mixin, offset } from '../utilities'
import { isObject, isArray } from 'lodash/lang'

import '../../less/layerselector.less'
import { LayerButton } from './LayerButton'
import { GroupElement } from './GroupElement'
import { MultiButton } from './MultiButton'
import { WMSLayerButton } from './WMSLayerButton'

/**
 * @typedef {g4uControlOptions} LayerSelectorOptions
 * @property {boolean} [collapsible=true] if the menu should be collapsible
 * @property {boolean} [collapsed=false] if the menu starts collapsed
 * @property {number} [minVisibleEntries=6] amount of minimal visible elements
 * @property {string} menuName the name of the layerMenu this selector is connected to. For example 'baseLayers'
 * @property {number} [minLayerAmount=1] the minimum number of layers which should be visible to show this selector
 * @property {boolean} [checkboxes=false] if the layerbuttons should have trailing checkboxes
 */

/**
 * @typedef {object} ButtonConfig
 * @property {object} [window] open a window if the button gets clicked
 * @property {string} [class] add a css class to the button
 * @property {[]} [accordion] accordionmenu items
 */

/**
 * @typedef {ButtonConfig} LayerButtonConfig
 * @property {"layer"} type
 * @property {Localizable} [title] if not set, the title of the layer will be chosen
 * @property {string|number} refId the id of the layer
 * @property {string} exclusive all layers with the same exclusive value are mutually exclusive active
 */

/**
 * @typedef {ButtonConfig} WMSLayerButtonConfig
 * @property {"WMS"} type
 * @property {Localizable} title
 * @property {string|number} refId the id of the layer
 * @property {string[]} LAYERS
 * @property {string[]} [QUERY_LAYERS]
 * @property {boolean} [checked] if QUERY_LAYERS are set and checked is true, the featureInfo appears turned on.
 */

/**
 * @typedef {ButtonConfig} GroupButtonConfig
 * @property {"group"} type
 * @property {Localizable} title
 * @property {string} [groupButton="onlyMenu"] can be "noButton", "onlyMenu" or "activate"
 *    "noButton": the group itself will have no button, the contained buttons are displayed on the same level
 *    "onlyMenu": the group has a button which can be clicked to open/close the submenu with the contained layers
 *    "activate": the group has 2 buttons. One will open/close the submenu, the other will de-/activate all layers
 * @property {string} [items="normal"] can be "normal" or "exclusive"
 *    "normal": all contained layers can be activated independently
 *    "exclusive": activating one layer in this group will deactivate the others.
 * @property {boolean} [unselectable=true] specifies if a button can be unselected
 * @property {boolean} [collapsible=true]
 * @property {boolean} [collapsed=true]
  */

/**
 * @typedef {object} ButtonActions
 * @property toggleActive
 * @property toggleDisabled
 * @property toggleLoading
 */

export class LayerSelector extends mixin(Control, ListenerOrganizerMixin) {
  /**
   * @param {LayerSelectorOptions} options
   */
  constructor (options = {}) {
    options.className = options.className || 'g4u-layerselector'
    options.element = $('<div>')[0]
    options.singleButton = false

    super(options)

    /**
     * @type {string}
     * @private
     */
    this.menuName_ = options.menuName

    /**
     * @type {number}
     * @private
     */
    this.minLayerAmount_ = options.hasOwnProperty('minLayerAmount') ? options.minLayerAmount : 1

    /**
     * @type {boolean}
     * @private
     */
    this.collapsible_ = !options.hasOwnProperty('collapsible') || options.collapsible

    /**
     * @type {boolean}
     * @private
     */
    this.collapsed_ = options.collapsed || false

    /**
     * classNames
     * @type {object.<string, string>}
     * @protected
     */
    this.classNames_ = {
      menu: this.getClassName() + '-menu',
      layerButton: this.getClassName() + '-layerbutton',
      active: this.getClassName() + '-active',
      featureInfo: this.getClassName() + '-info',
      featureInfoActive: this.getClassName() + '-info-active',
      disabled: this.getClassName() + '-disabled',
      spacer: this.getClassName() + '-spacer',
      checkbox: this.getClassName() + '-checkbox',
      radio: this.getClassName() + '-radio'
    }

    /**
     * @type {ButtonBox}
     * @private
     */
    this.menu_ = new ButtonBox({
      element: this.get$Element(),
      className: this.getClassName(),
      title: this.getLocaliser().selectL10N(this.getTitle()),
      collapsible: this.collapsible_,
      collapsed: this.collapsed_
    })

    this.listenAt(this.menu_).on('change:collapsed', () => this.dispatchEvent('change:size'))

    this.get$Element().append(this.menu_.get$Element())

    /**
     * @type {number}
     * @private
     */
    this.minVisibleButtons_ = options.minVisibleEntries || 5

    if (options.checkboxes !== false) {
      this.menu_.get$Element().addClass('g4u-layerselector-with-checkbox')
    }

    this.elements_ = []

    this.elementGroups_ = {}
  }

  /**
   * @param {G4UMap} map
   */
  setMap (map) {
    if (this.getMap()) {
      this.clear()
    }

    super.setMap(map)

    if (map) {
      this.build()

      this.listenAt(map.getView()).on('change:resolution', () => {
        const zoom = map.getView().getZoom()
        for (const elem of this.elements_) {
          elem.updateDisabled(zoom)
        }
      })

      const zoom = map.getView().getZoom()
      for (const elem of this.elements_) {
        elem.updateDisabled(zoom)
      }
    }
  }

  clear () {
    this.detachAllListeners()
    for (const elem of this.elements_) {
      elem.detach()
    }
    this.menu_.get$Body().empty()
  }

  addElement (config) {
    const element = this.getElement(config)
    this.menu_.get$Body().append(element.get$Element())
    this.elements_.push(element)
    return element.count()
  }

  build () {
    const menuConfig = this.getMap().get('layerConfig').menus[this.menuName_]
    let count = 0
    this.elements_ = []
    if (isArray(menuConfig)) {
      for (const buttonConfig of menuConfig) {
        count += this.addElement(buttonConfig)
      }
    } else if (isObject(menuConfig)) {
      count += this.addElement(menuConfig)
    }

    if (count < this.minLayerAmount_) {
      this.setVisible(false)
    }
  }

  getElement (config) {
    switch (config.type) {
      case 'layer':
        return new LayerButton(this, config, this.getMap())
      case 'group':
        return new GroupElement(this, config, this.getMap())
      case 'WMS':
        return new WMSLayerButton(this, config, this.getMap())
      case 'multi':
        return new MultiButton(this, config, this.getMap())
    }
  }

  addElementToGroup (element, groupName) {
    if (!this.elementGroups_.hasOwnProperty(groupName)) {
      this.elementGroups_[groupName] = []
    }
    this.elementGroups_[groupName].push(element)
  }

  deactivateOtherGroupElements (element, groupName) {
    for (const other of this.elementGroups_[groupName]) {
      if (other !== element) {
        other.setActive(false)
      }
    }
  }

  /**
   * @returns {boolean}
   */
  getCollapsible () {
    return this.collapsible_
  }

  /**
   * @returns {boolean}
   */
  getCollapsed () {
    return this.menu_.getCollapsed()
  }

  /**
   * @param {boolean} collapsed
   * @param {boolean} silent
   */
  setCollapsed (collapsed, silent) {
    if (collapsed !== this.menu_.getCollapsed()) {
      this.menu_.setCollapsed(collapsed, silent)
    }
  }

  /**
   * Returns true if the control is squeezable in the given dimension. Used by Positioning.
   * @param {string} dimension
   * @returns {boolean}
   */
  isSqueezable (dimension) {
    return dimension === 'height'
  }

  /**
   * Squeezes the control in the given dimension by the provided value. Used by Positioning
   * Returns the value the control could get squeezed by.
   * @param {string} dimension
   * @param {number} value
   * @returns {number}
   */
  squeezeBy (dimension, value) {
    if (dimension === 'height') {
      const $contentBox = this.get$Element().find(`.${this.getClassName()}-content`)
      const $buttons = $contentBox.find('button:visible')
        .filter(`.${this.getClassName()}-layerbutton,.${this.getClassName()}-menu-titlebutton`)

      if ($buttons.length > 1) {
        const height = $contentBox.height()
        const buttonHeight = offset($buttons.eq(1), $buttons.eq(0)).top

        const newHeight = Math.max(buttonHeight * this.minVisibleButtons_, height - value)

        if (height > newHeight) {
          $contentBox.css('max-height', newHeight)
          return height - newHeight
        }
      }
    }

    return 0
  }

  beforePositioning () {
    this.scrolled_ = this.menu_.get$Body().scrollTop()
  }

  /**
   * used by positioning
   */
  afterPositioning () {
    this.menu_.get$Body().scrollTop(this.scrolled_)
  }

  /**
   * Removes the squeeze. Used by Positioning.
   * @param {string} dimension
   */
  release (dimension) {
    if (dimension === 'height') {
      this.get$Element().find(`.${this.getClassName()}-content`).css('max-height', '')
    }
  }
}