Source: datalayer/data-layer.js

import * as Extent from 'ol/extent'
import * as Easing from 'ol/easing'

import { Style, Stroke, Fill, Circle, Text } from 'ol/style'

import * as Condition from 'ol/events/condition'

import { unByKey } from 'ol/Observable'
import Feature from 'ol/Feature'
import Collection from 'ol/Collection'
import * as Tilegrid from 'ol/tilegrid'

import Layer from 'ol/layer/Layer'
import VectorTile from 'ol/layer/VectorTile'
import VectorLayer from 'ol/layer/Vector'

import Cluster from 'ol/source/Cluster'
import VectorSource from 'ol/source/Vector'

import { Select, Draw, Modify, Translate, DragBox } from 'ol/interaction'
import { Point, LineString } from 'ol/geom'

import GeoJSON from 'ol/format/GeoJSON'

import * as proj from 'ol/proj'
import { getVectorContext } from 'ol/render'

import geojsonvt from 'geojson-vt'

import Zonal from '../interaction/zonal'
import Measure from '../interaction/Measure'
import CustomDrawTouch from '../interaction/CustomDrawTouch'
import CustomDraw from '../interaction/CustomDraw'

import * as MapviewerServices from '../tools/mapviewer-services'
import IsRequired from '../utils/IsRequired'

import { getPointForGeometry } from '../tools/services/centroid'

import ControlModifyTools from '../control/ControlModifyTools'
import CustomHeatmap from '../layer/CustomHeatMap'

import { bbox as bboxStrategy, tile as tileStrategy } from 'ol/loadingstrategy'
import { createXYZ } from 'ol/tilegrid'
import Guid from '../utils/Guid'

import { isSplitPossible, splitFeature } from '../tools/services/featureSplit'
import { geoJsonToFeature } from '../tools/services/geojsons'
import { getZoomForScale } from '../services/scales-service'

import { getWfsVectorSource } from '../utils/wfs-utils'

/**
* @typedef {Object} Mapviewer
*/

/**
 * Module de gestion des couches vectorielles de données
 * @module dataLayer
 */

/**
 * @typedef {String} MapMode Liste des modes de carte disponibles
 */

/**
 * @typedef {String} SelectMode
 */

const libNamespace = 'dataLayer'

const STYLE_NONE = [
  new Style({
    display: 'none',
  }),
]

const STYLE_CLUSTER_LINK = [
  new Style({
    stroke: new Stroke({
      color: '#FFF',
      width: 1,
    }),
  }),
]

const CACHE_CLUSTER = {}

const DEFAULT_CLUSTER_STYLE_FUNCTION = function ({ features, cluster, resolution, zoom }) {
  if (!CACHE_CLUSTER[features.length]) {
    CACHE_CLUSTER[features.length] = [
      new Style({
        image: new Circle({
          radius: 20,
          stroke: new Stroke({
            color: '#FAFAFA',
            width: 2,
          }),
          fill: new Fill({
            color: '#9400d3',
          }),
        }),
        text: new Text({
          text: `${features.length}`,
          font: 'bold 16px Roboto',
          offsetY: 2,
          fill: new Fill({
            color: '#FAFAFA',
          }),
        }),
      }),
    ]
  }
  return CACHE_CLUSTER[features.length]
}

const DATA_LAYER_TYPE = 'data'

/** Service de couche vectorielle */
class DataLayer {
  /** Nom des propriétés systeme dans une feature */
  propertiesName = {
    // Propriété définissant si la feature est visible
    VISIBLE_FEATURE: '__gmapv_visible',
    // Propriété définissant si la feature est selectionnée
    SELECTED_FEATURE: '__gmapv_located',
    // Propriété définissant si la feature est activée
    ACTIVATED_FEATURE: '__gmapv_activated',
    // Fonction de filtre de cluster
    CLUSTER_FILTER: '__gmapv_clusterFilter',
    // Propriété pour la géométrie alternative un style
    STYLE_GEOMETRY_POINT: '__gmapv_style_geometry_point',
    // propriété pour sauvegarder les tags
    TAG_FEATURE: '__gmapv_tags',
  }

  /** Nom des tags systeme dans les tags */
  systemTagName = {
    HOVERED_FEATURE: '__gmapv_hovered',
  }

  /**  Permet d'exclure des layers de la selection */
  excludeLayers = []

  /** types de géométries sélectionnable en mode rectangle */
  geometrieTypesSelectableInRectangularMode = []
  /** peut-on sélectionner les features en mode rectangle ? */
  selectHiddenFeatureInRectangularMode = false

  createStyle = null // Style pour la création
  /** Calques survolable */
  hoverableLayer = []
  /** Styles pour chaques calques */
  _layersStyle = {}

  /** instance du viewer
   * @type Mapviewer
   */
  ctx = null

  /**
   *
   * @param {Mapviewer} viewer
   * @param {Object} options options génériques du datalayer
   */
  constructor (viewer, options) {
    viewer.DATALAYER_LOADED = true
    /** @type Mapviewer */
    this.ctx = viewer

    /** @type {SelectMode} */
    this.selectMode = this.setSelectMode(DataLayer.SelectModes.REPLACE)

    Object.assign(this, options)

    this.created()
  }

  // permet de mapper les interaction select sur nos styles
  _styleSelectedFeature () {
    return (feature, resolution) => {
      // Dans le cas d'un cluster, on applique le style du layer dominant
      if (feature.get('features')) {
        // Récupère tous les identifiants de layers
        const layersId = feature.get('features')
          .map(feature => feature.layerSourceId_)
          .flat()

        // On récupère le layer avec le plus d'occurrence
        const layerId = layersId
          .sort((a, b) =>
            layersId.filter(v => v === a).length -
            layersId.filter(v => v === b).length)
          .pop()

        // Retourne le style pour le layer dominant
        return this._layersStyle[layerId](feature, resolution)
      }

      if (feature.get(this.propertiesName.VISIBLE_FEATURE) === false) {
        return false
      }
      // On va merger toutes les fonction de style de chaque layer où la feature
      // est présente. Ce cas peut se présenter si on a un layer de polygon et
      // un layer de point alors que la feature est la même pour les deux
      // layers. Il faudra s'assurer que le style contienne la geometry sur
      // laquelle appliquer le filtre (côté client)
      return feature.layerSourceId_
        .filter(id => this.isVisible(id) && this._layersStyle[id] && !this.excludeLayers.includes(id))
        .map(id => this._layersStyle[id](feature, resolution))
        .flat()
        .filter(style => style)
    }
  }

  /**
   * Fonction lancée à l'instanciation du module
   */
  created () {
    /** @type {Collection<Feature>} */
    this._currentSelection = new Collection()

    // Definition de l'interaction de selection propre au module dataLayer
    this._selectInteraction = new Select({
      condition: Condition.click,
      toggleCondition: (evt) => {
        return (this.selectMode === DataLayer.SelectModes.TOGGLE && Condition.click(evt)) ||
          (this.selectMode === DataLayer.SelectModes.REPLACE && Condition.shiftKeyOnly(evt))
      },
      addCondition: (evt) => {
        return this.selectMode === DataLayer.SelectModes.ADD && Condition.click(evt)
      },
      // removeCondition: (evt) => { return false/* this.selectMode === DataLayer.SelectModes.ADD */ },
      // On ne récupère que les layers de données non exclus
      layers: layer => {
        const layerId = layer.get(this.commonLayer.propertiesName.ID_LAYER)
        return layerId && this.layerExist(layerId) && this.isSelectableLayer(layer)
      },
      style: this._styleSelectedFeature(),
      hitTolerance: this.ctx.hitTolerance,
      features: this._currentSelection,
    })

    // On retransmet l'event hors de gmapv sous un aurtes nom
    this._selectInteraction.on('select', ev => this._dispatchSelectEvent(ev))

    this._zonalSelectInteraction = new Zonal({
      // On ne récupère que les layers de données non exclus
      layers: layer => {
        const layerId = layer.get(this.commonLayer.propertiesName.ID_LAYER)
        return layerId && this.layerExist(layerId) && this.isSelectableLayer(layer)
      },
      style: this._styleSelectedFeature(),
      features: this._currentSelection,
      filter: (feature) => { return feature.getGeometry().getType() === 'Point' && feature.get(this.propertiesName.VISIBLE_FEATURE) !== false },
      viewer: this.ctx,
    })

    this._zonalSelectInteraction.on('select', ev => this._dispatchSelectEvent(ev))

    // a DragBox interaction used to select features by drawing boxes
    this._dragBoxInteraction = new DragBox({})

    // clear selection when drawing a new box and when clicking on the map
    this._dragBoxInteraction.on('boxstart', () => {
      this._dragBoxInteraction.once('boxend', ev => {
        onDrawSelection.call(this, {
          geometry: this._dragBoxInteraction.getGeometry(),
          filter: feature => this.isSelectableInRectangularMode(feature),
          includeHidden: this.selectHiddenFeatureInRectangularMode,
        })
      })
    })

    this._polygonSelectInteraction = new Draw({
      type: 'Polygon',
    })

    this._polygonSelectInteraction.on('drawstart', () => {
      this._polygonSelectInteraction.once('drawend', ev => {
        onDrawSelection.call(this, { geometry: ev.feature.getGeometry() })
      })
    })

    function onDrawSelection (options) {
      const polygon = options?.geometry
      const filter = options.filter ? options.filter : () => true
      const includeHidden = options?.includeHidden || false

      if (!polygon) {
        console.warn('onDrawSelection appelé sans géométrie')
        return
      }

      const extent = polygon.getExtent()

      const featuresInExtent = this.getFeaturesInExtent(extent, {
        layers: this.getSelectableLayers(),
        filter,
        includeHidden,
      })

      const nextSelection = featuresInExtent.filter(feature =>
        MapviewerServices.intersectsGeometry(polygon, feature.getGeometry()))

      // ancienne selection
      const lastSelection = this.getCurrentSelection()
      let selected = []
      let deselected = []
      switch (this.selectMode) {
        case DataLayer.SelectModes.TOGGLE:
          selected = nextSelection.filter(feature => !lastSelection.includes(feature))
          deselected = lastSelection.filter(feature => nextSelection.includes(feature))
          break
        case DataLayer.SelectModes.ADD:
          selected = nextSelection.filter(feature => !lastSelection.includes(feature))
          break
        case DataLayer.SelectModes.REPLACE:
        default:
          selected = nextSelection
          deselected = lastSelection.filter(feature => !nextSelection.includes(feature))
      }

      // modifie la sélection OL
      deselected.forEach(feature => this._currentSelection.remove(feature))
      selected.forEach(feature => this._currentSelection.push(feature))
      // Dispatch l'event vers l'exterieur
      this._dispatchSelectEvent({ selected, deselected })
    }

    // Définition de l'interaction de modification
    this._modifyInteraction = new Modify({
      features: this._currentSelection,
    })

    // Gestion de l'evenement de modification de feature
    this._modifyInteraction.on('modifyend', ev => {
      this.ctx.dispatchEvent('change:features', {
        features: ev.features.getArray().slice(),
      })
    })

    // Définition de l'interaction de drag & drop (translate)
    this._dragAndDropInteraction = new Translate({
      layers: (layer) => {
        const layerId = layer.get(this.commonLayer.propertiesName.ID_LAYER)
        const isDraggable = this._dragAndDropInteraction.get('draggableLayerList').includes(layerId)
        return isDraggable
      },
      active: false,
    })
    // Passer une liste vide au debut
    this._dragAndDropInteraction.set('draggableLayerList', [])

    // Gestion de l'evenement de drag & drop de feature
    this._dragAndDropInteraction.on('translatestart', evt => {
      const common = {
        evenType: evt.type,
        coordinate: evt.coordinate,
        startCoordinate: evt.startCoordinate,
      }
      // Si des features sont superposées
      evt.features.forEach((feature) => {
        const rtn = {
          ...common,
          originalGeometry: feature.getGeometry(),
          modifiedFeature: feature,
          toolId: this.ctx.commonLayer.getCurrentToolId(),
        }
        this.ctx.dispatchEvent('modifyfeature:beforeModify', rtn)
      })
    })

    this._dragAndDropInteraction.on('translateend', evt => {
      const common = {
        evenType: evt.type,
        coordinate: evt.coordinate,
        startCoordinate: evt.startCoordinate,
      }
      // Si des features sont superposées
      evt.features.forEach((feature) => {
        const rtn = {
          ...common,
          originalGeometry: feature.getGeometry(),
          modifiedFeature: feature,
          toolId: this.ctx.commonLayer.getCurrentToolId(),
        }
        this.ctx.dispatchEvent('modifyfeature:modify', rtn)
      })
    })

    this._drawPointTouchInteraction = this.ctx.isDesktop
      ? new CustomDraw({
        active: false,
        type: 'Point',
        stopClick: true,
        viewer: this.ctx,
      })
      : new CustomDrawTouch({
        active: false,
        type: 'Point',
        viewer: this.ctx,
      })

    this._drawPointTouchInteraction.on('drawend', ev => {
      this.ctx.dispatchEvent('drawend', ev)
    })

    this._drawLineStringTouchInteraction = this.ctx.isDesktop
      ? new CustomDraw({
        active: false,
        type: 'LineString',
        stopClick: true,
        viewer: this.ctx,
      })
      : new CustomDrawTouch({
        active: false,
        type: 'LineString',
        viewer: this.ctx,
      })

    this._drawLineStringTouchInteraction.on('drawend', ev => {
      this.ctx.dispatchEvent('drawend', ev)
    })

    this._drawPolygonTouchInteraction = this.ctx.isDesktop
      ? new CustomDraw({
        active: false,
        type: 'Polygon',
        stopClick: true,
        viewer: this.ctx,
      })
      : new CustomDrawTouch({
        active: false,
        type: 'Polygon',
        viewer: this.ctx,
      })

    this._drawPolygonTouchInteraction.on('drawend', ev => {
      this.ctx.dispatchEvent('drawend', ev)
    })

    this.ctx.on('change:padding', ({ padding }) => {
      this._drawPointTouchInteraction.setPadding(padding)
      this._drawLineStringTouchInteraction.setPadding(padding)
      this._drawPolygonTouchInteraction.setPadding(padding)
    })

    this._measureLineInteraction = new Measure({
      mode: 'length',
      viewer: this.ctx,
      active: false,
    })

    this._measureLineInteraction.on('measureend', event => this.ctx.dispatchEvent('measureend', event))

    this._measureAreaInteraction = new Measure({
      mode: 'area',
      viewer: this.ctx,
      active: false,
    })

    this._measureAreaInteraction.on('measureend', event => this.ctx.dispatchEvent('measureend', event))

    // Mode de selection
    this.ctx.commonLayer.createMapMode({
      name: 'select',
      interactions: this._selectInteraction,
      isDefault: true,
      onactive: () => {
        this.setSelectMode(DataLayer.SelectModes.REPLACE)
      },
    })

    // Mode de selection multiples
    this.ctx.commonLayer.createMapMode({
      name: 'multiselect',
      interactions: this._selectInteraction,
      onactive: () => {
        this.setSelectMode(DataLayer.SelectModes.TOGGLE)
      },
    })

    // Mode de selection rectangulaire
    this.ctx.commonLayer.createMapMode({
      name: 'rectangular',
      interactions: [this._selectInteraction, this._dragBoxInteraction],

    })

    // Mode de selection zonale
    this.ctx.commonLayer.createMapMode({
      name: 'zonal',
      interactions: this._zonalSelectInteraction,
    })

    // Mode de selection polygonale
    this.ctx.commonLayer.createMapMode({
      name: 'polygon',
      interactions: this._polygonSelectInteraction,
    })

    // Mode de modification
    this.ctx.commonLayer.createMapMode({
      name: 'modify',
      interactions: [this._selectInteraction, this._modifyInteraction],
    })
    // Mode de drad & drop des features d'un layer
    this.ctx.commonLayer.createMapMode({
      name: 'dragAndDrop',
      interactions: [this._dragAndDropInteraction],
      oninactive: () => {
        // passer une liste vide pour desactiver les layers
        this._dragAndDropInteraction.set('draggableLayerList', [])
      },
    })

    // Mode de modification tactile
    this.ctx.commonLayer.createMapMode({
      name: 'modifyPointTouch',
      interactions: this._modifyPointTouchInteraction,
      onactive: () => {
        const selection = this.getCurrentSelection()
        if (selection.length === 1) {
          this._modifyPointTouchInteraction.setFeature(selection[0])
        }
      },
    })

    // Mode de mesure de ligne
    this.ctx.commonLayer.createMapMode({
      name: 'measureLine',
      interactions: this._measureLineInteraction,
      onactive: () => {
        this._measureLineInteraction.startMeasurement()
      },
    })

    // Mode de mesure de surface
    this.ctx.commonLayer.createMapMode({
      name: 'measureArea',
      interactions: this._measureAreaInteraction,
      onactive: () => {
        this._measureAreaInteraction.startMeasurement()
      },
    })

    // Mode de dessin de point
    this.ctx.commonLayer.createMapMode({
      name: '_drawPointTouch',
      interactions: this._drawPointTouchInteraction,
    })

    // Mode de dessin de ligne tactile
    this.ctx.commonLayer.createMapMode({
      name: '_drawLineStringTouch',
      interactions: this._drawLineStringTouchInteraction,
    })

    // Mode de dessin de polygon tactile
    this.ctx.commonLayer.createMapMode({
      name: '_drawPolygonTouch',
      interactions: this._drawPolygonTouchInteraction,
    })

    // fonction au changement de view
    this.ctx.getMap().on('change:view', this.onViewChange.bind(this))
  }

  /**
   * 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
   */

  onViewChange (event) {
    // lorsque l'on change de view, si la projection a évoluée, on reprojete les données
    const oldProjection = event.oldValue.getProjection().getCode()
    const newProjection = event.target.getView().getProjection().getCode()
    if (!oldProjection || !newProjection || oldProjection === newProjection) {
      // pas de srid, ou le même, rien a faire
      return
    }

    // sinon on reprojete les géométrie du srid existant vers le nouveau
    this.getLayers().forEach(layer => {
      const layerInfos = this.viewer.commonLayer.getLayerInfos(layer)
      const source = this.getSourceLayer(layerInfos.id)

      source.getFeatures().forEach((feature) => {
        // on peut avoir mit la même feature dans plusieurs calques, on sauvegarde le fait d'avoir changé sa projection
        if (feature.getGeometry() !== null && feature.get(this.viewer.commonLayer.propertiesName.GEOMETRY_PROJECTION) !== newProjection) {
          feature.getGeometry().transform(oldProjection, newProjection)
          feature.set(this.viewer.commonLayer.propertiesName.GEOMETRY_PROJECTION, newProjection, true)
        }
      })
    })
  }

  // Est-on dans un mode qui empêche de faire du survol de feature
  hasNonHoverableInteractionInProgress () {
    return ['_drawPointTouch', '_drawLineStringTouch', '_drawPolygonTouch', 'measureArea', 'measureLine'].includes(this.ctx.commonLayer.getCurrentMapMode()?.name)
  }

  /**
   * Permet de mettre à plat les features
   * Récupère que les features fonctionnelle même s'il
   * y a des features de cluster en paramètre
   *
   * @param {Feature} features Liste des features
   */
  flattenFeatures (features) {
    return features.map(feature => {
      const clusterizedFeatures = feature.get('features')
      if (!clusterizedFeatures) {
        return feature
      }

      if (clusterizedFeatures.length) {
        // TODO: Revoir le mode de selection pour ce cas là
        const layerId = clusterizedFeatures[0].layerSourceId_[0]

        return this.getSelectableFeatures(
          feature,
          this.getLayer(layerId),
        )
      }

      return null
    }).flat()
  }

  /**
   * Dispatch un event MapViewer à partir d'un event OL
   *
   * @param {ol.SelectEvent} event Evenement de sélection Openlayers
   * @private
   */
  _dispatchSelectEvent ({ selected = [], deselected = [], mapBrowserEvent }) {
    // S'assure de la non présence de cluster
    const flatDeselected = this.flattenFeatures(deselected)
    const flatSelected = this.flattenFeatures(selected)/* .filter((featSelected) => {
      const selectedId = featSelected.getId()
      return !flatDeselected.find((featUnselected) => featUnselected.getId() === selectedId)
    }) */
    // retire une feature désélectionné de la liste sélectionné (arrive avec les clusters)

    flatSelected.forEach(feature => feature.set(this.propertiesName.SELECTED_FEATURE, true))
    flatDeselected.forEach(feature => feature.set(this.propertiesName.SELECTED_FEATURE, false))

    // Dispatch l'event de MapViewer
    this.ctx.dispatchEvent('change:selection', {
      selected: flatSelected.slice(),
      deselected: flatDeselected.slice(),
      selection: this._currentSelection.getArray().slice(),
      coordinate: mapBrowserEvent && mapBrowserEvent.coordinate,
      toolId: this.ctx.commonLayer.getCurrentToolId(),
    })
  }

  /**
   * Permet de savoir si le layer est sélectionnable actuellement
   *
   * @param {ol.Layer} layer Layer
   * @returns {Boolean} True si le layer est sélectionnable
   */
  isSelectableLayer (layer) {
    const layerId = layer.get(this.commonLayer.propertiesName.ID_LAYER)
    return layer.getVisible() &&
      !this.excludeLayers.includes(layerId)
  }

  /**
   * Permet de savoir si le layer est survolable actuellement
   *
   * @param {ol.Layer} layer Layer
   * @returns {Boolean} True si le layer est survolable
   */
  isHoverableLayer (layer) {
    const layerId = layer.get(this.commonLayer.propertiesName.ID_LAYER)
    return layer.getVisible() &&
      this.hoverableLayer.includes(layerId)
  }

  /**
   * Retourne les layers actuellement sélectionnable
   *
   * @returns {Array<ol.Layer>} Liste des layers sélectionnable
   */
  getSelectableLayers () {
    return this.getLayers().filter(layer => this.isSelectableLayer(layer))
  }

  /**
   * Permet de récuéprer les features réellement sélectionnées à partir
   * d'une feature. Retourne toujours un tableau
   * Nécessaire sur un cluster pour récupérer le sous-ensemble
   *
   * @param {Feature} feature Feature normal ou issue d'un cluster
   * @param {ol.Layer} layer Layer de la feature
   * @param {number} resolution Resolution de la carte
   * @param {bool} includeHidden Inclure les feature cachée?
   * @returns {Array<Feature>} Liste des features fonctionnelles visibles
   */
  getSelectableFeatures (feature, layer, resolution = this.ctx.Map.getView().getResolution(), includeHidden = false) {
    // Dans le cas d'une feature de cluster à destructurer
    // NDHE: Si on à sélectionné des feature via leur cluster puis que l'on a zoomé
    //       isCluster n'est plus sur une source cluster..
    if (/* this.isCluster(layer) && */ feature.get('features')) {
      // Récupère le filtre de cluster
      const clusterFilter = layer.get(this.propertiesName.CLUSTER_FILTER)
      // Filtre les features visibles
      return clusterFilter(feature.get('features'), resolution)
    }

    // Evaluation de la visiblité si nécessaire
    if (this.isVisibleFeature({ feature, resolution, includeHidden })) {
      return [feature]
    }

    return []
  }

  /**
   * Permet de récupérer des features selectionnables présentes dans une extent
   *
   * @param {ol.Extent} extent Extent de l'extraction
   * @param {object} options Options
   * @param {Array<ol.Layer>|undefined} options.layers Liste des layers dans lesquelle rechercher. Par défaut, tout les layers de données
   * @param {number|undefined} options.resolution Résolution de la carte. Par defaut, celle de la carte principale.
   * @param {Function|undefined} options.filter Permet de filtrer les features retournées. La fonction prend une feature en argument et retourne un boolean. Par défaut, pas de filtre
   * @returns {Array<Feature>} Liste des features dans l'extent
   */
  getFeaturesInExtent (extent, {
    layers = this.getLayers(),
    resolution = this.ctx.Map.getView().getResolution(),
    filter = () => true,
    includeHidden = false,
  } = {}) {
    return layers.reduce((allFeatures, layer) => {
      layer.getSource().forEachFeatureInExtent(extent, feature => {
        const selectableFeatures = this.getSelectableFeatures(feature, layer, resolution, includeHidden)
        allFeatures.push(...selectableFeatures.filter(filter))
      })
      return allFeatures
    }, [])
  }

  /**
   * Permet de récupérer la selection courrante
   *
   * @return {Array<Feature>} Tableau des features sélectionnées
   */
  getCurrentSelection () {
    return this._currentSelection.getArray().slice()
  }

  /**
   * Permet d'effacer l'ensemble de la selection actuelle
   */
  clearCurrentSelection () {
    // Récupère les features à retirer et vide la selection
    const selection = this._currentSelection.getArray().slice()
    this._currentSelection.clear()

    // Dispatch l'event
    this._dispatchSelectEvent({
      selected: [],
      deselected: selection,
    })
  }

  /**
   * Permet de désactiver la selection sur un layer
   * Si des features du layer sont sélectionnées, elles ne seront pas déselectionnées par défaut
   *
   * @param {string | number} idLayer Identifiant du layer
   */
  excludeLayerSelection (idLayer) {
    if (this.layerExist(idLayer) && this.excludeLayers.indexOf(idLayer) === -1) {
      this.excludeLayers.push(idLayer)
    }
  }

  /**
   * Permet d'autoriser la sélection sur un layer
   *
   * @param {string | number} idLayer Identifiant du layer
   */
  includeLayerSelection (idLayer) {
    if (this.layerExist(idLayer) && this.excludeLayers.indexOf(idLayer) !== -1) {
      this.excludeLayers.splice(this.excludeLayers.indexOf(idLayer), 1)
    }
  }

  /**
   * Le calque génère-t-il des events au survol d'une feature?
   * @param {string | number} idLayer  Identifiant du layer
   * @param {Boolean} hoverable
   */
  setHoverableLayer (idLayer, hoverable) {
    if (hoverable && this.layerExist(idLayer) && this.hoverableLayer.indexOf(idLayer) === -1) {
      this.hoverableLayer.push(idLayer)
    } else if (!hoverable && this.layerExist(idLayer) && this.hoverableLayer.indexOf(idLayer) !== -1) {
      this.hoverableLayer.splice(this.hoverableLayer.indexOf(idLayer), 1)
    }
  }

  /**
   * Autorise ou non la sélection sur un layer
   * pour une plage de zoom donnée
   * en fonction du zoom courant
   *
   * @param {Object} options Options
   * @param {String|Number} options.idLayer Identifiant du layer
   * @param {Number|undefined} options.minZoom Contrainte sur le niveau de zoom minimal. Optionnelle.
   * @param {Number|undefined} options.maxZoom Contrainte sur le niveau de zoom maximal. Optionnelle.
   * @param {Number} options.currentZoom Niveau de zoom actuel de la carte.
   */
  setLayerSelectionAtZoomLevel ({ idLayer, minZoom, maxZoom, currentZoom }) {
    if (currentZoom >= (minZoom || 0) && currentZoom <= (maxZoom || Infinity)) {
      this.includeLayerSelection(idLayer)
    } else {
      this.excludeLayerSelection(idLayer)
    }

    /* if ((minZoom && currentZoom < minZoom) || (maxZoom && currentZoom > maxZoom)) {
      this.excludeLayerSelection(idLayer)
    } else {
      this.includeLayerSelection(idLayer)
    } */
  }

  /**
   * Permet d'ajouter une ou plusieurs features à la selection actuelle
   *
   * @param {Feature | Array<Feature>} features        Liste des
   * features à ajouter
   * @param {ol.mapBrowserEvent} mapBrowserEvent Permet d'attacher un
   * evenement du navigateur à la selection
   */
  addFeaturesSelection (features, mapBrowserEvent) {
    // Force to array
    features = !features || Array.isArray(features) ? features : [features]

    const realFeatures = features.filter(feature => {
      // Si layerSourceId_ n'existe pas, l'instance n'a jamais été ajoutée sur la carte.
      const isMapInstance = !!feature.layerSourceId_
      if (!isMapInstance) {
        console.warn('L\'instance de la feature n\'a jamais été ajoutée à la carte. Elle ne peut pas être sélectionnée.', feature)
      }
      return isMapInstance
    })

    if (realFeatures && realFeatures.length) {
      // Récupère la selection courante et les nouvelles à ajouter
      const current = this.getCurrentSelection()
      const selected = realFeatures.filter(item => current.indexOf(item) < 0).slice()

      if (selected.length) {
        // Ajoute les nouvelles features et lance un event natif
        this._currentSelection.extend(selected)
        this._dispatchSelectEvent({ selected, mapBrowserEvent })
      }
    }
  }

  /**
   * Permet de retirer une ou plusieurs features de la selection actuelle
   *
   * @param  {Feature | Array<Feature>} features        Liste des
   * features à retirer
   * @param  {ol.mapBrowserEvent} mapBrowserEvent Permet d'atacher un
   * evenement du navigateur à la selection
   */
  removeFeaturesSelection (features, mapBrowserEvent) {
    // Force to array
    features = !features || Array.isArray(features) ? features : [features]

    if (features && features.length) {
      // Récupère la selection courante et les features à retirer
      const current = this.getCurrentSelection()
      const deselected = features.filter(item => current.indexOf(item) !== -1).slice()

      if (deselected.length) {
        // Supprime les features demandé et lève un event natif
        deselected.forEach(item => this._currentSelection.remove(item))
        this._dispatchSelectEvent({ deselected, mapBrowserEvent })
      }
    }
  }

  /**
   * Permet de changer le mode de la carte
   * @param {MapMode} mode Mode de la carte
   */
  changeMapMode (mapMode, toolId = null) {
    // Appel de la nouvelle méthode standard
    this.ctx.commonLayer.setMapMode(mapMode, { toolId })

    if (this._cancelDrawing) {
      this._cancelDrawing()
    }
  }

  /**
   * Permet de retourner la liste des couches de données
   *
   * @return {array<string>} Liste des ids de layer de données
   */
  getLayersId () {
    return this.ctx.commonLayer.getLayersId(DATA_LAYER_TYPE)
  }

  /**
   * Permet de retourner les couches de données
   *
   * @return {array<ol.Layer>} Liste des layers de données
   */
  getLayers () {
    return this.ctx.commonLayer.getLayers(DATA_LAYER_TYPE)
  }

  /**
   * Permet de savoir si un layer existe
   *
   * @param {string} id Id du layer
   * @return {boolean} Si le layer existe
   */
  layerExist (id) {
    return this.ctx.commonLayer.layerExist(id, DATA_LAYER_TYPE)
  }

  /**
   * Permet de retourner un layer
   *
   * @param {string} id Id du layer
   * @return {ol.layer.Vector} Layer recherché
   */
  getLayer (id) {
    return this.ctx.commonLayer.getLayer(id, DATA_LAYER_TYPE)
  }

  /**
   * Permet de savoir si le layer est un layer de données
   *
   * @param {ol.Layer|String|Number} layer Layer ou identifiant de layer
   * @returns {Boolean} True si c'est un layer de données
   */
  isDataLayer (layer) {
    if (layer instanceof Layer) {
      return this.getLayers().includes(layer)
    }
    return this.layerExist(layer)
  }

  /**
   * Permet de récupérer une source depuis un id de layer
   *
   * @param {string} id Id du layer
   * @return {ol.source.Vector} Source vector du layer
   */
  getSourceLayer (id) {
    const l = this.getLayer(id)
    if (this.isCluster(l)) {
      return l.getSource().getSource()
    } else {
      return l.getSource()
    }
  }

  /**
   * Permet de rechercher une feature dans une source à l'aide de son id
   *
   * @param {string | number | Array<string | number>} idFeatures Identifiant de la/des features
   * @param {ol.source.Vector} layer Source du layer
   * @return {array<Feature>} Features correspondant aux ids
   */
  findFeatures (idsFeatures, idLayer) {
    // Si pas de layer, pas de recherche !
    if (!this.layerExist(idLayer)) {
      return []
    }

    // Récupère la source du layer
    const source = this.getSourceLayer(idLayer)

    // Si ce n'est pas un array, transforme en array de longueur 1
    if (!Array.isArray(idsFeatures)) {
      idsFeatures = [idsFeatures]
    }
    const res = []

    // Parcours le tableau pour rechercher
    source.getFeatures().forEach(feature => {
      if (idsFeatures.indexOf(feature.getId()) !== -1) {
        res.push(feature)
      }
    })
    return res
  }

  /**
   * Permet de rechercher une feature dans tous les layers vectoriels à l'aide de son id
   *
   * @param {string | number | Array<string | number>} idFeatures Identifiant de la/des features
   * @return {array<Feature>} Features correspondant aux ids
   */
  findFeaturesInAllLayers (idsFeatures) {
    let results = []
    this.getLayers()
      .forEach((layer) => {
        const layerId = layer.get(this.commonLayer.propertiesName.ID_LAYER)
        results = results.concat(this.findFeatures(idsFeatures, layerId))
      })
    return results
  }

  /**
   * Permet de masquer une feature
   *
   * @param {Feature | string | number} feature Feature Openlayers à masquer ou son identifiant
   * @param {string | undefined} idLayer Id du layer. Uniquement si feature est un identifiant
   * @param {boolean} silent Update without triggering an event.
   */
  hideFeature (feature, idLayer, silent) {
    // Si c'est un identifiant, recherche la feature
    if (feature.constructor !== Feature) {
      feature = this.findFeatures(feature, idLayer)[0]
    }
    silent = silent === true
    // Change la propriété pour la masquer
    if (feature.constructor === Feature) {
      feature.set(this.propertiesName.VISIBLE_FEATURE, false, silent)
    }
  }

  /**
   * Permet de masquer toutes les features d'un layer
   *
   * @param {string} idLayer Id du layer
   * @param {boolean} silent Update without triggering an event.
   */
  hideAllFeatures (idLayer, silent) {
    // Si le layer n'existe pas, annule la suite du traitement
    if (!this.layerExist(idLayer)) {
      return
    }

    this.getSourceLayer(idLayer).getFeatures().forEach(feature => {
      this.hideFeature(feature, null, silent)
    })
  }

  /**
   * Permet d'afficher une feature
   *
   * @param {Feature | string | number} feature Feature Openlayers à afficher ou son identifiant
   * @param {string | undefined} idLayer Id du layer. Uniquement si feature est un identifiant
   * @param {boolean=} silent Update without triggering an event.
   */
  showFeature (feature, idLayer, silent) {
    // Si c'est un identifiant, recherche la feature
    if (feature.constructor !== Feature) {
      feature = this.findFeatures(feature, idLayer)[0]
    }
    silent = silent === true
    // Change la propriété pour l'afficher
    if (feature.constructor === Feature) {
      feature.set(this.propertiesName.VISIBLE_FEATURE, true, silent)
    }
  }

  /**
   * Permet d'afficher toutes les features d'un layer
   *
   * @param {string} idLayer Id du layer
   * @param {boolean} silent Update without triggering an event.
   */
  showAllFeatures (idLayer, silent) {
    // Si le layer n'existe pas, annule la suite du traitement
    if (!this.layerExist(idLayer)) {
      return
    }

    this.getSourceLayer(idLayer).getFeatures().forEach(feature => {
      this.showFeature(feature, null, silent)
    })
  }

  translateFeature (feature) {
    const collect = new Collection([feature])
    const translate = new Translate({
      features: collect,
    })
    this.ctx.Map.addInteraction(translate)
    return translate
  }

  cancelTranslate (feature) {
    feature.setGeometry(feature.initialGeometry)
    return feature
  }

  stopTranslate (translate) {
    this.ctx.Map.removeInteraction(translate)
  }

  /**
   * Permet de déterminer si un layer est un cluster
   *
   * @param {ol.Layer} layer Layer dont on recherche le type
   * @return {boolean} True si layer est un cluster
   */
  isCluster (layer) {
    return layer.getSource().constructor === Cluster
  };

  /**
   * Permet de recalculer un layer (style, position, ...)
   * @param  {string} idLayer Id du layer
   */
  refreshCluster (idLayer) {
    if (this.layerExist(idLayer) && this.isCluster(this.getLayer(idLayer))) {
      this.getLayer(idLayer).getSource().refresh()
    }
  };

  /**
   * Permet de recalculer un layer
   * @param  {string} idLayer Id du layer
   */
  refreshLayer (idLayer) {
    this.ctx.commonLayer.refreshLayer(idLayer, DATA_LAYER_TYPE)
  };

  /**
   * Permet d'indiquer à un layer qu'il a changé
   * @param {string} idLayer Id du layer
   */
  changedLayer (idLayer) {
    this.ctx.commonLayer.changedLayer(idLayer, DATA_LAYER_TYPE)
  }

  linkFeatureToSource_ (feature, idLayer) {
    if (!feature.layerSourceId_) {
      feature.layerSourceId_ = []
    }
    if (feature.layerSourceId_.indexOf(idLayer) === -1) {
      feature.layerSourceId_.push(idLayer)
    }
  }

  unlinkFeatureToSource_ (feature, idLayer) {
    if (feature.layerSourceId_) {
      feature.layerSourceId_.filter(item => item !== idLayer)
    }
  }

  /**
   * renvoi les layers auquels la feature à été ajoutée
   * @param {Feature} feature Feature OpenLayer
   * @returns identifiant du layer
   */
  getFeatureLinkedLayers (feature) {
    return feature.layerSourceId_
  }

  /**
  * Permet de retourner un tableau avec tous les
  * identifiants des features qu'il contient
  *
  * @param {string} idLayer Id du layer
  * @return {array<string> | array<number>} Tableau des identifiants
  */
  getIdsFeatures (idLayer) {
    if (!this.layerExist(idLayer)) {
      return
    }

    const res = []
    this.getSourceLayer(idLayer).getFeatures().forEach(feature => {
      res.push(feature.getId())
    })
    return res
  }

  /**
  * Retourne l'ensemble des features d'un layer
  * @param  {string|Layer} layer Identifiant ou instance du layer
  * @return {Array<Feature>}           Liste des features du layer
  */
  getFeatures (layer) {
    // Dans le cas où l'instance est directement passé
    if (layer instanceof Layer && this.isDataLayer(layer)) {
      return this.isCluster(layer)
        ? layer.getSource().getSource().getFeatures()
        : layer.getSource().getFeatures()
    }

    // Dans le cas où un identifiant est passé
    return this.layerExist(layer) ? this.getSourceLayer(layer).getFeatures() : []
  }

  /**
  * Retourne une feature d'un layer
  * @param  {string}           idFeature  Id de la feature
  * @param  {string}           idLayer    Id du layer
  * @return {Feature}                  Feature du layer
  */
  getFeature (idFeature, idLayer) {
    return this.getFeatures(idLayer).find(feature => feature.getId() === idFeature)
  }

  /**
  * Permet d'ajouter des features openLayers dans un layer
  *
  * @param {array<Feature>} features Tableau des features à ajouter
  * @param {string} idLayer Id du layer
  * @param {boolean} overwrite True si les features déjà existantes doivent-être écrase. Par défaut false
  */
  addFeatures (features, idLayer, overwrite) {
    if (!this.layerExist(idLayer) || !features.length) {
      return
    }

    const source = this.getSourceLayer(idLayer)
    const listIds = this.getIdsFeatures(idLayer)

    const toAdd = []
    const toRemove = []

    features.forEach(feature => {
      if (listIds.indexOf(feature.getId()) < 0) {
        toAdd.push(feature)
      } else if (overwrite) {
        toRemove.push(feature)
        toAdd.push(feature)
      }
    })

    source.removeFeatures(toRemove)
    source.addFeatures(toAdd)

    this.refreshCluster(idLayer)
  }

  /**
   * Ajoute des features a partir d'un GeoJSON
   * @param {Object} geojsonObject Object au format {@link https://fr.wikipedia.org/wiki/GeoJSON GeoJSON} valide
   * @param {string} idLayer Id du layer contenant la feature
   * @param {string} originProjection projection d'origine des feature, sinon on suppose qu'elles sont dans le systeme de la carte
   */
  addGeojsonFeatures (geojsonObject, idLayer, originProjection = null) {
    const features = geoJsonToFeature(geojsonObject, originProjection, originProjection ? this.ctx.getMap().getView().getProjection() : null)
    this.addFeatures(features, idLayer)
  }

  /**
  * Permet de supprimer une feature d'un layer
  *
  * @param {Feature | string | number} feature La feature Openlayers à supprimer
  * @param {string} idLayer Id du layer contenant la feature
  */
  removeFeature (feature, idLayer) {
    if (!this.layerExist(idLayer)) {
      return
    }

    if (feature.constructor !== Feature) {
      feature = this.findFeatures(feature, idLayer)
      feature = feature.length ? feature[0] : false
    }

    if (feature && feature.constructor === Feature) {
      this.getSourceLayer(idLayer).removeFeature(feature)
      this.refreshCluster(idLayer)
      this.removeFeaturesSelection([feature])
      this.ctx.dispatchEvent('deletefeature:delete', { deletedFeature: feature })
    }
  }

  /**
  * Permet de mettre à jour une feature d'un layer
  *
  * @param {Feature | string | number} olFeature La feature Openlayers à mettre à jour
  * @param {string} idLayer Id du layer contenant la feature
  * @param {Object} geojsonFeature Données de la feature en geojson
  * @param {Object} geojsonFeature.properties Les propriétés mises à jour
  * @param {Object} geojsonFeature.geometry La géométrie à mettre à jour
  */
  updateFeature (feature, idLayer, geojsonFeature) {
    if (!this.layerExist(idLayer)) {
      return
    }

    if (feature.constructor !== Feature) {
      feature = this.findFeatures(feature, idLayer)
      feature = feature.length ? feature[0] : false
    }

    if (feature && feature.constructor === Feature) {
      const properties = (geojsonFeature && geojsonFeature.properties) || {}
      const geometry = (geojsonFeature && geojsonFeature.geometry) || null
      const olGeometry = geometry ? (new GeoJSON()).readGeometry(geometry) : null
      feature.setProperties(properties)
      feature.setGeometry(olGeometry)
      this.refreshCluster(idLayer)
    }
  }

  /**
  * Permet de supprimer un ensemble de features d'un layer
  *
  * @param {array<string | number | Feature>} features Tableau des features à supprimer
  * @param {string} idLayer Id du layer contenant les features
  */
  removeFeatures (features, idLayer) {
    if (!this.layerExist(idLayer) || !features.length) {
      return
    }

    // optimisation, si toutes les features sont de type olFeature, on passe par la méthode native
    if (features.every(feature => feature.constructor === Feature)) {
      this.getSourceLayer(idLayer).removeFeatures(features)
      return
    }

    features.forEach(feature => {
      this.removeFeature(feature, idLayer)
    })
  }

  /**
  * Permet d'effacer toutes les features d'un layer
  *
  * @param {string} idLayer Id du layer à vider
  */
  clearLayer (idLayer) {
    if (this.layerExist(idLayer)) {
      // Supprime le lien avec les features
      this.getFeatures(idLayer).forEach(feature => {
        this.unlinkFeatureToSource_(feature, idLayer)
      })
      this.getSourceLayer(idLayer).clear()
    }
  }

  /**
   * Permet de supprimer un layer
   *
   * @param {string} idLayer Id du layer à supprimer
   * @fires removeLayer Lancé avant la suppression
   * @fires removedLayer Lancé après la suppression
   */
  removeLayer (idLayer) {
    // Supprime les labels si présents
    if (this.ctx.LABELFEATURE_LOADED) {
      this.ctx.labelFeature.detachLayer(idLayer)
    }

    // Supprime le lien avec les features
    this.getFeatures(idLayer).forEach(feature => {
      this.unlinkFeatureToSource_(feature, idLayer)
    })

    // Supprime le layer
    this.ctx.commonLayer.removeLayer(idLayer, DATA_LAYER_TYPE)

    // Supprime la référence du style du layer
    if (this._layersStyle[idLayer]) {
      delete this._layersStyle[idLayer]
    }
  }

  /**
   * Permet de masquer un layer
   *
   * @param {string} id Id du layer à masquer
   */
  hideLayer (id) {
    this.ctx.commonLayer.hideLayer(id, DATA_LAYER_TYPE)
  }

  /**
   * Permet d'afficher un layer
   *
   * @param {string} id Id du layer à afficher
   */
  showLayer (id) {
    this.ctx.commonLayer.showLayer(id, DATA_LAYER_TYPE)
  }

  /**
   * Permet de monter le layer d'un niveau dans l'ordre d'affichage
   *
   * @param {string} id Id du layer
   */
  upLayer (id) {
    return this.ctx.commonLayer.upLayer(id, DATA_LAYER_TYPE)
  }

  /**
   * Permet de descendre le layer d'un nieau dans l'ordre d'affichage
   *
   * @param {string} id Id du layer
   */
  downLayer (id) {
    return this.ctx.commonLayer.downLayer(id, DATA_LAYER_TYPE)
  }

  /**
   * Permet de connaitre la visibilité d'un layer (en fonction de min/max zoom, propriété "visible" & group "visible")
   *
   * @param  {string}  id Identifiant du layer
   * @return {boolean}    Visibilité
   */
  isVisible (id) {
    return this.ctx.commonLayer.isVisible(id, DATA_LAYER_TYPE)
  }

  /**
   * Animation flash d'une feature pendant une durée donnée
   * Point: crée un cercle rouge dont le rayon augmente autour de la feature
   * LineString, Polygon: crée une bordure rouge dont la taille augmente
   * @param  {Feature} feature
   * @param {Number} duration durée de l'animation en ms
   */
  animateFeature (feature, duration) {
    const start = new Date().getTime()
    let listenerKey = null

    const layerSourceId = feature.layerSourceId_[0]
    const featureLayer = this.getLayer(layerSourceId)

    // récupère les morceaux de geom hétéroclite et les anime une a une
    let geometrys = []
    if (feature.getGeometry()) {
      switch (feature.getGeometry().getType()) {
        case 'Point':
        case 'LineString':
        case 'Polygon':
          geometrys = [feature.getGeometry()]
          break
        case 'GeometryCollection':
          geometrys = feature.getGeometry().getGeometries().map((geometry) => geometry)
          break
        case 'MultiPoint':
          geometrys = feature.getGeometry().getCoordinates().map((coordinates) => new Point(coordinates))
          break
        case 'MultiLineString':
          geometrys = feature.getGeometry().getLineStrings().map((lineString) => lineString)
          break
        case 'MultiPolygon':
          geometrys = feature.getGeometry().getPolygons().map((polygon) => polygon)
          break
      }
    }
    function animate (event) {
      const vectorContext = getVectorContext(event)
      const frameState = event.frameState
      const elapsed = frameState.time - start
      const elapsedRatio = elapsed / duration

      // radius will be 5 at start and 30 at end.
      const radius = Easing.easeOut(elapsedRatio) * 25 + 5
      const opacity = Easing.easeOut(1 - elapsedRatio)
      const strokeWidth = Easing.easeOut(elapsedRatio) * 25 + 0.1

      geometrys.forEach((geometry) => {
        let style = null
        switch (geometry.getType()) {
          case 'Point':
          case 'MultiPoint':
            style = new Style({
              image: new Circle({
                radius,
                stroke: new Stroke({
                  color: 'rgba(255, 0, 0, ' + opacity + ')',
                  width: 5 + opacity,
                }),
              }),
            })
            break
          case 'LineString':
          case 'MultiLineString':
          case 'Polygon':
          case 'MultiPolygon':
          case 'GeometryCollection':
            style = new Style({
              stroke: new Stroke({
                color: 'rgba(255, 0, 0, ' + opacity + ')',
                width: strokeWidth,
              }),
            })
            break
          default:
            console.warn('[datalayer-animate] Type de geometry non valide')
        }

        if (style) {
          vectorContext.setStyle(style)
          vectorContext.drawGeometry(geometry)
        }
      })

      // si pas de style (défault on risquer la boucle infinie..)
      if (elapsed > duration) {
        unByKey(listenerKey)
        return
      }
      // tell OpenLayers to continue postrender animation
      this.ctx.Map.render()
    }
    if (geometrys.length > 0) {
      listenerKey = featureLayer.on('postrender', animate.bind(this))
    }
  }

  /**
   * Animation flash d'un ensemble de features pendant une durée donnée
   * @param  {Array<Feature>} features Features
   * @param {Number} duration durée de l'animation en ms
   */
  animateFeatures (features, duration) {
    if (!features || features.length < 1) {
      return
    }
    features.forEach(feature => {
      this.animateFeature(feature, duration)
    })
  }

  /**
   * Permet de center sur un groupe de feature
   * @param  {Array<Feature>} features Features devant apparaitre dans le zoom
   * @param  {boolean}           noFly    Permet de désactiver l'animation de déplacement
   * @param {Object}             options  Permet de passer des options d'ajustement au composant ol.View (voir ol.View.fit)
   */
  centerOnFeatures (features, noFly, options) {
    if (!features || features.length < 1) {
      return
    }

    const extentMax = Extent.createEmpty()

    // Parcours les features
    features.forEach(feature => {
      if (feature.getGeometry()) {
        Extent.extend(extentMax, feature.getGeometry().getExtent())
      }
    })

    if (Extent.isEmpty(extentMax)) {
      return
    }

    // Ne se déplace que s'il existe une extent max (non infinie)
    if (extentMax && !(
      extentMax[0] === Infinity ||
      extentMax[1] === Infinity ||
      extentMax[2] === -Infinity ||
      extentMax[3] === -Infinity)) {
      // Si noFly, déplace la carte sans annimation
      if (noFly) {
        this.ctx.zoomToExtent(extentMax, options)
      } else { // Sinon utilise une animation de MapViewer
        this.ctx.flyToExtent(extentMax, options)
      }
    }
  }

  /**
   * Permet de récupérer l'extent englobant tous les layers demandés
   *
   * @param {Array<string>} idLayers Ids des layers
   */
  getLayersExtent (idLayers) {
    idLayers = !Array.isArray(idLayers) ? [idLayers] : idLayers

    let extentMax = null

    // Recherche les layers passé en paramètre
    idLayers.forEach(id => {
      if (this.layerExist(id) && this.isDataLayer(id)) {
        // Si aucune extent n'est récupérer, applique celle du layer dirrectement
        if (!extentMax) {
          extentMax = this.getSourceLayer(id).getExtent()
        } else { // sinon, cumule l'extent du layer avec l'extent global
          Extent.extend(extentMax, this.getSourceLayer(id).getExtent())
        }
      }
    })

    return extentMax
  }

  /**
   * Permet de centrer sur toutes les features d'un layer.
   * Centre sur un niveau de zoom exact adapté pour voir l'ensemble des features et avoir un fond de plan net
   *
   * @param {string} idLayer Id du layer
   * @param {boolean | undefined} noFly Permet de centrer sur le layer sans animation. Par défaut 'false'
   * @param {Object} options  Permet de passer des options d'ajustement au composant ol.View (voir ol.View.fit)
   */
  centerOnLayers (idLayers, noFly, options) {
    const extentMax = this.getLayersExtent(idLayers)

    // Ne se déplace que s'il existe une extent max (non infinie)
    if (extentMax && !(
      extentMax[0] === Infinity ||
      extentMax[1] === Infinity ||
      extentMax[2] === -Infinity ||
      extentMax[3] === -Infinity)) {
      // Si noFly, se déplace à l'extent sans annimation
      if (noFly) {
        this.ctx.zoomToExtent(extentMax, options)
      } else { // Sinon utilise une annimation de déplacement
        this.ctx.flyToExtent(extentMax, options)
      }
    }
  }

  /**
   * Permet de retourner le style d'une feature
   */
  _applyStyleToFeature ({ style, feature, resolution }) {
    // effectu les traitement de la feature:

    const zoom = this.ctx.Map.getView().getZoomForResolution(resolution)

    const selected = !!feature.get(this.propertiesName.SELECTED_FEATURE)
    const activated = !!feature.get(this.propertiesName.ACTIVATED_FEATURE)

    const tags = feature.get(this.propertiesName.TAG_FEATURE) || {}
    const hovered = !!tags[this.systemTagName.HOVERED_FEATURE]

    // si on a une macro-fonction englobante, on l'execute
    let stylesToApply = typeof style === 'function'
      ? style({
        feature,
        resolution,
        zoom,
        selected,
        activated,
        hovered,
        tags,
      })
      : style

    // Transforme un style unitaire, fonction ou tableau de tout ca en tableau de style
    stylesToApply = (Array.isArray(stylesToApply) ? stylesToApply : [stylesToApply]).flatMap((styleToApply) => {
      // si le style est une fonction on l'exectute
      styleToApply = typeof styleToApply === 'function'
        ? styleToApply({
          feature,
          resolution,
          zoom,
          selected,
          activated,
          hovered,
          tags,
        })
        : styleToApply
        // pour chaque style du tableau on détermine si on a besoin de créer d'une géométry alternative que l'on va coller sur la feature comme "propriété"

      return styleToApply
    })

    // Force un style vide si pas de style
    return stylesToApply.length > 0 ? stylesToApply : STYLE_NONE
  }

  isVisibleFeature ({ feature, includeHidden, evaluateStyles = true, filter, resolution }) {
    // Si on souaite inclure les features masquées
    if (!includeHidden && feature.get(this.propertiesName.VISIBLE_FEATURE) === false) {
      return false
    }

    const zoom = this.ctx.Map.getView().getZoomForResolution(resolution)
    const selected = !!feature.get(this.propertiesName.SELECTED_FEATURE)
    const activated = !!feature.get(this.propertiesName.ACTIVATED_FEATURE)

    const tags = feature.get(this.propertiesName.TAG_FEATURE) || {}
    const hovered = !!tags[this.systemTagName.HOVERED_FEATURE]

    // Si on filtre les features du cluster
    if (filter && !filter({ feature, resolution, zoom, selected, activated, hovered })) {
      return false
    }

    // Si on souhaite évaluer les styles pour contraindre l'affichage
    if (evaluateStyles) {
      return feature.layerSourceId_ && feature.layerSourceId_
        .filter(layerId => this._layersStyle[layerId])
        .some(layerId => {
          const styles = this._layersStyle[layerId](feature, resolution, {
            bypassCluster: true,
          })
          return styles && (!Array.isArray(styles) || styles.length)
        })
    }

    return true
  }

  /**
   * Permet de filtrer les features en fonction de leur visibilité
   */
  filterVisibleFeatures ({ features, includeHidden, evaluateStyles = true, filter, resolution }) {
    return features.filter(feature => this.isVisibleFeature({
      feature,
      includeHidden,
      evaluateStyles,
      filter,
      resolution,
    }))
  }

  /**
   * Construit une fonction de filtre de visiblité pour ne pas avoir
   * à conserver les informations (filtre, ...)
   */
  buildFilterVisibleFeatures ({ includeHidden, evaluateStyles = true, filter }) {
    return (features, resolution) => this.filterVisibleFeatures({
      features,
      includeHidden,
      evaluateStyles,
      filter,
      resolution,
    })
  }

  /**
   * Permet de retourner une fonction de style pour un layer de données
   */
  _getDataLayerStyleFunction ({ style, cluster, layer }) {
    const clusterStyle = cluster && (cluster.style || DEFAULT_CLUSTER_STYLE_FUNCTION)

    const clusterFilter = cluster && this.buildFilterVisibleFeatures({
      includeHidden: cluster.includeHidden,
      filter: cluster.filter,
    })
    layer.set(this.propertiesName.CLUSTER_FILTER, clusterFilter)

    return (feature, resolution, { bypassCluster } = {}) => {
      // Si la feature est masquée
      if (feature.get(this.propertiesName.VISIBLE_FEATURE) === false) {
        return STYLE_NONE
      }

      if (!bypassCluster && cluster && this.isCluster(layer)) {
        const clusterizedFeatures = feature.get('features')

        // On évalue une feature qui n'est pas un cluster
        if (!clusterizedFeatures) {
          return STYLE_NONE
        }

        const visibleFeatures = clusterFilter(clusterizedFeatures, resolution)

        // Cas d'un cluster vide (sous ensemble non visible)
        if (visibleFeatures.length === 0) {
          return STYLE_NONE
        }

        // Cas d'une représentation avec le style du cluster
        if (visibleFeatures.length > 1 || cluster.groupFeaturesAlone) {
          return typeof clusterStyle === 'function'
            ? clusterStyle({
              features: visibleFeatures,
              cluster: feature, // pour garder une compatibilité surement inutilisée
              feature, // les fonction de styles.js attendent "feature"
              resolution,
              zoom: this.ctx.Map.getView().getZoomForResolution(resolution),
            })
            : clusterStyle
        }

        // Une seule feature, donc pas de cluster (style de la feature)
        return this._applyStyleToFeature({
          style,
          feature: visibleFeatures[0],
          resolution,
        })
      }

      return this._applyStyleToFeature({ style, feature, resolution })
    }
  }

  _getExplodedClusterStyleFunction ({ style }) {
    return (feature, resolution) => {
      // Si la feature est masquée
      if (feature.get(this.propertiesName.VISIBLE_FEATURE) === false) {
        return STYLE_NONE
      }

      // Style du trait entre une feature et le cluster
      if (feature.get('selectclusterlink')) {
        return STYLE_CLUSTER_LINK
      }

      // Style d'une feature
      if (feature.get('selectclusterfeature')) {
        return this._applyStyleToFeature({
          style,
          feature: feature.get('feature'),
          resolution,
        })
      }
    }
  }

  getFeaturesAtPixel (pixel) {
    const features = []
    this.ctx.Map.forEachFeatureAtPixel(pixel, (feature, layer) =>
      features.push({ feature, layer }))
    return features
  }

  _buildExplodedClusterLayer ({ mainLayer, style, zIndex }) {
    return new VectorLayer({
      id: `explodedLayer ${mainLayer.getId()}`,
      parentLayer: mainLayer,
      zIndex,
      updateWhileAnimating: true,
      updateWhileInteracting: true,
      source: new VectorSource({
        features: new Collection(),
      }),
      style: this._getExplodedClusterStyleFunction({ style }),
    })
  }

  _buildDetailedCluster ({ cluster, layer }) {
    const features = cluster.get('features')

    if (features < 2) {
      return null
    }

    // Rayon de la vue explosée
    const pointRadius = 100
    // Nombre maximal de features dans la vue explosée
    const circleMaxObjects = 8
    // Centre de la vue explosée (emplacement du cluster)
    const center = cluster.getGeometry().getCoordinates()
    // Nombre d'unités par pixel
    const pixelSize = this.ctx.Map.getView().getResolution()
    // Nombre de points dans la vue explosée
    const maxOnCircle = Math.min(features.length, circleMaxObjects)
    // Rayon en unité de mesure de la carte
    const radius = pointRadius * pixelSize * (0.5 * maxOnCircle / 4)

    // Pour chaque point de la vue explosée
    for (let i = 0; i < maxOnCircle; ++i) {
      // Récupère l'angle pour positionner la feature sur le cercle
      let angle = 2 * Math.PI * i / maxOnCircle

      // Cas particulier pour 2 et 4 features
      if (maxOnCircle === 2 || maxOnCircle === 4) {
        angle += Math.PI / 4
      }

      // Coordonnées de l'emplacement de la feature détaillée
      const point = [
        center[0] + radius * Math.sin(angle),
        center[1] + radius * Math.cos(angle),
      ]

      // Création de la feature détaillée
      layer.getSource().addFeature(new Feature({
        selectclusterfeature: true,
        feature: features[i],
        geometry: new Point(point),
      }))

      // Création de la ligne qui relie la feature détaillée au cluster
      layer.getSource().addFeature(new Feature({
        selectclusterlink: true,
        geometry: new LineString([center, point]),
      }))
    }
  }

  _refreshClusterVisibility ({ layer, clusterSource, defaultSource, constraintZoom, currentZoom }) {
    // On met à jour la source du layer si nécessaire
    if (currentZoom <= constraintZoom && layer.getSource() !== clusterSource) {
      layer.setSource(clusterSource)
    }

    if (currentZoom > constraintZoom && layer.getSource() !== defaultSource) {
      layer.setSource(defaultSource)
    }
  }

  /**
  * Permet de créer un layer de données
  *
  * @param {Object} options Options
  *
  * @param {String|Number} options.layerId Identifiant du layer
  * @param {String?} options.idGroup Nom du groupe de calque
  * @param {Array<Feature>|undefined} options.features Features à inséréer. Optionnel.
  * @param {ol.style.Style|Array<ol.style.Style>|ol.style.StyleFunction|undefined} options.style Style du layer. Optionnel.
  * @param {Boolean|Object|undefined} options.selectable Activer la sélection sur le layer. Par défaut true.
  * @param {Number} options.selectable.minZoom Zoom minimal pour être sélectionnable (exclusif). Optionnels.
  * @param {Number} options.selectable.maxZoom Zoom maximal pour être sélectionnable (inclusif). Optionnels.
  * @param {Number} options.selectable.minScale echelle minimal pour être sélectionnable (exclusif). Optionnels. prévaut sur options.selectable.maxZoom.
  * @param {Number} options.selectable.maxScale echelle maximal pour être sélectionnable (inclusif). Optionnels. prévaut sur options.selectable.minZoom.
  * @param {Boolean|undefined} [options.hoverable=false] Activer le mode survol de ce calque. Par défaut false. Il faudra lancer le service de survol sur Core pour que cela soit prit en compte (setFeatureHover)
  * @param {Number|undefined} options.minZoom Zoom minimal pour afficher le layer (exclusif)
  * @param {Number|undefined} options.maxZoom Zoom maximal pour afficher le layer (inclusif)
  * @param {Number|undefined} options.minResolution Résolution minimale pour afficher le layer (inclusif). Non utilisée si maxZoom défini
  * @param {Number|undefined} options.maxResolution Résolution maximale pour afficher le layer (exclusif). Non utilisée si minZoom défini
  * @param {(Number|undefined)?} options.zIndex Si le zIndex est présent, celui-ci prévaut sur la position du layer sur la carte. Optionnel.
  * @param {String} options.attributions Attribution de la carte. Optionnel.
  *
  * @param {Object?} options.cluster Options pour définir un layer de type Cluster. Optionnel.
  * @param {ol.style.Style|Array<ol.style.Style>|ol.style.StyleFunction|undefined} options.cluster.style Style des points de cluster. Optionnel.
  * @param {Number|undefined} options.cluster.distance Distance de regroupement. Par défaut 50.
  * @param {Boolean|undefined} options.cluster.exploded Possibilité de voir une vue éclatée d'un groupe. Par défaut false. @TODO_EXPLODED: Non testée.
  * @param {Boolean|undefined} options.cluster.groupFeaturesAlone Les groupe d'une features seront représentés sous forme de groupe. Par défaut false.
  * @param {Boolean|undefined} options.cluster.includeNoStyle Inclure dans les groupes les features sans style. Par défaut false.
  * @param {Boolean|undefined} options.cluster.includeHidden Inclure dans les groupes les features masquée. Par défaut false.
  * @param {Boolean|undefined} options.cluster.zoom Permet d'afficher le cluster uniquement jusqu'à un certain niveau de zoom. Par défaut, toujours visible.
  *
  * @param {Object?} options.label Affichage de labels sur la carte. Optionnel.
  * @param {Function} options.label.templateFunction Fonction de template pour générer un label. Prends une feature et la résolution en entrée et retourne un template HTML. Optionnel.
  * @param {Number} options.label.minZoom Zoom minimal pour afficher les labels. Optionnels.
  * @param {Number} options.label.maxZoom Zoom maximal pour afficher les labels. Optionnels.
  * @param {Number} options.label.minScale echelle minimal pour afficher les labels. Optionnels. prévaut sur options.label.maxZoom.
  * @param {Number} options.label.maxScale echelle maximal pour afficher les labels. Optionnels. prévaut sur options.label.minZoom.
  *
  * @param {Object?} options.wfs Option de chargement des données via un service wfs
  * @param {String} options.wfs.url Url du service wfs
  * @param {string} options.wfs.geometryName nom du champs geométrie du service (défaut geo)
  * @param {Object} options.wfs.params paramètre du service wfs
  * @param {string} options.wfs.params.typename Nom de la couche du service WFS à exploiter
  * @param {string} [options.wfs.params.srsname=EPSG:3857] projection de retour des données
  * @param {string} [options.wfs.params.version=1.1.1] version du service WFS
  * @param {string} options.wfs.params.* autres paramètres du service WFS
  * @param {string} [options.wfs.projection=EPSG:3857] projection de la bbox pour interroger le service
  * @param {string} [options.wfs.geometryName=geometry] nom de la géométrie pour interroger la bbox
  * @param {boolean} [options.wfs.bboxStrategy=false] Utiliser une stratégie de chargement par bbox (true) ou par tuile (false). Par défaut false
  * @param {boolean | Object} [options.wfs.tileStrategy=false] Utiliser une stratégie de chargemen par tuile (false). Par défaut true
  * @param {boolean | Object} [options.wfs.tileStrategy.tileSize=256] Taille des tuiles pour la stratégie par tuile
  * @param {string} options.wfs.cqlFilter filtre cql
  * @param {Object<string,(string|Array)>} options.wfs.filters objet represant des filtres a générer (exemple {code_insee:[9420,8521]}). Ignoré si cqlFilter est défini
  *
  * @param {string} options.title titre pour affichage dans layermanager
  * @param {boolean} options.displayInLayerSwitcher pour afficher ou non dans layermanager
  *
  * @param {Object?} options.ui (deprecated) élement pour l'IHM si on utilise la layervisibilitybar
  * @param {string} options.ui.title tooltip du calque
  * @param {Element|string} options.ui.html contenu html du bouton a afficher
  * @param {string} options.ui.icon icone si html non saisie (utilisé comme <i class='icon'/>)
  *
  * @param {LegendOption[]?} options.legends Options de légende (voir examples)
  * @param {loadingStrategy?} options.loadingStrategy stratégie de chargement de carte avec un loader personnalisé

  */
  addDataLayer ({
    idGroup = null,
    layerId = IsRequired('layerId'),
    features = [],
    // TODO Vérifier à quoi servait et ce qu'était defaultStyleFunction
    style = null,
    selectable = true,
    hoverable = false,
    minZoom,
    maxZoom,
    minResolution,
    maxResolution,
    minScale,
    maxScale,
    zIndex,
    attributions = '',
    cluster,
    label,
    wfs,
    ui,
    title = null,
    displayInLayerSwitcher = true,
    legends = null,
    loadingStrategy = null,
  }) {
    // Verifie que le layer n'existe pas
    if (this.layerExist(layerId)) {
      throw new Error(`Layer ${layerId} already exists`)
    }

    // les notions de zoom & scale/résolutions sont inversées (max zoom = proche du sol)
    // options minScale? on la convertie en zoom
    if (minScale) {
      maxZoom = getZoomForScale(this.ctx.Map.getView(), minScale)
    }

    // options maxScale? on la convertie en zoom
    if (maxScale) {
      minZoom = getZoomForScale(this.ctx.Map.getView(), maxScale)
    }

    // converti les scales pour les label
    if (label && label.minScale) {
      label.maxZoom = getZoomForScale(this.ctx.Map.getView(), label.minScale)
    }

    if (label && label.maxScale) {
      label.minZoom = getZoomForScale(this.ctx.Map.getView(), label.maxScale)
    }

    // converti les scales pour les selectable
    if (selectable && selectable.minScale) {
      selectable.maxZoom = getZoomForScale(this.ctx.Map.getView(), selectable.minScale)
    }

    if (selectable && selectable.maxScale) {
      selectable.minZoom = getZoomForScale(this.ctx.Map.getView(), selectable.maxScale)
    }

    const olEvents = []

    /* si l'utilisateur a renseigné une url de service wfs,
    on configure une strategy bbox ou tuile
    et wfsSource sera notre source de vecteurs */
    const wfsSource = getWfsVectorSource(wfs, { attributions })

    const defaultSource = wfsSource ||
    (loadingStrategy // l'utilisateur veut appliquer un loader spécifique
      ? new VectorSource({
        attributions,
        strategy: loadingStrategy?.bboxStrategy ? bboxStrategy : tileStrategy(createXYZ({ tileSize: loadingStrategy?.tileStrategy?.tileSize || 256 })),
        loader: async function (extent, resolution, projection, success, failure) {
          try {
            const features = await loadingStrategy.loader(extent, resolution, projection)
            this.addFeatures(features)
            success(features)
          } catch (error) {
            console.error('[datalayer-loadingStrategy]', error)
            this.removeLoadedExtent(extent)
            // si on est pas dans le scope on peut faire ceci:
            failure()
          }
        },
      })
      : new VectorSource({ features, attributions })) /* Chargement classique des données */

    defaultSource.on('addfeature', (evt) => {
      this.linkFeatureToSource_(evt.feature, layerId)
    })
    defaultSource.on('removefeature', (evt) => {
      this.unlinkFeatureToSource_(evt.feature, layerId)
    })

    // Création du layer principal
    title = title || ui?.title // compatibilité ControlLayerVisibility et layerSwitcher
    const mainLayer = new VectorLayer({
      title,
      [this.commonLayer.propertiesName.GROUP_LAYER]: idGroup,
      [this.commonLayer.propertiesName.ID_LAYER]: layerId,
      source: defaultSource,
      minZoom,
      maxZoom,
      minResolution,
      maxResolution,
      zIndex,
    })

    mainLayer.set('displayInLayerSwitcher', displayInLayerSwitcher)

    // Ajout de la fonction de style si présente
    mainLayer.setStyle(this._getDataLayerStyleFunction({
      style,
      cluster,
      layer: mainLayer,
    }))

    // Ajoute une référence au layer dans la feature
    features.forEach(feature => {
      this.linkFeatureToSource_(feature, layerId)
    })

    // Garde une référence dans le cas d'une fonction de style
    if (mainLayer.getStyleFunction()) {
      this._layersStyle[layerId] = mainLayer.getStyleFunction()
    }

    // Création de la source pour le cluster
    if (cluster) {
      // Création de la source du cluster
      const clusterSource = new Cluster({
        distance: cluster.distance || 50,
        source: defaultSource,
        geometryFunction: feature => getPointForGeometry(feature.getGeometry()),
      })

      // Si le cluster est contraint par le niveau de zoom
      if (cluster.zoom) {
        this._refreshClusterVisibility({
          layer: mainLayer,
          clusterSource,
          defaultSource,
          constraintZoom: cluster.zoom,
          currentZoom: this.ctx.Map.getView().getZoom(),
        })
        // Traitement à réaliser lors du changement de résolution
        olEvents.push(this.ctx.Map.getView().on('change:resolution', ({ target: view }) => {
          this._refreshClusterVisibility({
            layer: mainLayer,
            clusterSource,
            defaultSource,
            constraintZoom: cluster.zoom,
            currentZoom: view.getZoom(),
          })
        }))
      } else {
        mainLayer.setSource(clusterSource)
      }
    }

    // Création de la vue explosée du cluster
    // TODO: Exploded Force la désactivation tant que c'est pas testé !!!
    if (cluster && cluster.exploded && !cluster.exploded) {
      // Gestion de la vue explosée
      const explodedLayer = this._buildExplodedClusterLayer({
        mainLayer,
        style,
        zIndex,
      })
      // Ajoute le layer à la carte
      this.ctx.Map.addLayer(explodedLayer)

      // Traitement à réaliser lors d'un clic sur la carte
      olEvents.push(this.ctx.Map.on('click', ev => {
        const clickedFeatures = this.getFeaturesAtPixel(ev.pixel)

        const detailedFeatures = clickedFeatures.some(({ feature, layer }) =>
          layer === explodedLayer || feature.get('selectclusterfeature'))

        // Dans le cas d'un clic sur une feature détaillée
        if (detailedFeatures.length) {
          return null
        }

        // Dans tous les autres cas, ou retire la vue explosée
        explodedLayer.getSource().clear()

        // Cherche si un cluster est cliqué
        const clickedCluster = clickedFeatures.find(({ layer }) => layer === mainLayer)

        if (clickedCluster) {
          this._buildDetailedCluster({
            cluster: clickedCluster,
            layer: explodedLayer,
          })
        }
      }))

      // Traitement à réaliser lors d'un changement de résolution
      olEvents.push(this.ctx.Map.getView().on('change:resolution', () => {
        // Vide la vue explosée à chaque changement de zoom
        explodedLayer.getSource().clear()
      }))

      // Si le layer source change, actualise la vue explosée
      mainLayer.on('change', () => explodedLayer.getSource().refresh())

      // Traitement à réaliser lors de la suppression du layer de données
      this.ctx.once(`removedLayer:${layerId}`, () => {
        // Supprime le layer de la vue explosée des clusters
        this.ctx.Map.removeLayer(explodedLayer)
      })
    }

    // Ajout du layer de données sur la carte OpenLayers
    this.ctx.commonLayer.addLayer(mainLayer, DATA_LAYER_TYPE, ui)

    // Si on souhaite utiliser le module de label sur ce layer
    if (this.ctx.LABELFEATURE_LOADED && label && label.templateFunction) {
      this.ctx.labelFeature.attachLayer({
        idLayer: layerId,
        templateFunction: label.templateFunction,
        minZoom: label.minZoom,
        maxZoom: label.maxZoom,
      })
    }

    // Cas d'un layer non sélectionnable
    if (!selectable) {
      this.excludeLayerSelection(layerId)
    }

    // Si le calque est hoverable
    this.setHoverableLayer(layerId, hoverable)

    // il y a des options pour générer une légende?
    if (legends) {
      this.ctx.legendManager.addLegends(mainLayer, legends)
    }

    // Cas d'une sélection contrainte par le niveau de zoom
    if (selectable.minZoom || selectable.maxZoom) {
      // Initialise l'état en fonction du zoom actuel
      this.setLayerSelectionAtZoomLevel({
        idLayer: layerId,
        minZoom: selectable.minZoom,
        maxZoom: selectable.maxZoom,
        currentZoom: this.ctx.Map.getView().getZoom(),
      })

      // On observe le changement de résolution pour mettre à jour l'état de sélection
      olEvents.push(this.ctx.Map.getView().on('change:resolution', ev => {
        const currentZoom = this.ctx.Map.getView().getZoom()
        this.setLayerSelectionAtZoomLevel({
          idLayer: layerId,
          minZoom: selectable.minZoom,
          maxZoom: selectable.maxZoom,
          currentZoom,
        })
      }))
    }

    // Traitement à réaliser lors de la suppression du layer
    this.ctx.once(`removedLayer:${layerId}`, () => {
      // Retire les events de la Map et la View liés au layer
      olEvents.forEach(unByKey)
    })
  };

  geojsonvtReplacer (key, value) {
    if (value && value.geometry) {
      let type
      const rawType = value.type
      const geometry = value.geometry

      if (rawType === 1) {
        type = geometry.length === 1 ? 'Point' : 'MultiPoint'
      } else if (rawType === 2) {
        type = geometry.length === 1 ? 'LineString' : 'MultiLineString'
      } else if (rawType === 3) {
        type = geometry.length === 1 ? 'Polygon' : 'MultiPolygon'
      }

      return {
        type: 'Feature',
        id: value.id,
        geometry: {
          type,
          coordinates: geometry.length === 1 ? geometry : [geometry],
        },
        properties: value.tags,
      }
    } else {
      return value
    }
  };

  /**
   * Permet de définir une couche de données vectoriel
   * Cette couche permet de charger un nombre important de données
   * @param {Object} options Paramètres pour limport { SelectModes } from './data-layer';
   * @param {string} options.id Id du layerimport Mapviewer from './../core/core';

   * @param {string | undefined} options.attributions Définition des attributions propre au layer. Vide par défaut
   * @param {ol.style.Style | Array.<ol.style.Style> | ol.style.StyleFunction | undefined} options.style Tableau des styles à appliquer au features ou fonction de style openlayers
   * @param {boolean | undefined} options.selectable Autoriser la selection sur le layer. Activée par défaut
   * @param {Object | undefined} options.geojson Geojson en EPSG:4326 des données. Permet de créer le layer plus rapidement que les features. options.geojson ou options.features requis
   * @param {Array<Feature> | undefined} options.features Features à charger. Préférer l'utilisation de options.geojson. options.geojson ou options.features requis
   */
  addVectorDataLayer (options) {
    // Définition des arguments par défaut
    const args = {
      attributions: '',
      // TODO Vérifier à quoi servait et ce qu'était defaultStyleFunction
      style: null,
      selectable: true,
      ui: null,
    }

    // Ajoute les arguments en paramètres dans les arguments par défauts
    Object.assign(args, options)

    // Le layer ne peut pas avoir le même id qu'un layer existant
    if (this.layerExist(args.id)) {
      throw new Error('Layer ' + args.id + ' already exixts')
    }

    if (!args.features && !args.geojson) {
      throw new Error('Data are required for ' + args.id)
    }

    // Un tableau contenant des features est requis pour créer le layer
    if (args.features && (!Array.isArray(args.features) || !args.features.length)) {
      throw new Error('Features to be an array for layer ' + args.id)
    }

    let geojson = args.geojson

    if (!geojson) {
      // Convertie les features en GeoJSON pour les passer à geojson-vt
      geojson = JSON.parse(MapviewerServices.featureToGeoJson(args.features, 'EPSG:3857', 'EPSG:4326'))
    }

    // Indexation des features sur les tuiles
    const tileIndex = geojsonvt(geojson, { extent: 4096, debug: 0 })
    const tilePixels = new proj.Projection({ code: 'TILE_PIXELS', units: 'tile-pixels' })

    /*
     * Si une feature chevauche deux tuiles, elles est représenté par deux
     * features. On doit donc posséder un objet permettant de connaitres les
     * features selectionnées qui sera utilisé par la fonction de style
     */
    let selectedFeatures = {}

    // Création de la source vectorielle
    const vectorSource = new VectorTile({
      format: new GeoJSON(),
      tileGrid: Tilegrid.createXYZ(),
      // tilePixelRatio: 16,
      // incompatible OL > 6.14, voir https://github.com/openlayers/openlayers/releases/tag/v6.15.0 si probleme
      tileLoadFunction: tile => {
        // Récupère les informations de la tuile
        const format = tile.getFormat()
        const tileCoord = tile.getTileCoord()

        // Récupère les features à afficher sur la tuile
        const data = tileIndex.getTile(tileCoord[0], tileCoord[1], tileCoord[2])

        // Définition du loader de la tuile
        tile.setLoader((extent, resolution, projection) => {
          // Création des features à afficher
          const features = format.readFeatures(
            JSON.stringify({
              type: 'FeatureCollection',
              features: data ? data.features : [],
            }, this.geojsonvtReplacer),
            {
              extent,
              featureProjection: tilePixels,
            })
          tile.setFeatures(features)
        })
      },
      url: 'data',
    })

    // Création du layer vectoriel
    const vectorLayer = new VectorTile({
      source: vectorSource,
      [this.commonLayer.propertiesName.ID_LAYER]: args.id,
      zIndex: args.zIndex,
    })

    // Définition du style pour la source
    vectorLayer.setStyle((feature, resolution) => {
      const stylesNone = [
        new Style({
          display: 'none',
        }),
      ]
      // Si la feature est masquée
      if (feature.get(this.propertiesName.VISIBLE_FEATURE) !== undefined && feature.get(this.propertiesName.VISIBLE_FEATURE) === false) {
        return stylesNone
      }

      if (typeof args.style === 'function') {
        return args.style({
          feature,
          resolution,
          selected: selectedFeatures[feature.getId()],
          zoom: this.ctx.Map.getView().getZoomForResolution(resolution),
        })
      } else {
        return args.style
      }
    })

    // Ajout du layer
    this.ctx.commonLayer.addLayer(vectorLayer, DATA_LAYER_TYPE, args.ui)

    // Si selectable à false, desactive la selection sur le layer
    if (!args.selectable) {
      this.excludeLayerSelection(args.id)
    }

    // Permet la gestion de la selection pour les couches vectorielles
    this.ctx.on('change:selection', ev => {
      selectedFeatures = {}
      const selection = ev.currentSelection[args.id]
      // Si des données sont selectionnées sur le layer
      if (selection) {
        selection.forEach(feature => {
          // Determine les features selectionnées
          selectedFeatures[feature.getId()] = true
        })
      }
      // Indique au layer qu'il a changé
      vectorLayer.changed()
    })
  };

  /**
   * Permet de créer un layer de données sous forme de HeatMap
   *
   * @param {Object}             options                  Options pour la création
   * @param {string | undefined} options.weightProperties Propriété du poid de la feature
   * @param {string}             options.id               Id du layer à créer
   * @param {string}             options.features         Features à ajouter au layer
   * @param {number | undefined} options.zIndex           zIndex du layer sur la carte
   */
  addHeatMapLayer ({
    id,
    features,
    zIndex,
    weightProperties = 'weight',
    ui,
    displayInLayerSwitcher = true,
  }) {
    this.ctx.commonLayer.addLayer(new CustomHeatmap({
      source: new VectorSource({ features }),
      [this.commonLayer.propertiesName.ID_LAYER]: id,
      weight: weightProperties,
      zIndex,
      displayInLayerSwitcher,
    }), DATA_LAYER_TYPE, ui)
  }

  /**
   * Permet de passer en mode de création pour dessiner une feature
   * et l'ajouter automatiquement sur le layer
   * Nouvelle signature afin d'étendre plus facilement les fonctionnalités
   *
   * @param {string  | number | object} idFeature     IdFeature pour version deprecated ou Options pour la création
   * @param {string | number}    options.idFeature    Identifiant de la feature à créer
   * @param {string}             options.idLayer      Identifiant du layer
   * @param {string}             options.geometryType Type de géométrie à dessiner
   * @param {Object}             options.properties   Propriétés de la feature
   * @param {object}             options.digitalizeOptions  options de l'outil de digitalisation
   * @param {object}             options.createTemplates    templates de création de géométries
   * @param {string}             options.toolId       Outil qui demande la création
   * @param {Boolean}            options.autoValidate Validation automatique (implicite si boite outil masquée)
   * @param {Boolean}            options.hideToolbox  Masquer la boite à outil (impossible pour multigéométrie)
   * @param {Boolean}            options.hideGPS      Masquer l'option GPS sur les outils d'ajout
   * @param {Array}              options.modifyTools  Outils de modifications disponibles ['move','rotate','scale','modifyVertex','removeVertex']
   * @param {Object}             options.createToolOptions Options pour la popup d'outils (pas encore utilisé)
   * @return {Promise}                                Ajoute la feature au calque idLayer et retourne un objet étendu avec la feature une fois créer
   *
  * @example
  *  Ajoute la feature au calque idLayer et retourne un objet étendu avec la feature une fois créer
  * mapviewer.dataLayer.createFeature( {
  *               "geometryType": "GeometryCollection", // type de geometrie a créer (type geojson)
  *               "idLayer": "features-EQU", // calque ou créer la geométrie
  *               "idFeature" : "efefef-1234",
  *               "properties" : {test:1},
  *               "toolId": "create-EQU", // sera renvoyé lors de la levé de l'event createfeature:create
  *               "digitalizeOptions": { // options de l'outil de digitalisation
  *                   "showConstructionPoint": true,
  *                   "maxPoints" : 5,
  *                   "showConstructionLine" : true,
  *               },
  *               autoValidate: false,
  *               hideToolbox: false,
  *               modifyTools: ['move','rotate','scale','modifyVertex','removeVertex'],
  *               hideGPS: [],
  *               "createTemplates": { // templates de création de géométries
  *                   "features": [{ // geojson features
  *                           "type": "Feature",
  *                           "geometry": {
  *                               "type": "MultiPolygon",
  *                               "coordinates": [coordonnées de l objet]
  *                           },
  *                           "properties": { // propriétés recopiées sur la feature openlayers créées (si elle n'existe pas déjà)
  *                               "name": "Assainissement - Arbre"
  *                           },
  *                           "display": { // libellé affiché dans l'ihm de création
  *                               "label": "Assainissement - Arbre"
  *                           }
  *                       }, {
  *                           "type": "Feature",
  *                           "geometry": {
  *                               "type": "MultiPolygon",
  *                               "coordinates": [coordonnées de l objet]
  *                           },
  *                           "properties": {
  *                               "name": "Assainissement - Bac graisse"
  *                           },
  *                           "display": {
  *                               "label": "Assainissement - Bac graisse"
  *                           }
  *                       },
  *                   ]
  *               }
  *           }
  *       }
  *   ]
  *})
   */
  createFeature (idFeature, idLayer, geometryType, properties = {}, padding) {
    if (typeof idFeature !== 'object') {
      console.warn('[deprecated] ancienne signature de la méthode, utiliser createFeature({idFeature,idLayer,type,properties,padding,onFeature,useLRS,toolId})')
      return this.createFeature({ idFeature, idLayer, geometryType, properties, padding })
    }

    // on est en train de create ou modif, si c'est le cas on stoppe avant
    if (this._cancelModifyDrawing) {
      this._cancelModifyDrawing()
    }

    const options = idFeature
    const CREATE_MODE = {
      Point: 'Point',
      LineString: 'LineString',
      Polygon: 'Polygon',
      // pour les multigéométrie on lance la création comme une modification de feature vide
      GeometryCollection: 'GeometryCollection',
      MultiPoint: 'MultiPoint',
      MultiLineString: 'MultiLineString',
      MultiPolygon: 'MultiPolygon',
    }

    return new Promise((resolve, reject) => {
      // On s'assure de l'existance du layer
      if (!this.layerExist(options.idLayer)) {
        return reject(new Error('layerNotFound'))
      }

      // Si pas d'id de feature ou qu'il existe déjà sur le layer
      if (!options.idFeature || this.findFeatures(options.idFeature, options.idLayer).length) {
        return reject(new Error('FeatureAlreadyExist'))
      }

      // On s'assure qu'il y a un mode de création pour le type demandé
      if (!CREATE_MODE[options.geometryType]) {
        return reject(new Error(`invalidTypeFeature ${options.geometryType}`))
      }

      // on créée une feature vide que l'on envoit a l'outil de modification
      const modifyFeature = new Feature()
      const modifyOptions = {
        ...options,
        feature: modifyFeature,
      }
      this.modifyFeature(modifyOptions).then((result) => {
        if (!result) {
          resolve(null)
          return
        }

        result.modifiedFeature.setProperties(options.properties)
        result.modifiedFeature.setId(options.idFeature)
        // ajoute la feature au calque correspondant
        this.addFeatures([result.modifiedFeature], options.idLayer, true)

        // previent l'appli qu'on a fini (par exemple audit re-traite les feature créée)
        const rtn = {
          originalGeometry: result.modifiedFeature.getGeometry(),
          createdFeature: result.modifiedFeature,
          toolId: options.toolId,
        }

        this.ctx.dispatchEvent('createfeature:create', rtn)
        resolve(rtn)
      }).catch((err) => {
        this.ctx.horizontalToolbar.setVisible(true)
        console.error('[datalayer-createFeature] Error : ', err)
        reject(err)
      }) // renvoi d'une erreur du modifyfeature

      // on créée une feature vide que l'on envoit a l'outil de modification
    }).catch((err) => {
      this.ctx.horizontalToolbar.setVisible(true)
      console.error(err)
      throw (err)
    })
  }

  /**
   * Permet de passer en mode de modification pour dessiner une feature
   * et la modifier automatiquement sur le layer
   * Nouvelle signature afin d'étendre plus facilement les fonctionnalités
   *
   * @param {string  | number | object} idFeature     IdFeature pour version deprecated ou Options pour la création
   * @param {string | number}    options.idFeature    Identifiant de la feature à créer
   * @param {string}             options.idLayer      Identifiant du layer
   * @param {string}             options.geometryType Type de géométrie à dessiner
   * @param {object}             options.properties   Propriétés de la feature
   * @param {object}             options.digitalizeOptions  options de l'outil de digitalisation
   * @param {object}             options.createTemplates    templates de création de géométries
   * @param {string}             options.toolId       Outil qui demande la création
   * @param {Boolean}            options.autoValidate Validation automatique (implicite si boite outil masquée)
   * @param {Boolean}            options.hideToolbox  Masquer la boite à outil (impossible pour multigéométrie)
   * @param {Boolean}            options.hideGPS      Masquer l'option GPS sur les outils d'ajout
   * @param {Array}              options.modifyTools  Outils de modifications disponibles ['move','rotate','scale','modifyVertex','removeVertex']
   * @param {Object}             options.createToolOptions Options pour la popup d'outils (pas encore utilisé)
   * @return {Promise}                                Ajoute la feature au calque idLayer et retourne un objet étendu avec la feature une fois créer
   *
  * @example
  *  Ajoute la feature au calque idLayer et retourne un objet étendu avec la feature une fois créer
  * mapviewer.dataLayer.modifyFeature( {
  *               "type": "GeometryCollection", // type de geometrie a créer (type geojson)
  *               "feature": olFeature,  si non présent recherche la feature avec les paramètres idLayer/idFeature
  *               "idLayer": "features-EQU", // calque ou se trouve la geométrie
  *               "idFeature" : "efefef-1234",
  *               "properties" : {test:1},
  *               "toolId": "create-EQU", // sera renvoyé lors de la levé de l'event createfeature:create
  *               "digitalizeOptions": { // options de l'outil de digitalisation
  *                   "showConstructionPoint": true,
  *                   "maxPoints" : 5,
  *                   "showConstructionLine" : true,
  *               },
  *               autoValidate: false,
  *               hideToolbox: false,
  *               modifyTools: ['move','rotate','scale','modifyVertex','removeVertex'],
  *               hideGPS: [],
  *               "createTemplates": { // templates de création de géométries
  *                   "features": [{ // geojson features
  *                           "type": "Feature",
  *                           "geometry": {
  *                               "type": "MultiPolygon",
  *                               "coordinates": [coordonnées de l objet]
  *                           },
  *                           "properties": { // propriétés recopiées sur la feature openlayers créées (si elle n'existe pas déjà)
  *                               "name": "Assainissement - Arbre"
  *                           },
  *                           "display": { // libellé affiché dans l'ihm de création
  *                               "label": "Assainissement - Arbre"
  *                           }
  *                       }, {
  *                           "type": "Feature",
  *                           "geometry": {
  *                               "type": "MultiPolygon",
  *                               "coordinates": [coordonnées de l objet]
  *                           },
  *                           "properties": {
  *                               "name": "Assainissement - Bac graisse"
  *                           },
  *                           "display": {
  *                               "label": "Assainissement - Bac graisse"
  *                           }
  *                       },
  *                   ]
  *               }
  *           }
  *       }
  *   ]
  *})
   */
  modifyFeature (options) {
    // on est en train de create ou modif, si c'est le cas on stoppe avant
    if (this._cancelModifyDrawing) {
      this._cancelModifyDrawing()
    }

    let _modifyControl = null

    const cleanAfterModify = () => {
      if (_modifyControl) { this.ctx.Map.removeControl(_modifyControl) }

      this.changeMapMode('select', 'select-single')
      this.ctx.horizontalToolbar.setVisible(true)
      this._cancelModifyDrawing = null
      _modifyControl = null
    }

    return new Promise((resolve, reject) => {
      if (!options.feature) {
        // Si pas d'id de feature ou qu'il existe déjà sur le layer
        if (options.idFeature && options.idLayer) {
          const features = this.findFeatures(options.idFeature, options.idLayer)
          options.feature = features.length === 1 ? features[0] : null
        }
      }
      if (!options.feature) {
        return reject(new Error(`'[datalayer-modifyFeature] modifyFeatureNotFound ${options.idFeature} - ${options.idLayer}`))
      }

      this.ctx.horizontalToolbar.setVisible(false)

      // pour les bouton d'annulation et de validation de la boite de dialogue modify
      this._cancelModifyDrawing = () => {
        // console.log('cancel modify drawing')
        cleanAfterModify()
        resolve(null)
      }

      const _validateModifyDrawing = (feature) => {
        // console.log('validate modify drawing: ', feature)
        cleanAfterModify()

        // clone et nettoi la feature avant de la renvoyer
        const id = feature.getId()
        feature = feature.clone()
        feature.setId(id)
        Object.values(this.propertiesName).forEach((propertie) => {
          feature.unset(propertie)
        })

        // previent l'appli qu'on a fini (par exemple audit re-traite les feature créée)
        const rtn = {
          originalGeometry: feature.getGeometry(),
          modifiedFeature: feature,
          toolId: options.toolId,
        }

        this.ctx.dispatchEvent('modifyfeature:modify', rtn)
        resolve(rtn)
      }

      const createToolOptions = {
        ...(options?.createToolOptions ? options.createToolOptions : {}), // si on a passé des options cosmétique pour l'outil de création on l'utilise
        feature: options.feature,
        geometryType: options.geometryType,
        toolId: options.toolId,
        viewer: this.ctx,
        digitalizeOptions: options.digitalizeOptions || {},
        createTemplates: options.createTemplates || {},
        title: options.title || null,
        autoValidate: options.autoValidate,
        hideToolbox: options.hideToolbox,
        modifyTools: options.modifyTools,
        hideGPS: options.hideGPS,
        // la boite d'outils de création ne fait qu'utiliser des outils venant de datalayer, on fournit les méthodes et interractions
        methods: {
          cancel: this._cancelModifyDrawing,
          validate: _validateModifyDrawing,
        },
        mapModes: {
          Point: '_drawPointTouch',
          LineString: '_drawLineStringTouch',
          Polygon: '_drawPolygonTouch',
        },
      }

      _modifyControl = new ControlModifyTools(createToolOptions)
      this.ctx.Map.addControl(_modifyControl)
    }).catch((err) => {
      console.error(err)
      // dans tout les cas on fait le ménage
      cleanAfterModify()
      throw (err)
    })
  }

  /**
   * Effectue l'union de feature et génère les évènements utiles
   * @param {Object} options Options de l'outil
   * @param {Array<Feature>}   options.features    - Liste des features a fusionner
   * @param {string | number}  [options.idFeature] - Identifiant de la feature à créer (ou guid)
   * @param {string}           [options.idLayer]   - Identifiant du layer (ou layer de la première feature)
   * @param {toolId}           [options.toolId='tool-union']    - Outil qui demande la création
   * @param {Boolean}          [options.updateLayer = true] - On supprime les anciennes features et on ajout la nouvelle
   * @param {Boolean}          [options.contiguonsPolygon = false] - les polygones doivent être contigue? Sinon peut créer des multipolygone
   * @param {Boolean}          [options.copyProperties = false] - copie les propriétées de la première feature
   * @returns {Feature} features fusionnées
   */
  unionFeature (options = {}) {
    const { features, contiguonsPolygon = false, updateLayer = true, toolId = 'tool-union', copyProperties = false } = options
    let { idLayer, idFeature } = options

    if (!features) {
      throw new Error('\'[datalayer-unionFeature] paramètre features manquant')
      // return reject(new Error(`'[datalayer-unionFeature] paramètre features manqnuant`))
    }

    if (!MapviewerServices.FeatureUnion.isUnionPossible(features, contiguonsPolygon)) {
      throw new Error('\'[datalayer-unionFeature] ces features ne peuvent pas être fusionnées', features)
    }

    // si on indique pas de layer, on utilise le layer de la feature
    idLayer = idLayer || features[0]?.layerSourceId_?.[0]
    idFeature = idFeature || Guid()

    const newFeature = MapviewerServices.FeatureUnion.union(features)
    if (copyProperties) {
      // const { geometry, ...properties } = features[0].getProperties()
      const forbiddenProperties = Object.values(this.propertiesName).concat('geometry')
      const properties = Object.fromEntries(Object.entries(features[0].getProperties())
        .filter(([key]) => !forbiddenProperties.includes(key)))
      newFeature.setProperties(properties)
    }

    newFeature.setId(idFeature)
    newFeature.set('merged', true)

    const rtn = {
      originalGeometry: newFeature.getGeometry(),
      createdFeature: newFeature,
      toolId,
    }

    // on veut maj le calque
    // on supprime les anciennes feature, on ajoute la nouvelle
    if (updateLayer) {
      this.removeFeatures(features, idLayer)
      this.addFeatures([newFeature], idLayer, true)
      this.ctx.dispatchEvent('createfeature:create', rtn)
    }
    rtn.deletedFeatures = features
    this.ctx.dispatchEvent('createfeature:union', rtn)
    return rtn
  }

  async splitFeature (options = {}) {
    const { feature, updateLayer = true, toolId = 'tool-split', copyProperties = false } = options
    let { idLayer } = options

    if (!feature) {
      throw new Error('\'[datalayer-splitFeature] paramètre feature manquant')
    }

    if (!isSplitPossible(feature)) {
      throw new Error('\'[datalayer-splitFeature] cette feature ne peut pas être découpés', feature)
    }

    // si on indique pas de layer, on utilise le layer de la feature
    idLayer = idLayer || feature?.layerSourceId_?.[0]
    // idFeature = idFeature || Guid()

    const newFeatures = await splitFeature({ feature, viewer: this.ctx, toolId })

    if (!newFeatures) {
      throw new Error('\'[datalayer-splitFeature] Opération annulée')
    }

    if (copyProperties) {
      const forbiddenProperties = Object.values(this.propertiesName).concat('geometry')
      const properties = Object.fromEntries(Object.entries(feature.getProperties())
        .filter(([key]) => !forbiddenProperties.includes(key)))
      // const { geometry, ...properties } = feature.getProperties()
      newFeatures.forEach(newFeature => newFeature.setProperties(properties))
    }

    newFeatures.forEach((newFeature, index) => {
      newFeature.setId(Guid())
      newFeature.setProperties({
        splitted: true,
        parentId: feature.getId(),
        index,
      })
    })

    const rtn = {
      createdFeatures: newFeatures,
      toolId,
    }

    // on veut maj le calque
    // on supprime les anciennes feature, on ajoute la nouvelle
    if (updateLayer) {
      this.removeFeature(feature, idLayer)
      this.addFeatures(newFeatures, idLayer, true)
      newFeatures.forEach(newFeature => {
        this.ctx.dispatchEvent('createfeature:create',
          {
            originalGeometry: newFeature.getGeometry(),
            createdFeature: newFeature,
            toolId,
          })
      })
    }
    rtn.deletedFeature = feature
    this.ctx.dispatchEvent('createfeature:split', rtn)
    return rtn

    // return
  }

  startDragAndDropFeatures (mapMode, layerList) {
    this._dragAndDropInteraction.set('draggableLayerList', layerList)
    this.changeMapMode(mapMode)
  }

  stopDragAndDropFeatures () {
    this.ctx.commonLayer.disableCurrentMapMode()
  }

  /**
   * Ajout d'un tag sur une feature
   * @param {Feature} feature Feature OpenLayer à activer
   * @param {String} tag At tag a ajouter a la feature
   * @param {Object} options options
   * @param {boolean} options.noRefresh Permet de ne par recharger la carte (changement non visible).
   */
  addTagToOlFeature (feature, tag, { noRefresh } = {}) {
    const currentFeatureTags = feature.get(this.propertiesName.TAG_FEATURE) || {}
    currentFeatureTags[tag] = true
    feature.set(this.propertiesName.TAG_FEATURE, currentFeatureTags, true)
    if (!noRefresh) {
      feature.changed()
    }
  }

  removeTagToOlFeature (feature, tag, { noRefresh } = {}) {
    if (this.hasTag(feature, tag)) {
      const currentFeatureTags = feature.get(this.propertiesName.TAG_FEATURE) || {}
      delete currentFeatureTags[tag]
      feature.set(this.propertiesName.TAG_FEATURE, currentFeatureTags, true)
      if (!noRefresh) {
        feature.changed()
      }
    }
  }

  hasTag (feature, tag) {
    if (!feature) { return false }
    return feature.get(this.propertiesName.TAG_FEATURE)?.[tag] || false
  }

  addTagToFeature (idFeature, idLayer, tag, { noRefresh } = {}) {
    const feature = this.getFeature(idFeature, idLayer)
    if (feature) {
      this.addTagToOlFeature(feature, tag, { noRefresh })
    }
  }

  removeTagToFeature (idFeature, idLayer, tag, { noRefresh } = {}) {
    const feature = this.getFeature(idFeature, idLayer)
    if (feature) {
      this.removeTagToOlFeature(feature, tag, { noRefresh })
    }
  }

  /**
   * Activer une feature OpenLayers
   * @param {Feature} feature Feature OpenLayer à activer
   * @param {boolean} noRefresh Permet de ne par recharger la carte (changement non visible).
   */
  activeOlFeature (feature, { noRefresh } = {}) {
    feature.set(this.propertiesName.ACTIVATED_FEATURE, true, noRefresh)
  }

  /**
   * Desactiver une feature OpenLayers
   * @param {Feature} feature Feature OpenLayer à desactiver
   * @param {boolean} noRefresh Permet de ne par recharger la carte (changement non visible).
   */
  desactiveOlFeature (feature, { noRefresh } = {}) {
    feature.set(this.propertiesName.ACTIVATED_FEATURE, false, noRefresh)
  }

  /**
   * Activer des features OpenLayers
   * @param {Array<Feature>} features Feature OpenLayer à activer
   * @param {boolean} noRefresh Permet de ne par recharger la carte (changement non visible).
   */
  activeOlFeatures (features, { noRefresh } = {}) {
    features.forEach(item => this.activeOlFeature(item, { noRefresh }))
  }

  /**
   * Desactiver des features OpenLayers
   * @param {Array<Feature>} features Feature OpenLayer à desactiver
   * @param {boolean} noRefresh Permet de ne par recharger la carte (changement non visible).
   */
  desactiveOlFeatures (features, { noRefresh } = {}) {
    features.forEach(item => this.desactiveOlFeature(item, { noRefresh }))
  }

  /**
   * Activer une feature
   * @param {*} idFeature Identifiant de la feature à activer
   * @param {string} idLayer Identifiant du layer
   * @param {boolean} noRefresh Permet de ne par recharger la carte (changement non visible).
   */
  activeFeature (idFeature, idLayer, { noRefresh } = {}) {
    const feature = this.getFeature(idFeature, idLayer)
    if (feature) {
      this.activeOlFeature(feature, { noRefresh })
    }
  }

  /**
   * Desactiver une feature
   * @param {*} idFeature Identifiant de la feature à desactiver
   * @param {string} idLayer Identifiant du layer
   * @param {boolean} noRefresh Permet de ne par recharger la carte (changement non visible).
   */
  desactiveFeature (idFeature, idLayer, { noRefresh } = {}) {
    const feature = this.getFeature(idFeature, idLayer)
    if (feature) {
      this.desactiveOlFeature(feature, { noRefresh })
    }
  }

  /**
   * Activer des features
   * @param {Array<*>} idFeature Identifiants des features à activer
   * @param {string} idLayer Identifiant du layer
   * @param {boolean} noRefresh Permet de ne par recharger la carte (changement non visible).
   */
  activeFeatures (idFeatures, idLayer, { noRefresh } = {}) {
    // Créer une map id/features
    // Plus performant qu'un filter/includes
    const featuresMap = this.getFeatures(idLayer).reduce((acc, feature) => {
      acc[feature.getId()] = feature
      return acc
    }, {})

    // Récuère les features à partir des ids
    const features = idFeatures.map(id => featuresMap[id]).filter(item => item)

    this.activeOlFeatures(features, { noRefresh })
  }

  /**
   * Desactiver des features
   * @param {Array<*>} idFeature Identifiants des features à desactiver
   * @param {string} idLayer Identifiant du layer
   * @param {boolean} noRefresh Permet de ne par recharger la carte (changement non visible).
   */
  desactiveFeatures (idFeatures, idLayer, { noRefresh } = {}) {
    // Créer une map id/features
    // Plus performant qu'un filter/includes
    const featuresMap = this.getFeatures(idLayer).reduce((acc, feature) => {
      acc[feature.getId()] = feature
      return acc
    }, {})

    // Récuère les features à partir des ids
    const features = idFeatures.map(id => featuresMap[id]).filter(item => item)

    this.desactiveOlFeatures(features, { noRefresh: true })

    if (!noRefresh && this.layerExist(idLayer)) {
      this.getSourceLayer(idLayer).changed()
    }
  }

  /**
   * Descativer toutes les features
   * Si idLayer, on ne désactive que les features de ce layer
   * @param {string|undefined} idLayer Identifiant du layer
   * @param {boolean} noRefresh Permet de ne par recharger la carte (changement non visible).
   */
  desactiveAllFeatures ({ idLayer, noRefresh } = {}) {
    if (idLayer) {
      this.desactiveOlFeatures(this.getFeatures(idLayer), { noRefresh: true })
    } else {
      this.getLayers().forEach(layer =>
        this.desactiveOlFeatures(this.getFeatures(layer), { noRefresh: true }))
    }
    if (!noRefresh && this.layerExist(idLayer)) {
      this.getSourceLayer(idLayer).changed()
    }
  }

  /**
   * Options pour le mode de sélection bbox
   * @param {Object} options
   */
  setRectangularModeOptions (options) {
    if (options && options.include) {
      this.setgeometrieTypesSelectableInRectangularMode(options.include)
    }

    // sélection-t-on les features invisibles dans ce mode?
    this.selectHiddenFeatureInRectangularMode = options && options.selectHiddenFeature !== false
  }

  /**
   * garde les géométries de type de features filtrées en mode rectangulaire
   * @param {Array<string>} geometries
   */
  setgeometrieTypesSelectableInRectangularMode (geometries) {
    if (geometries && Array.isArray(geometries)) {
      this.geometrieTypesSelectableInRectangularMode = geometries
    } else {
      this.geometrieTypesSelectableInRectangularMode = []
    }
  }

  /**
   * Teste si la feature doit être sélectionnée lors d'une selection rectangulaire
   * @param {Object} feature
   */
  isSelectableInRectangularMode (feature) {
    return (!this.geometrieTypesSelectableInRectangularMode.length ||
    this.geometrieTypesSelectableInRectangularMode.includes(feature.getGeometry().getType())) &&
    (feature.get(this.propertiesName.VISIBLE_FEATURE) !== false || this.selectHiddenFeatureInRectangularMode)
  }

  /**
   * change le mode de sélection
   * @param {SelectModes} mode mode de sélection @link DataLayer.SelectModes
   */
  setSelectMode (mode) {
    this.selectMode = mode
    this.ctx.dispatchEvent('change:selectMode', { selectMode: mode })
  }

  /**
   * Renvoit le mode de sélection actuel
   * @returns {SelectModes}
   */
  getSelectMode () {
    return this.selectMode
  }

  /** instance du viewer
   * @type Mapviewer
   */
  get viewer () {
    return this.ctx
  }

  /** Fonction commonLayer */
  get commonLayer () {
    return this.viewer.commonLayer
  }
}

/**
 * @enum {MapMode}
 */
DataLayer.MapModes = {
  /**
   * Mode de modification de features
   */
  modify: 'modify',
  /**
   * Mode de sélection cumulant les features
   */
  multiselect: 'multiselect',
  /**
   * Mode de sélection simple
   */
  select: 'select',
  /**
   * Mode de sélection par tracé d'une zone rectangulaire
   */
  rectangular: 'rectangular',
  /**
   * Mode de sélection de features contenues dans une autre
   */
  zonal: 'zonal',
  /**
   * Mode de sélection via le tracé d'un polygon sur la carte
   */
  polygon: 'polygon',
  /**
   * Mode de mesure de distances
   */
  measureLine: 'measureLine',
  /**
   * Mode de mesure de surfaces
   */
  measureArea: 'measureArea',
  /**
   * Mode  drag & drop sur layer
   */
  dragAndDrop: 'dragAndDrop',
}

/**
 * Liste des modes de sélection disponibles
 * @enum {SelectMode}
 */
DataLayer.SelectModes = {
  /**
   * la sélection remplace la précédente (mode par défaut)
   */
  REPLACE: 'replace',
  /**
   * la re-selection d'un élement le déselectionne
   */
  TOGGLE: 'toggle',
  /**
   * Mode de sélection additive
   */
  ADD: 'add',
}
export const SelectModes = DataLayer.SelectModes

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

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