Home Reference Source

src/configurators/ControlFactory.js

import View from 'ol/View'
import { transform } from 'ol/proj'

import { Attribution } from '../controls/Attribution'
import { ComposedControl } from '../controls/ComposedControl'
import { ArrowButtons } from '../controls/ArrowButtons'
import { CombinedZoom } from '../controls/CombinedZoom'
import { LegendViewer } from '../controls/LegendViewer'
import { ZoomToBbox } from '../controls/ZoomToBbox'
import { LayerSelector } from '../layerSelector/LayerSelector'
import { GeolocationButton } from '../controls/GeoLocationButton'
import { MeasurementButton } from '../controls/MeasurementButton'
import { LanguageSwitcherButton } from '../controls/LanguageSwitcherButton'
import { LanguageSwitcherMenu } from '../controls/LanguageSwitcherMenu'
import { MobileControls } from '../controls/MobileControls'
import { InfoButton } from '../controls/InfoButton'
import { LinkButton } from '../controls/LinkButton'
import { PrintButton } from '../controls/PrintButton'
import { Logo } from '../controls/Logo'
import { HelpButton } from '../controls/HelpButton'
import { WindowDecorator } from '../controls/WindowDecorator'

import { asObject, checkFor } from '../utilities'
import { copyDeep } from '../utilitiesObject'

import { G4UMap } from '../G4UMap'

import { Debug } from '../Debug'
import { ActiveGroup } from '../controls/ActiveGroup'
import { MousePosition } from '../controls/MousePosition'
import { ScaleLine } from '../controls/ScaleLine'
import { OverviewMap } from '../controls/OverviewMap'
import { SingleDrawButton } from '../controls/SingleDrawButton'
import { StaticOverviewMap } from '../controls/StaticOverviewMap'

/**
 *  Inside of this object the controls are configured. Normally they are referenced by their type, but if
 *  you have multiple controls of one type you can specify a name under which it is referenced. Then you
 *  have to specify the `controlType` in the configuration object.
 *
 *  Example:
 *  let `'example'` be the chosen name for an attribution control. Then we can reference it in `onMap` or
 *  any `contains` property as `'example'`. The configuration object is now named `'example'` but needs to
 *  have an `'controlType'` property which needs to be set to `'attribution'`.
 *
 *  ```
 *    "controls": {
 *      "onMap": ["example"],
 *      "example": {
 *        "controlType": "attribution",
 *        ...
 *      }
 *    }
 *  ```
 *
 * @typedef {object} ControlsConfig
 * @property {string[]} onMap All controls mentioned in this array will be added directly to the map.
 * @property {ArrowButtonOptions} [arrowButtons]
 * @property {AttributionOptions} [attribution]
 * @property {CombinedZoomOptions} [combinedZoom]
 * @property {GeoLocationButtonOptions} [geolocationButton]
 * @property {HelpButtonOptions} [helpButton]
 * @property {InfoButtonOptions} [infoButton]
 * @property {LanguageSwitcherButtonOptions} [languageSwitcherButton]
 * @property {LanguageSwitcherMenuOptions} [languageSwitcherMenu]
 * @property {LayerSelectorOptions} [layerSelector]
 * @property {LinkButtonOptions} [linkButton]
 * @property {LogoOptions} [logo]
 * @property {MeasurementButtonOptions} [measurementButton]
 * @property {ShiftableComposedControlOptions} [mobileControls]
 * @property {MousePositionOptions} [mousePosition]
 * @property {OverviewMapOptions} [overviewMap]
 * @property {g4uControlOptions} [printButton]
 * @property {ScaleLineOptions} [scaleLine]
 * @property {DrawButtonsOptions} [singleDrawButton]
 * @property {ComposedControlOptions} [toolbox]
 * @property {ComposedControlOptions} [layerMenu]
 */

/**
 * @typedef {Object} ControlFactoryOptions
 * @property {L10N} localiser
 * @property {G4UMap} map
 */

/**
 * A Factory that takes a control config as an object and returns the correspondig control. Modules can expand the
 * functionality of this class via the createControl method
 */
export class ControlFactory {
  /**
   * @param {ControlFactoryOptions} options
   * @public
   */
  constructor (options) {
    /**
     * @type {L10N}
     * @private
     * */
    this.localiser_ = options.localiser

    /**
     * @type {G4UMap}
     * @private
     */
    this.map_ = options.map

    /**
     * @type {Map.<string, ActiveGroup>}
     * @private
     */
    this.activeGroups_ = new Map()
  }

  /**
   * @returns {Positioning}
   */
  getPositioning () {
    return this.map_.get('controlPositioning')
  }

  /**
   * The factory function
   * @param {string} controlType
   * @param {g4uControlOptions} options
   * @returns {Control}
   * @public
   */
  createControl (controlType, options) {
    switch (controlType) {
      case 'mousePosition':
        options.projection = options.projection || this.map_.get('interfaceProjection')
        return new MousePosition(options)
      case 'attribution':
        return new Attribution(options)
      case 'zoom':
        return new CombinedZoom(options)
      case 'scaleLine':
        return new ScaleLine(options)
      case 'logo':
        return new Logo(options)
      case 'linkButton':
        return new LinkButton(options)
      case 'mobileControls':
        return new MobileControls(options)
      case 'overviewMap': {
        const projection = options.projection || this.map_.get('mapProjection')
        options.view = new View({ projection: projection })
        return new OverviewMap(options)
      }
      case 'staticOverviewMap':
        return new StaticOverviewMap(options)
      case 'infoButton':
        return new WindowDecorator({
          component: new InfoButton(options)
        })
      case 'arrowButtons': {
        const mapConfig = this.map_.get('mapConfig')
        if (mapConfig.view) {
          if (mapConfig.view.center) {
            options.initCenter = transform(mapConfig.view.center,
              this.map_.get('interfaceProjection'), this.map_.get('mapProjection'))
          }
          if (mapConfig.view.zoom) {
            options.initZoom = mapConfig.view.zoom
          }
        }
        return new ArrowButtons(options)
      }
      case 'layerSelector':
        return new LayerSelector(options)
      case 'geolocationButton':
        return new GeolocationButton(options)
      case 'languageSwitcherButton':
        return new LanguageSwitcherButton(options)
      case 'languageSwitcherMenu':
        return new LanguageSwitcherMenu(options)
      case 'printButton':
        return new PrintButton(options)
      case 'distanceMeasurementButton':
        options.className = options.className || 'g4u-distance-measurement'
        options.type = 'LineString'
        options.dimension = 1
        return new WindowDecorator({
          component: new MeasurementButton(options)
        })
      case 'areaMeasurementButton':
        options.className = options.className || 'g4u-area-measurement'
        options.type = 'Polygon'
        options.dimension = 2
        return new WindowDecorator({
          component: new MeasurementButton(options)
        })
      case 'helpButton':
        options.configControls = this.map_.get('mapConfig').controls
        return new WindowDecorator({
          component: new HelpButton(options)
        })
      case 'toolbox':
        options.className = options.className || 'g4u-toolbox'
        return new ComposedControl(options)
      case 'layerMenu':
        options.className = options.className || 'g4u-layermenu'
        return new ComposedControl(options)
      case 'singleDrawButton':
        return new SingleDrawButton(options)
      case 'legendViewer':
        return new LegendViewer(options)
      case 'zoomToBbox':
        return new ZoomToBbox(options)
    }
  }

  /**
   * Function to add a control by its name to a certain receiver
   * @param {ComposedControl|G4UMap} receiver
   * @param {string} controlName
   * @public
   */
  addControlTo (receiver, controlName) {
    /**
     * @type {g4uControlOptions}
     */
    const config = copyDeep(asObject(this.controlsConfig[controlName]))

    const controlType = config.controlType || controlName

    config.controlName = controlName

    config.localiser = this.localiser_

    if (!config.hasOwnProperty('importance') && receiver.getImportance) {
      config.importance = receiver.getImportance()
    }

    let control = this.createControl(controlType, config, receiver)

    const modules = this.map_.getModules()

    for (let i = 0, ii = modules.length; i < ii && (control === undefined); i++) {
      control = modules[i].createControl(controlType, config, receiver)
    }

    if (control) {
      if (config.activeGroup) {
        if (!this.activeGroups_.has(config.activeGroup)) {
          this.activeGroups_.set(config.activeGroup, new ActiveGroup())
        }
        this.activeGroups_.get(config.activeGroup).addControl(control)
      }

      if (!this.map_.controlsByName[controlName]) {
        this.map_.controlsByName[controlName] = [control]
      } else {
        this.map_.controlsByName[controlName].push(control)
      }

      receiver.addControl(control)

      if (config.contains) {
        this.addControlMultipleInternal_(control, config.contains)
      }

      if (control.rewire) {
        control.rewire()
      }

      if (receiver instanceof G4UMap) {
        this.getPositioning().addControl(control)
      }
    } else if (control === undefined) {
      throw new Error('Unrecognized control type ' + controlType +
        ', maybe you did forget to set the property type of the control?')
    }
  }

  /**
   * Adds multiple controls to one receiver
   * @param {ComposedControl|G4UMap} receiver
   * @param {string[]} controlNames
   * @private
   */
  addControlMultipleInternal_ (receiver, controlNames) {
    controlNames.forEach(controlName => {
      Debug.tryOrThrow(() => {
        this.addControlTo(receiver, controlName)
      })
    })
  }

  /**
   * Add all configured Controls to the map
   * @public
   */
  addControls () {
    /**
     * @type {ControlsConfig}
     */
    this.controlsConfig = this.map_.get('mapConfig').controls
    if (checkFor(this.controlsConfig, 'onMap')) {
      this.addControlMultipleInternal_(this.map_, this.controlsConfig.onMap)
    }
  }

  /**
   * Sets the localiser to pass on to the controls
   * @param {L10N} loc
   * @public
   */
  setLocaliser (loc) {
    this.localiser_ = loc
  }
}