src/controls/LayerSelector.js
import $ from 'jquery'
import { GroupLayer } from '../layers/GroupLayer'
import { ButtonBox } from '../html/ButtonBox'
import { Control } from './Control'
import { offset, mixin } from '../utilities'
import { Window } from '../html/Window'
import '../../less/layerselector.less'
import { ListenerOrganizerMixin } from '../ListenerOrganizerMixin'
import { URL } from '../URLHelper'
import { addTooltip, changeTooltip } from '../html/html'
/**
* @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} layerGroupName the name of the layerGroup 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
*/
/**
* @typedef {object} LayerButton
* @property {string} title
* @property {boolean} [checked] if QUERY_LAYERS are set and checked is true, the featureInfo appears turned on.
* @property {string[]} LAYERS
* @property {string[]} [QUERY_LAYERS]
*/
/**
* This control shows Buttons to let you select the layer you want to see on the map.
* It supports categories and nested categories - each {GroupLayer}-Object will be interpreted as a category.
*/
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.layerGroupName_ = options.layerGroupName
/**
* @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'
}
/**
* @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.menu_.on('change:collapsed', () => this.dispatchEvent('change:size'))
this.get$Element().append(this.menu_.get$Element())
/**
* @type {number}
* @private
*/
this.minVisibleButtons_ = options.minVisibleEntries || 5
/**
* @type {boolean}
* @private
*/
this.visible_ = true
}
/**
* @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)
}
}
addWindowToButton ($button, layer) {
const windowConfig = layer.get('window')
const window = new Window({
parentClassName: this.getClassName(),
map: this.getMap()
})
if (layer.get('addClass')) {
window.get$Element().addClass(layer.get('addClass'))
}
let content
const showWindow = () => {
if (this.getMap().get('localiser').isRtl()) {
window.get$Body().prop('dir', 'rtl')
} else {
window.get$Body().prop('dir', undefined)
}
window.get$Body().html(content)
window.setVisible(true)
}
const hideWindow = () => {
window.setVisible(false)
}
this.listenAt($button).on('click', () => {
if (layer.getVisible()) {
if (!content) {
const url = URL.extractFromConfig(windowConfig, 'url', undefined, this.getMap())
$.get(url.finalize(), data => {
content = data
showWindow()
})
} else {
showWindow()
}
} else {
hideWindow()
}
})
}
updateDisabledButtons () {
this.dispatchEvent('update:disabled')
}
/**
* this method builds a button for a layer. It toggles visibility if you click on it
* @param {ol.layer.Base} layer
* @param {jQuery} $target
*/
buildLayerButton (layer, $target) {
if (layer.get('available')) {
const $button = $('<button>')
.addClass(this.classNames_.layerButton)
.attr('id', layer.get('id'))
.html(layer.get('title'))
if (layer.get('addClass')) {
$button.addClass(layer.get('addClass'))
}
if (layer.get('disabled')) {
$button.addClass(this.classNames_.disabled)
}
this.on('update:disabled', () => {
$button.toggleClass(this.classNames_.disabled, layer.get('disabled'))
})
if (this.getMap().get('localiser').isRtl()) {
$button.prop('dir', 'rtl')
}
const activeClassName = this.classNames_.menu + '-active'
this.listenAt($button).on('click', () => {
layer.setVisible(!layer.getVisible())
this.dispatchEvent({
type: 'click:layer',
layer: layer
})
})
if (layer.getVisible()) {
$button.addClass(activeClassName)
}
this.listenAt(layer).on('change:visible', () => {
$button.toggleClass(activeClassName, layer.getVisible())
if (!layer.getVisible()) {
$button.removeClass('g4u-layer-loading')
}
})
if (layer.get('window')) {
this.addWindowToButton($button, layer)
}
this.listenAt(layer).on('loadcountstart', () => {
$button.addClass('g4u-layer-loading')
})
this.listenAt(layer).on('loadcountend', () => {
$button.removeClass('g4u-layer-loading')
})
$target.append($button)
return $button
}
}
/**
* builds a category button which collapses on click
* @param {GroupLayer} categoryLayer
* @param {jQuery} $target
*/
buildCategoryButton (categoryLayer, $target) {
let $nextTarget = $target
if (categoryLayer.get('available')) {
const activateChildren = categoryLayer.get('activateChildren') !== false
let collapsed = categoryLayer.get('collapsed')
if (collapsed === undefined) {
collapsed = !categoryLayer.countChildrenVisible() && (categoryLayer.get('collapsed') !== false)
categoryLayer.set('collapsed', collapsed)
}
const menu = new ButtonBox({
className: this.classNames_.menu,
title: this.getLocaliser().selectL10N(categoryLayer.get('title')),
rtl: this.getMap().get('localiser').isRtl(),
titleButton: activateChildren,
collapsed,
id: categoryLayer.get('id'),
addClass: categoryLayer.get('addClass')
})
const countChildren = categoryLayer.countChildren()
let countVisibleChildren = categoryLayer.countChildrenVisible()
const updateButtonActivities = () => {
if (countVisibleChildren === 0) {
menu.setCollapseButtonActive(false)
if (activateChildren) {
menu.setTitleButtonActive(false)
}
} else if (countVisibleChildren === countChildren) {
menu.setCollapseButtonActive(true)
if (activateChildren) {
menu.setTitleButtonActive(true)
}
} else {
menu.setCollapseButtonActive(true)
if (activateChildren) {
menu.setTitleButtonActive(false)
}
}
}
updateButtonActivities()
const forEachChildLayer = childLayer => {
this.listenAt(childLayer)
.on(['change:visible', 'change:childVisible'], e => {
const changedLayer = e.child || childLayer
if (changedLayer.getVisible()) {
countVisibleChildren++
} else {
countVisibleChildren--
}
updateButtonActivities()
})
}
categoryLayer.getLayers().forEach(forEachChildLayer)
this.listenAt(categoryLayer.getLayers())
.on('add', e => forEachChildLayer(e.element))
this.listenAt(menu)
.on('title:click', () => {
const visible = countVisibleChildren < countChildren
categoryLayer.recursiveForEach(childLayer => {
if (!(childLayer instanceof GroupLayer)) {
childLayer.setVisible(visible)
}
})
this.dispatchEvent({
type: 'click:layer',
layer: categoryLayer
})
})
$target.append(menu.get$Element())
$nextTarget = menu.get$Body()
this.listenAt(menu)
.on('change:collapsed', () => {
categoryLayer.set('collapsed', menu.getCollapsed())
this.dispatchEvent('change:size')
this.changed()
})
for (const childLayer of categoryLayer.getLayers().getArray()) {
this.chooseButtonBuilder(childLayer, $nextTarget)
}
return menu
} else {
for (const childLayer of categoryLayer.getLayers().getArray()) {
this.chooseButtonBuilder(childLayer, $nextTarget)
}
}
}
buildWMSButton (wmsLayer, $target) {
if (wmsLayer.get('available')) {
const layerButtons = wmsLayer.get('buttons')
if (layerButtons && layerButtons.length) {
let countActiveButtons = 0
const wmsSource = wmsLayer.getSource()
let allLayersParams = []
let allQueryLayersParams = []
const featureInfoCheckable = wmsSource.isFeatureInfoCheckable()
let menu
if (layerButtons.length > 1) {
menu = new ButtonBox({
className: this.classNames_.menu,
title: this.getLocaliser().selectL10N(wmsLayer.get('title')),
titleButton: true,
collapsed: wmsLayer.get('collapsed') !== false,
id: wmsLayer.get('id'),
addClass: wmsLayer.get('addClass')
})
$target.append(menu.get$Element())
this.listenAt(menu)
.on('change:collapsed', () => {
this.dispatchEvent('change:size')
this.changed()
})
$target = menu.get$Body()
}
const activeClassName = this.classNames_.menu + '-active'
for (const layerButton of layerButtons) {
allLayersParams = allLayersParams.concat(layerButton.LAYERS)
allQueryLayersParams = allQueryLayersParams.concat(layerButton.QUERY_LAYERS)
const $button = $('<span>')
.addClass(this.classNames_.layerButton)
.addClass('button')
.html(this.getLocaliser().selectL10N(layerButton.title))
const $toggleFeatureInfo = $('<span>')
.addClass(this.classNames_.featureInfo)
addTooltip($toggleFeatureInfo,
this.getLocaliser().localiseUsingDictionary('LayerSelector featureInfo show'))
if (wmsLayer.get('disabled')) {
$button.addClass(this.classNames_.disabled)
}
this.on('update:disabled', () => {
$button.toggleClass(this.classNames_.disabled, wmsLayer.get('disabled'))
})
$target.append($button)
const toggleButtonActive = () => {
const active = !wmsSource.getWMSLayersVisible(layerButton.LAYERS)
if (active) {
countActiveButtons++
} else {
countActiveButtons--
}
$button.toggleClass(activeClassName, active)
wmsSource.toggleWMSLayers(layerButton.LAYERS, active)
if (!active && featureInfoCheckable) {
setCheckboxActive(false)
}
if (active && layerButton.checked) {
setCheckboxActive(true)
}
if (countActiveButtons === 0) {
wmsLayer.setVisible(false)
if (menu) {
menu.setCollapseButtonActive(false)
menu.setTitleButtonActive(false)
}
} else if (countActiveButtons === layerButtons.length) {
wmsLayer.setVisible(true)
if (menu) {
menu.setCollapseButtonActive(true)
menu.setTitleButtonActive(true)
}
} else {
wmsLayer.setVisible(true)
if (menu) {
menu.setCollapseButtonActive(true)
menu.setTitleButtonActive(false)
}
}
this.dispatchEvent({
type: 'click:layer',
layer: wmsLayer,
wmsLayer: true
})
}
const setCheckboxActive = checkboxActive => {
if (checkboxActive && !wmsSource.getWMSLayersVisible(layerButton.LAYERS)) {
toggleButtonActive()
}
wmsSource.toggleWMSQueryLayers(layerButton.QUERY_LAYERS, checkboxActive)
$toggleFeatureInfo.toggleClass(this.classNames_.featureInfoActive, checkboxActive)
if (checkboxActive) {
changeTooltip($toggleFeatureInfo,
this.getLocaliser().localiseUsingDictionary('LayerSelector featureInfo hide'))
} else {
changeTooltip($toggleFeatureInfo,
this.getLocaliser().localiseUsingDictionary('LayerSelector featureInfo show'))
}
}
if (wmsLayer.getVisible()) {
toggleButtonActive()
}
this.listenAt($button)
.on('click', () => {
toggleButtonActive()
})
if (featureInfoCheckable) {
$button.append($toggleFeatureInfo)
this.listenAt($toggleFeatureInfo).on('click', e => {
setCheckboxActive(!$toggleFeatureInfo.hasClass(this.classNames_.featureInfoActive))
e.stopPropagation()
})
}
if (layerButtons.length === 1) {
this.listenAt(wmsLayer).on('loadcountstart', () => {
$button.addClass('g4u-layer-loading')
})
this.listenAt(wmsLayer).on('loadcountend', () => {
$button.removeClass('g4u-layer-loading')
})
}
}
if (menu) {
this.listenAt(menu).on('title:click', () => {
const activateAll = countActiveButtons < layerButtons.length
if (activateAll) {
wmsSource.toggleWMSLayers(allLayersParams, true)
countActiveButtons = layerButtons.length
} else {
wmsSource.toggleWMSLayers(allLayersParams, false)
if (featureInfoCheckable) {
wmsSource.toggleWMSQueryLayers(allQueryLayersParams, false)
menu.get$Body().find('input[type=checkbox]').prop('checked', false)
}
countActiveButtons = 0
}
menu.get$Body().find('button').toggleClass(activeClassName, activateAll)
wmsLayer.setVisible(activateAll)
menu.setCollapseButtonActive(activateAll)
menu.setTitleButtonActive(activateAll)
this.dispatchEvent({
type: 'click:category',
layer: wmsLayer,
wmsLayer: true
})
})
this.listenAt(wmsLayer).on('loadcountstart', () => {
menu.get$Element().addClass('g4u-layer-loading')
})
this.listenAt(wmsLayer).on('loadcountend', () => {
menu.get$Element().removeClass('g4u-layer-loading')
})
return menu
}
} else {
const wmsSource = wmsLayer.getSource()
const featureInfoCheckable = wmsSource.isFeatureInfoCheckable()
const activeClassName = this.classNames_.menu + '-active'
const $button = $('<span>')
.addClass(this.classNames_.layerButton)
.addClass('button')
.html(this.getLocaliser().selectL10N(wmsLayer.get('title')))
if (wmsLayer.get('addClass')) {
$button.addClass(wmsLayer.get('addClass'))
}
if (wmsLayer.get('disabled')) {
$button.addClass(this.classNames_.disabled)
}
this.on('update:disabled', () => {
$button.toggleClass(this.classNames_.disabled, wmsLayer.get('disabled'))
})
const $toggleFeatureInfo = $('<span>')
.addClass(this.classNames_.featureInfo)
addTooltip($toggleFeatureInfo,
this.getLocaliser().localiseUsingDictionary('LayerSelector featureInfo show'))
$target.append($button)
const toggleButtonActive = () => {
const active = !wmsLayer.getVisible()
wmsLayer.setVisible(active)
$button.toggleClass(activeClassName, active)
if (!active && featureInfoCheckable) {
setCheckboxActive(false)
}
if (active && wmsSource.isFeatureInfoChecked()) {
setCheckboxActive(true)
}
this.dispatchEvent({
type: 'click:layer',
layer: wmsLayer,
wmsLayer: true
})
}
const featureInfoParams = wmsSource.getFeatureInfoParams()
const setCheckboxActive = checkboxActive => {
if (checkboxActive && !wmsLayer.getVisible()) {
toggleButtonActive()
}
wmsSource.toggleWMSQueryLayers(featureInfoParams.QUERY_LAYERS, checkboxActive)
$toggleFeatureInfo.toggleClass(this.classNames_.featureInfoActive, checkboxActive)
if (checkboxActive) {
changeTooltip($toggleFeatureInfo,
this.getLocaliser().localiseUsingDictionary('LayerSelector featureInfo hide'))
} else {
changeTooltip($toggleFeatureInfo,
this.getLocaliser().localiseUsingDictionary('LayerSelector featureInfo show'))
}
}
this.listenAt($button)
.on('click touchstart', () => {
toggleButtonActive()
this.dispatchEvent({
type: 'click:layer',
layer: wmsLayer,
wmsLayer: true
})
})
this.listenAt(wmsLayer).on('change:visible', () => {
$button.toggleClass(activeClassName, wmsLayer.getVisible())
})
if (wmsLayer.get('window')) {
this.addWindowToButton($button, wmsLayer)
}
if (featureInfoCheckable) {
$button.append($toggleFeatureInfo)
this.listenAt($toggleFeatureInfo).on('click touchstart', e => {
setCheckboxActive(!$toggleFeatureInfo.hasClass(this.classNames_.featureInfoActive))
e.stopPropagation()
})
}
this.listenAt(wmsLayer).on('loadcountstart', () => {
$button.addClass('g4u-layer-loading')
})
this.listenAt(wmsLayer).on('loadcountend', () => {
$button.removeClass('g4u-layer-loading')
})
}
}
}
/**
* This method chooses the right builder function
* @param {ol.layer.Base} layer
* @param {jQuery} $target
*/
chooseButtonBuilder (layer, $target) {
if (layer instanceof GroupLayer) {
return this.buildCategoryButton(layer, $target)
} else if (layer.getSource && layer.getSource() && layer.getSource().isFeatureInfoCheckable) {
return this.buildWMSButton(layer, $target)
} else {
return this.buildLayerButton(layer, $target)
}
}
clear () {
this.detachAllListeners()
this.menu_.get$Body().empty()
}
build () {
this.layers_ = this.getMap().get(this.layerGroupName_).getLayers()
if (this.layers_.getLength() >= this.minLayerAmount_) {
this.setVisible(true)
const menuFunctions = new ButtonBox({ className: this.classNames_.menu })
for (const layer of this.layers_.getArray()) {
this.chooseButtonBuilder(layer, this.menu_.get$Body())
}
menuFunctions.giveLastVisible(this.get$Element().children(':last-child').children(':last-child'))
this.listenAt(this.menu_).on('change:collapsed', () => this.dispatchEvent('change:size'))
} else {
this.setVisible(false)
}
}
rebuild () {
this.clear()
this.build()
}
/**
* 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', '')
}
}
}