Home Reference Source

src/G4UMap.js

import $ from 'jquery'
import { isPlainObject } from 'lodash/lang'
import OlMap from 'ol/Map'
import JSON5 from 'json5'

import { MapConfigurator } from './configurators/MapConfigurator'
import { Debug } from './Debug'
import { cssClasses } from './globals'
import { L10N } from './L10N'
import { layerConfigConverter } from './layerSelector/layerConfigConverter'
import { getRegisteredModules } from './moduleRegistration'
import './openlayersInjections'
import { PopupModifierManager } from './PopupModifierManager'

import '../less/map.less'

/**
 * @typedef {object} Configs
 * @property {string|object} client the client config
 * @property {string|object} layer the layer config
 * @property {string|object} translations the translations
 * @property {string|object} [styles] the style map
 */

/**
 * @typedef {object} G4UMapOptions
 * @property {L10N} [localiser]
 * @property {object.<string, PopupModifier>} [popupModifiers]
 */

/**
 * Definition of the map-object
 * Main task of the constructor is to load and read out the configuration.
 * Uses the functions makeMapLayers, makeMapUI to create the map.
 *
 * Custom properties accessible via method .get('propertyName')
 *
 * @fires 'resize'
 * @fires 'userActionTracking'
 * @fires 'beforeConfigLoad'
 * @fires 'afterConfigLoad'
 * @fires 'afterConfiguring'
 * @fires 'change:ready'
 * @fires 'change:ready:ui'
 * @fires 'change:ready:layers'
 */
export class G4UMap extends OlMap {
  /**
   * @param {HTMLElement|jQuery|string} target element or id of an element
   * @param {Configs} configs
   * @param {G4UMapOptions} [options={}]
   */
  constructor (target, configs, options = {}) {
    // //////////////////////////////////////////////////////////////////////////////////////// //
    //                     Call of the Parents Class Constructor                                //
    // //////////////////////////////////////////////////////////////////////////////////////// //

    super({
      controls: [],
      interactions: [],
      view: null
    })

    this.set('guide4youVersion', GUIDE4YOU_VERSION) // eslint-disable-line

    this.set('options', options)

    /**
     * @type {Map.<string, ol.interaction.Interaction[]>}
     * @private
     */
    this.defaultInteractions_ = new Map()

    /**
     * @type {Map.<string, ol.interaction.Interaction[]>}
     * @private
     */
    this.supersedingInteractions_ = new Map()

    /**
     * @type {Module[]}
     * @private
     */
    this.modules_ = []

    this.addModules(getRegisteredModules())

    this.set('ready', false)

    this.on(['change:ready', 'change:ready:ui', 'change:ready:layers'], /** ol.ObjectEvent */ e => {
      if (this.get(e.key)) {
        this.dispatchEvent(e.key)
      }
    })

    // popupModifiers

    const popupModifiers = new PopupModifierManager()
    this.set('popupModifiers', popupModifiers)

    if (options.popupModifiers) {
      this.on('change:featurePopup', () => {
        for (const name of Object.keys(options.popupModifiers)) {
          popupModifiers.register(name, options.popupModifiers[name])
        }
      })
    }

    // Setting the target of the map

    if (typeof target === 'string' && target[0] !== '#') {
      this.setTarget($('#' + target).get(0))
    } else {
      this.setTarget($(target).get(0))
    }

    // set the display mode to desktop initially to render overviewmpa correctly
    $(this.getTarget()).children().addClass(cssClasses.desktop)

    // //////////////////////////////////////////////////////////////////////////////////////////
    //                            Load config files if needed                                 //
    // //////////////////////////////////////////////////////////////////////////////////////////

    this.dispatchEvent('beforeConfigLoad')

    this.set('mapConfigReady', false)
    this.set('layerConfigReady', false)

    this.loadConfigs(configs).then(() => {
      const config = this.get('mapConfig')

      this.dispatchEvent('afterConfigLoad')

      this.set('proxy', config.proxy)

      // //////////////////////////////////////////////////////////////////////////////////////// //
      //                                     Localization                                         //
      // //////////////////////////////////////////////////////////////////////////////////////// //

      if (!options.localiser) {
        const localiserOptions = {}

        if (config.hasOwnProperty('languageSettings')) {
          const l10nconf = config.languageSettings

          localiserOptions.currentLanguage = l10nconf.currentLanguage

          if (l10nconf.hasOwnProperty('defaultLanguage')) {
            localiserOptions.defaultLanguage = l10nconf.defaultLanguage
          }

          if (l10nconf.hasOwnProperty('availableLanguages')) {
            localiserOptions.availableLanguages = l10nconf.availableLanguages
          }

          if (l10nconf.hasOwnProperty('languageFile')) {
            Debug.warn('You are using a languageFile options in your client config. This option is not used ' +
              'anymore.\n Either remove the option if you want to use the default values or pass it via ' +
              'the configs object to createMap (`createMap(target, { l10n: \'path/to/l10n.json\' })`).')
          }
        }

        const localiser = new L10N(this.get('translations'), localiserOptions)
        this.set('localiser', localiser)
      } else {
        this.set('localiser', options.localiser)
      }

      this.asSoonAs('ready', true, () => {
        this.get('localiser').on('change:language', () => {
          const visibilities = this.getLayerGroup().getIdsVisibilities()

          this.get('configurator').configureLayers()
          this.get('configurator').configureUI()

          this.getLayerGroup().setIdsVisibilities(visibilities)
        })
      })

      // //////////////////////////////////////////////////////////////////////////////////////// //
      //                                    Configurator                                          //
      // //////////////////////////////////////////////////////////////////////////////////////// //

      this.set('configurator', new MapConfigurator(this))

      this.dispatchEvent('afterConfiguring')

      if (this.get('ready:ui') && this.get('ready:layers')) {
        this.set('ready', true)
      }

      this.on(['change:ready:ui', 'change:ready:layers'], /** ol.ObjectEvent */ e => {
        if (!this.get(e.key)) {
          this.set('ready', false)
        }

        if (this.get('ready:ui') && this.get('ready:layers')) {
          this.set('ready', true)
        }
      })
    }).catch(Debug.defaultErrorHandler)
  }

  static loadConfigFile (fileName) {
    return $.ajax({
      url: fileName,
      dataType: 'text'
    }).then(data => {
      try {
        return JSON5.parse(data)
      } catch (err) {
        Debug.error(`The config file ${fileName} couldn't be parsed.`)
        Debug.error(err)
      }
    }).fail((err) => {
      Debug.error(`The config file ${fileName} couldn't be loaded.`)
      Debug.error(err)
    })
  }

  updateClientConfig (config) {
    let clientPromise
    if (isPlainObject(config)) {
      clientPromise = Promise.resolve(config)
    } else {
      if (!this.get('configFileName')) {
        this.set('configFileName', config)
      }
      clientPromise = G4UMap.loadConfigFile(config)
    }
    clientPromise.then(data => {
      this.set('mapConfigReady', true)
      this.set('mapConfig', data)
    })
    return clientPromise
  }

  updateLayerConfig (config) {
    let layerPromise
    if (isPlainObject(config)) {
      layerPromise = Promise.resolve(config)
    } else {
      if (!this.get('layerConfigFileName')) {
        this.set('layerConfigFileName', config)
      }
      layerPromise = G4UMap.loadConfigFile(config)
    }
    layerPromise.then(data => {
      this.set('layerConfigReady', true)
      this.set('layerConfig', layerConfigConverter(data))
    })
    return layerPromise
  }

  loadConfigs (configs) {
    this.set('mapConfigReady', false)
    this.set('layerConfigReady', false)

    const configPromises = []

    if (configs.hasOwnProperty('client')) {
      configPromises.push(this.updateClientConfig(configs.client))
    } else {
      Debug.error('No client config provided.')
    }

    // issue reload of mapConfig if the name was changed
    this.on('change:configFileName', /** ol.ObjectEvent */ e => {
      this.set('ready', false)
      this.set('mapConfigReady', false)

      this.oldMapConfigs_ = this.oldMapConfigs_ || {}
      this.oldMapConfigs_[e.oldValue] = this.get('mapConfig')

      if (this.oldMapConfigs_.hasOwnProperty(this.get('configFileName'))) {
        this.updateClientConfig(this.oldMapConfigs_[this.get('configFileName')])
      } else {
        this.updateClientConfig(this.get('configFileName'))
      }
    })

    if (configs.hasOwnProperty('layer')) {
      configPromises.push(this.updateLayerConfig(configs.layer))
    } else {
      Debug.error('No layer config provided.')
    }

    // issue reload of layerConfig if the name was changed
    this.on('change:layerConfigFileName', /** ol.ObjectEvent */ e => {
      this.set('ready', false)
      this.set('layerConfigReady', false)

      this.oldLayerConfigs_ = this.oldLayerConfigs_ || {}
      this.oldLayerConfigs_[e.oldValue] = this.get('layerConfig')

      if (this.oldLayerConfigs_.hasOwnProperty(this.get('layerConfigFileName'))) {
        this.updateLayerConfig(this.oldLayerConfigs_[this.get('layerConfigFileName')])
      } else {
        this.updateLayerConfig(this.get('layerConfigFileName'))
      }
    })

    if (configs.hasOwnProperty('translations')) {
      configPromises.push(G4UMap.loadConfigFile(configs.translations).then(data => {
        this.set('translations', data)
      }))
    } else {
      Debug.error('No translations provided')
    }

    if (configs.hasOwnProperty('styles')) {
      configPromises.push(G4UMap.loadConfigFile(configs.styles).then(data => {
        this.set('styleMap', data)
      }))
    }

    return Promise.all(configPromises)
  }

  /**
   * Searches all controls of the specified name
   * @param {string} name
   * @returns {Control[]}
   */
  getControlsByName (name) {
    return this.controlsByName[name] || []
  }

  getControlsByType (type) {
    let matched = []
    for (const controls of Object.values(this.controlsByName)) {
      matched = matched.concat(controls.filter(c => c instanceof type))
    }
    return matched
  }

  /**
   * @param {Module} module
   */
  addModule (module) {
    module.setMap(this)
    this.modules_.push(module)
  }

  /**
   * @param {Module[]} modules
   */
  addModules (modules) {
    for (const module of modules) {
      this.addModule(module)
    }
  }

  /**
   * The listener is called once immediately after the next postrender event
   * @param listener
   */
  afterPostrender (listener) {
    this.once('postrender', () => setTimeout(listener, 0))
  }

  /**
   * @returns {Module[]}
   */
  getModules () {
    return this.modules_
  }

  /**
   * @param {ol.interaction.Interaction} interaction
   */
  removeInteraction (interaction) {
    let index

    this.defaultInteractions_.forEach(interactions => {
      index = interactions.indexOf(interaction)
      if (index > -1) {
        interactions.splice(index, 1)
      }
    })

    this.supersedingInteractions_.forEach(interactions => {
      index = interactions.indexOf(interaction)
      if (index > -1) {
        interactions.splice(index, 1)
      }
    })

    super.removeInteraction(interaction)
  }

  /**
   * Remove all interactions
   */
  removeInteractions () {
    while (this.getInteractions() && this.getInteractions().getLength()) {
      for (const interaction of this.getInteractions().getArray()) {
        this.removeInteraction(interaction)
      }
    }
  }

  /**
   * overwrite base method to notify developer about differing api
   */
  addInteraction () {
    throw new Error('Use addDefaultInteraction or addSupersedingInteraction')
  }

  /**
   * Add an interaction that should be active by default (i.e. in the normal state of the map)
   * @param {string} eventTypes a list of space separated eventtypes this interaction reacts on
   * @param {ol.interaction.Interaction} interaction
   */
  addDefaultInteraction (eventTypes, interaction) {
    for (const eventtype of eventTypes.split(' ')) {
      if (this.defaultInteractions_.has(eventtype)) {
        this.defaultInteractions_.get(eventtype).push(interaction)
      } else {
        this.defaultInteractions_.set(eventtype, [interaction])
      }
    }

    super.addInteraction(interaction)
  }

  /**
   * This deactivates all interactions which use a given event type
   * @param {string} eventType
   */
  deactivateInteractions (eventType) {
    for (const defInteraction of this.defaultInteractions_.get(eventType)) {
      defInteraction.setActive(false)
    }
    for (const supInteraction of this.supersedingInteractions_.get(eventType)) {
      supInteraction.setActive(false)
    }
  }

  /**
   * Reactivates all default interactions which use a specified event type
   * @param {string} eventType
   */
  activateInteractions (eventType) {
    for (const defInteraction of this.getDefaultInteractions(eventType)) {
      defInteraction.setActive(true)
    }
  }

  /**
   * Gets all interactions which use the specified event type
   * @param {string} eventType
   * @returns {ol.interaction.Interaction[]}
   */
  getDefaultInteractions (eventType) {
    return this.defaultInteractions_.get(eventType)
  }

  /**
   * This adds an interaction to the map which prohibits other interactions which use the same eventtype to be active
   * at the same time. When the superseding interaction is activated all affected ones get deactivated and vice versa
   * @param {string} eventTypes a list of space separated eventtypes this interaction reacts on
   * @param {ol.interaction.Interaction} interaction
   */
  addSupersedingInteraction (eventTypes, interaction) {
    const eventTypes_ = eventTypes.split(' ')

    const onActivation = () => {
      // deactivation of all other interactions with the same eventtypes

      for (const eventType of eventTypes_) {
        for (const supersedingInteraction of this.supersedingInteractions_.get(eventType)) {
          if (interaction !== supersedingInteraction) {
            supersedingInteraction.setActive(false)
          }
        }

        if (this.defaultInteractions_.get(eventType)) {
          for (const defaultInteraction of this.defaultInteractions_.get(eventType)) {
            defaultInteraction.setActive(false)
          }
        }
      }
    }

    const onDeactivation = () => {
      // reactivation of the default interactions
      // NOTE: if a superseding turned off another superseding interactions it won't reactivate it
      for (const eventType of eventTypes_) {
        if (this.defaultInteractions_.get(eventType)) {
          for (const defaultInteraction of this.defaultInteractions_.get(eventType)) {
            defaultInteraction.setActive(true)
          }
        }
      }
    }

    for (const eventType of eventTypes_) {
      if (this.supersedingInteractions_.has(eventType)) {
        this.supersedingInteractions_.get(eventType).push(interaction)
      } else {
        this.supersedingInteractions_.set(eventType, [interaction])
      }
    }

    if (interaction.getActive()) {
      onActivation()
    }

    interaction.on('change:active', /** ol.ObjectEvent */ e => {
      if (e.oldValue !== interaction.getActive()) {
        if (interaction.getActive()) {
          this.activating_ = true
          onActivation()
          this.activating_ = false
        } else {
          if (this.interactionDecativationTimeout_) {
            clearTimeout(this.interactionDecativationTimeout_)
          }

          if (!this.activating_) {
            this.interactionDecativationTimeout_ = setTimeout(onDeactivation, 500)
          }
        }
      }
    })

    super.addInteraction(interaction)
  }

  /**
   * @param {GroupLayer} groupLayer
   * @param {boolean} [silent=false] provide map to layers
   */
  setLayerGroup (groupLayer, silent = false) {
    if (!silent) {
      groupLayer.provideMap(this)
    }
    super.setLayerGroup(groupLayer)
  }

  /**
   * Remove all controls
   */
  removeControls () {
    let controls = this.getControls()

    if (controls) {
      // its neccessary to loop, problem caused by the nesting?
      while (controls.getLength() > 0) {
        controls.forEach(control => this.removeControl(control))
        controls = this.getControls()
      }
    }
  }
}