Source: commonLayer/common-layer.js

/* eslint vars-on-top: 0, newline-after-var: 0, consistent-return:0, no-throw-literal:0 */

import { Group as LayerGroup } from 'ol/layer'
import { getZoomForScale } from '../services/scales-service'

/**
 * Module de gestion générique des couches et groupes
 * @module commonLayer
 */
const libNamespace = 'commonLayer'
/** Les groupes sont ajoutés au */
const GROUP_LAYER_TYPE = 'group'

/** Service de gestion générique des couches */
class CommonLayer {
  /** propriétés additionelles des calques */
  propertiesName = {
  /** Propriété définissant l'id du layer */
    ID_LAYER: '__gmapv_id',
    /** Propriété définissant le type du layer */
    TYPE_LAYER: '__gmapv_type',
    /** Propriété pour une ihm du layer */
    UI_LAYER: '__gmapv_ui',
    /** Propriété pour le groupe du layer */
    GROUP_LAYER: '__gmapv_group',
    /** on l'affiche dans le controle layerManager? */
    DISPLAY_IN_LAYER_SWITCHER: 'displayInLayerSwitcher',
    /** considéré comme "baseLayer" dans layerManager, un seul peut être afficher a la fois (dans un même groupe) */
    BASE_LAYER: 'baseLayer',
    /** pour background, si plusieurs background affichés, determine la projection prioritataire a utiliser */
    PROJECTION_PRIORITY: 'projectionPriority',
    // propriété pour sauvegarder la projection courante de la feature
    GEOMETRY_PROJECTION: '__gmapv_projection',
  }

  layerList = []
  mapModesAvailable = {}
  currentMapMode = null
  defaultMapMode = null

  /** instance du viewer
   * @type {import("../core/core").default}
   */
  ctx = null

  constructor (viewer, options) {
    viewer.COMMONLAYER_LOADED = true
    this.ctx = viewer
    Object.assign(this, options)

    // actions de maintenance lors de la suppression de calque
    this.ctx.Map.getLayers().on('remove', (event) => {
      const idLayer = event.element.get(this.propertiesName.ID_LAYER)
      if (idLayer) {
        // retire le calque des ressources utilisant un token (si il y en a)
        this.ctx.tokenManagerPool.removeResource('layer', idLayer)
      }
    })
  }

  /**
   * renvois les infos de type et d'id du calque
   * @param {ol.layer} layer
   * @returns {{type:string, id:string}} Type et Id du calque
   */
  getLayerInfos (layer) {
    return {
      type: layer.get(this.propertiesName.TYPE_LAYER),
      id: layer.get(this.propertiesName.ID_LAYER),
    }
  }

  /**
   * Récupère la liste des layers existant
   *
   * @param {String} type Type du layer
   * @returns {Array<ol.Layer>} Liste de layers
   */
  getLayers (type) {
    if (type) {
      return this.layerList.filter(layer =>
        layer.get(this.propertiesName.TYPE_LAYER) === type)
    }
    return this.layerList.slice()
  }

  /**
   * Récupère un layer
   *
   * @param {String|Number} id Identifiant du layer
   * @param {String} type Type du layer
   * @returns {ol.layer.Layer} Layer
   */
  getLayer (id, type) {
    return this.getLayers(type).find(layer =>
      layer.get(this.propertiesName.ID_LAYER) === id)
  }

  /**
   * Permet de récupérer la source d'un layer
   *
   * @param {String|Number} id Identifiant du layer
   * @param {String} type Layer
   * @return {ol.source.Source} Source du layer
   */
  getSource (id, type) {
    const layer = this.getLayer(id, type)
    return layer && layer.getSource && layer.getSource()
  }

  /**
   * Retourne la liste des identifiants des layers
   *
   * @param {String} type Type du layer
   * @returns {Array<String>} Liste des identifiants des layers
   */
  getLayersId (type) {
    return this.getLayers(type).map(layer =>
      layer.get(this.propertiesName.ID_LAYER))
  }

  /**
   * Permet de savoir si un layer existe
   *
   * @param {String|Number} id Identifiant du layer
   * @param {String} type Type du layer
   * @returns {Boolean} True si le layer existe
   */
  layerExist (id, type) {
    return this.getLayersId(type).includes(id)
  }

  /**
   * Ajout un layer sur la carte
   *
   * @param {ol.Layer} layer Layer à ajouter dans la carte
   * @param {String} type Type du layer
   * @param {Object} ui élement pour l'IHM si on utilise la layervisibilitybar
   * @param {string} ui.title tooltip du calque
   * @param {Element|string} ui.html contenu html du bouton a afficher
   * @param {string} ui.icon icone si html non saisie (utilisé comme <i class='icon'/>)
   */
  addLayer (layer, type, ui) {
    const layerName = layer.get(this.propertiesName.ID_LAYER)
    if (this.layerExist(layerName, type)) {
      throw new Error(`Layer ${layerName} already exists`)
    }

    // Ajoute le type du layer
    layer.set(this.propertiesName.TYPE_LAYER, type)
    if (ui) { layer.set(this.propertiesName.UI_LAYER, ui) }
    // Ajoute le layer à la carte et à la liste du module
    const idGroup = layer.get(this.propertiesName.GROUP_LAYER)
    const group = this.getGroup(idGroup)

    this.layerList.push(layer)
    if (group) {
      // si on ajoute dans un groupe tout en ayant le layermanager actif et "baseLayer"
      // on masque les autres calques du groupe (bugfix pour ol-ext)
      if (layer.getVisible() && layer.get(this.propertiesName.BASE_LAYER)) {
        group.getLayers().forEach(gLayer => {
          if (gLayer.get(this.propertiesName.BASE_LAYER)) {
            gLayer.setVisible(false)
          }
        })
      }
      group.getLayers().push(layer)
    } else {
      this.ctx.Map.addLayer(layer)
    }
  }

  /**
   * Retire un layer de la carte
   *
   * @param {String|Number} id Identifiant du layer
   * @param {String} type Type du layer
   * @fires removeLayer:id Lancé avant la suppression
   * @fires removedLayer:id Lancé après la suppression
   */
  removeLayer (id, type) {
    if (this.layerExist(id, type)) {
      this.ctx.dispatchEvent(`removeLayer:${id}`)
      const layer = this.getLayer(id, type)
      this.layerList.splice(this.layerList.indexOf(layer), 1)

      const idGroup = layer.get(this.propertiesName.GROUP_LAYER)
      const group = this.getGroup(idGroup)
      if (group) {
        group.getLayers().remove(layer)
      } else {
        this.ctx.Map.removeLayer(layer)
      }
      //
      this.ctx.dispatchEvent(`removedLayer:${id}`)
    }
  }

  /**
   * Permet de masquer un layer
   *
   * @param {String|Number} id Identifiant du layer
   * @param {String} type Type du layer
   */
  hideLayer (id, type) {
    if (this.layerExist(id, type)) {
      this.getLayer(id, type).setVisible(false)
    }
  }

  /**
   * Permet d'afficher un layer
   *
   * @param {String|Number} id Identifiant du layer
   * @param {String} type Type du layer
   */
  showLayer (id, type) {
    if (this.layerExist(id, type)) {
      const layer = this.getLayer(id, type)

      const idGroup = layer.get(this.propertiesName.GROUP_LAYER)
      const group = this.getGroup(idGroup)
      if (group && layer.get(this.propertiesName.BASE_LAYER)) {
        // si on ajoute dans un groupe tout en ayant le layermanager actif et "baseLayer"
        // on masque les autres calques du groupe (bugfix pour ol-ext)
        group.getLayers().forEach(gLayer => {
          if (gLayer.get(this.propertiesName.BASE_LAYER)) {
            gLayer.setVisible(false)
          }
        })
      }
      layer.setVisible(true)
    }
  }

  /**
   * Permet de remonter d'un niveau un layer
   *
   * @param {String|Number} id Identifiant du layer
   * @param {String} type Type du layer
   * @returns {Boolean} Retourne true si le layer n'est pas au plus haut
   */
  upLayer (id, type) {
    if (this.layerExist(id, type)) {
      const mapLayers = this.ctx.Map.getLayers()
      const index = mapLayers.getArray().indexOf(this.getLayer(id, type))
      // si il n'est pas déja au plus haut
      if (index + 1 < mapLayers.getLength()) {
        mapLayers.insertAt(index + 1, mapLayers.removeAt(index))
        return index + 1 !== mapLayers.length
      }
    }
    return false
  }

  /**
   * Permet de descendre d'un niveau un layer
   *
   * @param {String|Number} id Identifiant du layer
   * @param {String} type Type du layer
   * @return {Boolean} Retourne True si le layer n'est pas au plus bas
   */
  downLayer (id, type) {
    if (this.layerExist(id, type)) {
      const mapLayers = this.ctx.Map.getLayers()
      const index = mapLayers.getArray().indexOf(this.getLayer(id, type))
      if (index > 0) {
        mapLayers.insertAt(index - 1, mapLayers.removeAt(index))
        return index - 1 !== 0
      }
    }
    return false
  }

  /**
   * Permet de notifier au layer qu'il à changé
   *
   * @param {String|Number} id Identifiant du layer
   * @param {String} type Type du layer
   */
  layerChanged (id, type) {
    if (this.layerExist(id, type)) {
      this.getLayer(id, type).changed()
    }
  }

  /**
   * Force un nouveau rendu du layer sur la carte
   * Refreshes the source. The source will be cleared, and data from the server will be reloaded.
   *
   * @param {String|Number} id Identifiant du layer
   * @param {String} type Type du layer
   */
  refreshLayer (id, type) {
    if (this.layerExist(id, type)) {
      this.getLayer(id, type).getSource().refresh()
    }
  }

  /**
   * Permet de connaitre la visibilité d'un layer (en fonction de min/max zoom, propriété "visible" & group "visible")
   *
   * @param {String|Number} id Identifiant du layer
   * @return {Boolean} True si visible
   */
  isVisible (id, type) {
    if (this.layerExist(id, type)) {
      return this.getLayer(id, type).isVisible()
    }
  }

  /**
   * ajoute un groupe a la carte

   * @param {Object} options Paramètres compatibles ol.layer.group
   */
  addGroup (idGroup, options) {
    const group = new LayerGroup({
      [this.propertiesName.ID_LAYER]: idGroup,
      ...options,
    })

    // Ajoute le type du layer car un groupe est ajouté aux layers
    // group.set(this.propertiesName.TYPE_LAYER, GROUP_LAYER_TYPE)
    this.addLayer(group, GROUP_LAYER_TYPE)
  }

  /** Renvoit tout les groupes présents dans la carte */
  getGroups () {
    return this.getLayersId(GROUP_LAYER_TYPE)
  }

  /** Renvoit un groupe par son id
  * @param {string} idGroup Id du groupe
  */
  getGroup (idGroup) {
    return this.getLayer(idGroup, GROUP_LAYER_TYPE)
  }

  /**
   * Est ce que l'entrée est un calque ou un groupe?
   * @param {ol.layer|ol.layer.group} groupOrLayer
   * @returns {boolean} l'entrée est un calque ou un groupe ?
   */
  isGroup (groupOrLayer) {
    return groupOrLayer.get(this.propertiesName.TYPE_LAYER) === GROUP_LAYER_TYPE
  }

  /**
   * renvoit le groupe d'un calque
   *
   * @param {ol.layer} layer Calque OpenLayer
   * @returns group
   */
  getLayerGroup (layer) {
    // const layer = this.getLayer(id, type)
    if (!layer) { return null }
    const idGroup = layer.get(this.propertiesName.GROUP_LAYER)
    const group = this.getGroup(idGroup)
    return group || null
  }

  /**
   * Mets à jours les propriétés OpenLayer ou Karteis Mapviewer d'un calque
   * @param {ol.layer} layer Calque OpenLayer
   * @param {Object} options Options possibles pour les calques
   */
  updateCommonProperties (layer, options) {
    if (options.visible !== undefined) {
      layer.setVisible(options.visible)
    }
    if (options.opacity !== undefined) {
      layer.setOpacity(options.opacity)
    }
    if (options.minZoom !== undefined) {
      layer.setMinZoom(options.minZoom)
    }
    if (options.maxZoom !== undefined) {
      layer.setMaxZoom(options.maxZoom)
    }
    // les notions de zoom & scale/résolutions sont inversées (max zoom = proche du sol)
    if (options.minScale !== undefined) {
      layer.setMaxZoom(getZoomForScale(this.ctx.Map.getView(), options.minScale))
    }
    if (options.maxScale !== undefined) {
      layer.setMinZoom(getZoomForScale(this.ctx.Map.getView(), options.maxScale))
    }
    if (options.idGroup !== undefined) {
      layer.set(this.propertiesName.GROUP_LAYER, options.idGroup)
    }
    if (options.baseLayer !== undefined) {
      layer.set(this.propertiesName.BASE_LAYER, options.baseLayer)
    }
    if (options.displayInLayerSwitcher !== undefined) {
      layer.set(this.propertiesName.DISPLAY_IN_LAYER_SWITCHER, options.displayInLayerSwitcher)
    }
    if (options.projectionPriority !== undefined) {
      layer.set(this.propertiesName.PROJECTION_PRIORITY, options.projectionPriority)
    }
    if (options.ui !== undefined) {
      layer.set(this.propertiesName.UI_LAYER, options.ui)
    }
  }

  /**
   * Ajoute un mode de carte
   * @param {Object} options
   * @param {string} options.name nom
   * @param {Array<ol.interaction>} options.interactions
   * @param {Array<ol.control>} options.controls
   * @param {function} options.onactive
   * @param {function} options.oninactive
   * @param {Boolean} options.isDefault
  */
  createMapMode ({
    name,
    interactions = [],
    controls = [],
    onactive = () => {},
    oninactive = () => {},
    isDefault = false,
  } = {}) {
    if (this.mapModesAvailable[name]) {
      throw new Error(`Mode ${name} already exists`)
    }

    this.mapModesAvailable[name] = {
      interactions: Array.isArray(interactions) ? interactions : [interactions],
      controls: Array.isArray(controls) ? controls : [controls],
      onactive,
      oninactive,
    }

    // S'il doit-être utilisé en tant que mode par défaut
    if (isDefault) {
      this.defaultMapMode = name
      this.setMapMode(this.defaultMapMode)
    }
  }

  removeMapMode (name) {
    if (this.mapModesAvailable[name]) {
      // Si c'est le mode actf, le désactive avant tout
      if (this.currentMapMode.name === name) {
        this.disableCurrentMapMode()
      }
      this.mapModesAvailable[name] = undefined
    }
  }

  upsertMapMode ({ name, ...options }) {
    if (!this.mapModesAvailable[name]) {
      return this.createMapMode({ name, ...options })
    }

    // S'il s'agit du mode actif, on le relance en même temps
    if (this.currentMapMode.name === name) {
      this.disableCurrentMapMode({ silent: true })
      this.mapModesAvailable[name] = { name, ...options }
      this.setMapMode(name, { silent: true })
    } else {
      this.mapModesAvailable[name] = { name, ...options }
    }
  }

  disableCurrentMapMode ({ silent } = {}) {
    if (this.currentMapMode) {
      // Supprime les interactions
      this.currentMapMode.interactions.forEach(interaction => {
        // ndhe: avec la nouvelle version d'ol-ext une interraction peut activer/desactiver des contrôles lorsqu'on l'active/desactive.
        // du coup on la desactive avant de la supprimer
        interaction.setActive(false)
        this.ctx.Map.removeInteraction(interaction)
      })

      this.currentMapMode.controls.forEach(control => {
        this.ctx.Map.removeControl(control)
      })

      // Lance les traitements lors de la désactivation du mode
      this.currentMapMode.oninactive()
      this.currentMapMode = null

      if (!silent) {
        this.ctx.dispatchEvent('change:mapmode', { name: null, toolId: null })
      }
    }
  }

  applyMapMode ({
    name,
    interactions = [],
    controls = [],
    onactive = () => {},
    oninactive = () => {},
    silent,
    toolId,
  }) {
    // Désactive l'ancien mode
    this.disableCurrentMapMode({ silent: true })

    // Initialise le nouveau mode
    this.currentMapMode = { name, interactions, controls, onactive, oninactive, toolId }
    interactions.forEach(interaction => {
    // ndhe: avec la nouvelle version d'ol-ext une interraction peut activer/desactiver des contrôles lorsqu'on l'active/desactive.
      // du coup on l'active avant de la supprimer
      interaction.setActive(true)
      this.ctx.Map.addInteraction(interaction)
    })

    this.currentMapMode.controls.forEach(control => {
      this.ctx.Map.addControl(control)
    })

    // Lance les traitements lors de la désactivation du mode
    onactive()

    // Envoie un event hors de gmapv
    if (!silent) {
      this.ctx.dispatchEvent('change:mapmode', { name, toolId })
    }
  }

  setMapMode (name = this.defaultMapMode, { silent, toolId } = {}) {
    // Pas de nouveau mode de carte
    if (!name) {
      return this.disableCurrentMapMode({ silent })
    }

    // Mode non disponible
    if (!this.mapModesAvailable[name]) {
      throw new Error(`Mode ${name} dosen't exists`)
    }

    // Applique le nouveau mode sur la carte
    if (!this.currentMapMode || this.currentMapMode.name !== name || (toolId && this.currentMapMode.toolId !== toolId)) {
      this.applyMapMode({ name, ...this.mapModesAvailable[name], silent, toolId })
    }
  }

  getCurrentMapMode () {
    return this.currentMapMode
  }

  getCurrentToolId () {
    return this.currentMapMode && this.currentMapMode.toolId
  }

  isCurrentMapMode (name) {
    return this.currentMapMode && this.currentMapMode.name === name
  }
}

// Permet d'etendre le module
export default function extendCoreLib (options) {
  return function patch (viewer) {
    const functions = { }

    functions[libNamespace] = new CommonLayer(viewer, options)
    return Object.assign(viewer, functions)
  }
}