Home Reference Source

src/urlapi/URLAPI.js

import $ from 'jquery'

import { Debug } from '../Debug'
import { Query } from './Query'

import { availableLayersParam } from './handling/availableLayers'
import { centerParam } from './handling/center'
import { closeButtonParam } from './handling/closeButton'
import { configurationFileParam } from './handling/configurationFile'
import { fitRectangleParam } from './handling/fitRectangle'
import { languageParam } from './handling/language'
import { layerConfigurationFileParam } from './handling/layerConfigurationFile'
import { markerParam } from './handling/marker'
import { responsivenessParam } from './handling/responsiveness'
import { rotationParam } from './handling/rotation'
import { searchParam } from './handling/search'
import { visibleLayersParam } from './handling/visibleLayers'
import { zoomParam } from './handling/zoom'

/**
 * @callback SetToMapFunction
 * @param {G4UMap} map
 * @param {Query} query
 */

/**
 * @callback GetFromMapFunction
 * @param {G4UMap} map
 * @param {Query} query
 * @returns {object.<string,string>} result
 */

/**
 * @typedef {object} URLParameter
 * @property {string[]} keys an array of keys this URLParameter reacts to. If any of this are contined in the URL
 *    the setToMap function of this parameter is called.
 * @property {string} [setEvent] if this event is thrown by the map the setToMap function will be called.
 * @property {function} [SetToMapFunction] this function should adjust the map state according to the given keys.
 * @property {function} [GetFromMapFunction] this function is called to get the needed values from the map to create the
 *    needed URL Parameter(s). This function needs to return an object with key value pairs representing the values
 *    to be written in the URL
 */

/**
 * @typedef {object} URLAPIOptions
 * @property {G4UMap} map
 * @property {string[]} [excluded] parameters to exclude
 * @property {object} [init] set all properties to this initial values
 * @property {string} [mode='both'] possible values: 'js', 'url', 'both'
 */

/**
 * The purpose of the URLAPI is two-fold:
 *
 * 1)   to parse the page's search string, extract information like layers to
 *      be activated, the location, etc. and modify the map's state accordingly,
 *
 * 2)   to read the map's state and generate a link that said layers to be
 *      activated, the location, etc.
 */
export class URLAPI {
  /**
   * @param {URLAPIOptions} options
   */
  constructor (options) {
    /**
     * @type {G4UMap}
     * @private
     */
    this.map_ = options.map

    /**
     * In this object all initial values are stored. This is needed to create only the needed Parameters for an URL.
     * All getters are called once after the map is ready for the first time to fill this.
     * @type {object}
     * @private
     */
    this.initialValues_ = {}

    /**
     * @type {URLParameter[]}
     * @private
     */
    this.parameters_ = [
      availableLayersParam,
      centerParam,
      closeButtonParam,
      configurationFileParam,
      fitRectangleParam,
      languageParam,
      layerConfigurationFileParam,
      markerParam,
      responsivenessParam,
      rotationParam,
      searchParam,
      visibleLayersParam,
      zoomParam
    ]

    if (options.hasOwnProperty('moduleParameters') &&
      Array.isArray(options.moduleParameters) && options.moduleParameters.length) {
      this.parameters_ = this.parameters_.concat(options.moduleParameters)
    }

    /**
     * All used keys by all parameters
     * @type {string[]}
     * @private
     */
    this.parameterKeys_ = []

    /**
     * All getters by all parameters
     * @type {function[]}
     * @private
     */
    this.parameterGetters_ = []

    for (const parameterConf of this.parameters_) {
      // gather keys
      this.parameterKeys_ = this.parameterKeys_.concat(parameterConf.keys)

      // set values and gather values before setting to avoid unneccessary parameters
      if (parameterConf.hasOwnProperty('setToMap')) {
        this.map_.once(parameterConf.setEvent, () => {
          // read out initial value
          if (parameterConf.hasOwnProperty('getFromMap')) {
            $.extend(this.initialValues_, parameterConf.getFromMap(this.map_, this.query_))
          }
          // set value
          parameterConf.setToMap(this.map_, this.query_)
        })
      }

      // gather getters
      if (parameterConf.hasOwnProperty('getFromMap')) {
        this.parameterGetters_.push(parameterConf.getFromMap)
      }
    }

    const extractFromUrl = options.mode !== 'js'
    const jsValues = options.mode !== 'url' ? options.init : {}
    this.query_ = new Query(this.parameterKeys_, options.excluded || [], extractFromUrl, jsValues)
  }

  /**
   * Adds an layer which should be controlled with the URLAPI
   * @param {VectorLayer} layer needs to have a {{QuerySource}}
   * @param {string} key the key this layer should be controlled by
   */
  addApiLayer (layer, key) {
    if (this.parameterKeys_.indexOf(key) > -1) {
      Debug.error('Key is already in use.')
    } else if (key.toLowerCase() !== key) {
      Debug.error('Key should be lowercase.')
    } else {
      key = decodeURIComponent(key)
      const queryString = window.location.search
      const match = queryString.match(new RegExp('(\\?|&)' + key + '=(.*?)(&|$)', 'i'))
      if (match) {
        const value = decodeURIComponent(match[2]).trim()
        this.query_.setUrlValue(key, value)
      }

      // get
      const get = () => {
        const obj = {}
        obj[key] = layer.getSource().getQueryValues()
        return obj
      }

      this.parameterGetters_.push(get)
      this.initialValues_[key] = get()[key]

      // set
      if (this.query_.isSet(key)) {
        layer.getSource().setQueryValues(this.query_.getArray(key))
        layer.setVisible(true)
      }
    }
  }

  getCurrentParameters () {
    const values = {}

    /**
     * Compares if the two values are equal
     * @param valA
     * @param valB
     * @returns {boolean}
     */
    function equal (valA, valB) {
      if (typeof (valA) !== typeof (valB)) {
        return false
      }

      if (valA instanceof Array) {
        if (valA.length !== valB.length) {
          return false
        }
        for (let i = 0, ii = valA.length; i < ii; i++) {
          if (valA[i] !== valB[i]) {
            return false
          }
        }
        return true
      } else if (typeof (valA) === 'object') {
        throw new Error('object comparsion not supported atm.')
      } else {
        return valA === valB
      }
    }

    for (const getter of this.parameterGetters_) {
      const keyValuePairs = getter(this.map_, this.query_)
      if (keyValuePairs) {
        for (const key in keyValuePairs) {
          if (keyValuePairs.hasOwnProperty(key) && keyValuePairs[key] !== undefined) {
            // check if the value differs from the initial Value
            if (this.query_.isSet(key) ||
                !(this.initialValues_.hasOwnProperty(key) && equal(this.initialValues_[key], keyValuePairs[key]))) {
              values[key] = keyValuePairs[key].toString()
            }
          }
        }
      }
    }

    return values
  }

  /**
   * Create query string from map and optional name of config file
   * @param {object} [options={}]
   * @param {string} [options.baseURL] the base URL to use. Defaults to the current location.
   * @param {object} [options.parameters=undefined] additional parameters to add to the url.
   * @returns {string} Query string
   */
  makeURL (options = {}) {
    const baseURL = options.baseURL || window.location.href.match(/^[^?]*/)[0]

    const assignmentList = []

    const values = this.getCurrentParameters()

    for (const key in values) {
      assignmentList.push(encodeURIComponent(key) + '=' + encodeURIComponent(values[key]))
    }

    if (options.parameters !== undefined) {
      for (const key in options.parameters) {
        assignmentList.push(encodeURIComponent(key) + '=' + encodeURIComponent(options.parameters[key]))
      }
    }

    const url = baseURL || ''

    if (assignmentList.length === 0) {
      return url
    }

    if (url.indexOf('?') > -1) {
      return url + '&' + assignmentList.join('&')
    } else {
      return url + '?' + assignmentList.join('&')
    }
  }

  /**
   * Create query string from map and optional name of config file
   * @param {object} [options={}]
   * @param {string} [options.baseURL] the base URL to use. Defaults to the current location.
   * @returns {string} Query string
   */
  makeHTMLSnippet (options = {}) {
    const baseURL = options.baseURL || window.location.href.match(/^[^?]*/)[0]
    const rand = Math.random().toString(36).substring(7)
    const clientConfig = this.map_.get('configFileName')
    const layerConfig = this.map_.get('layerConfigFileName')
    const values = this.getCurrentParameters()
    delete values.conf
    delete values.layconf
    return `
      <div>
        <script src="${baseURL}/js/ol.js"></script>
        <script src="${baseURL}/js/jquery.min.js"></script>
        <script src="${baseURL}/js/g4u.js"></script>
        <link rel="stylesheet" href="${this.mapUrl}/css/g4u.css" />
        <div id="#g4u-${rand}"></div>
          <script>
            g4u.createMap('#g4u-${rand}', '${clientConfig}', '${layerConfig}', { apiInit: ${JSON.stringify(values)}})
        </script>
      </div>
    `
  }
}