Source: bglayer/background-layer.js

/* eslint-disable prefer-promise-reject-errors */
/* eslint-disable no-unused-expressions */
/* eslint vars-on-top: 0, newline-after-var: 0, consistent-return:0, no-throw-literal:0 */
import Axios from 'axios'

import Tile from 'ol/layer/Tile'
import WebGLTile from 'ol/layer/WebGLTile'

import OSM from 'ol/source/OSM'
import BingMaps from 'ol/source/BingMaps'
import TileArcGISRest from 'ol/source/TileArcGISRest'
import XYZ from 'ol/source/XYZ'
import StadiaMaps from 'ol/source/StadiaMaps'

import WMTS, { optionsFromCapabilities } from 'ol/source/WMTS'
import WMTSCapabilities from 'ol/format/WMTSCapabilities'
import KML from 'ol/format/KML'

import TileImage from 'ol/source/TileImage'
import ImageWMS from 'ol/source/ImageWMS'
import TileWMS from 'ol/source/TileWMS'
import ImageMapGuide from 'ol/source/ImageMapGuide'
import FallbackXYZ from '../sources/fallbackXYZ'
import DataTile from 'ol/source/DataTile'
import VectorSource from 'ol/source/Vector'

import Image from 'ol/layer/Image'

import TileGrid from 'ol/tilegrid/TileGrid'
import TilegridWMTS from 'ol/tilegrid/WMTS'

import Attribution from 'ol/control/Attribution'

import * as proj from 'ol/proj'
import * as Extent from 'ol/extent'

import { unByKey } from 'ol/Observable'

import MultiClipInteraction from '../interaction/multiclips-interaction'

import { applyStyle } from 'ol-mapbox-style'
import { VectorTile as VectorTileLayer, Vector as VectorLayer } from 'ol/layer'
import { getCenter } from 'ol/extent'

import pickBy from 'lodash/pickBy'

import MapboxStyleParser from 'geostyler-mapbox-parser'
import SLDParser from 'geostyler-sld-parser'
import QgisStyleParser from 'geostyler-qgis-parser'
import OpenLayersParser from 'geostyler-openlayers-parser'
import ShapefileDataParser from 'geostyler-shapefile-parser'

import { geoJsonToFeature } from '../tools/services/geojsons'
import { SdCard } from '../tools/services/sd-card'
import { getWfsVectorSource } from '../utils/wfs-utils'
import { base64ToUtf8 } from '../utils/Base64'

/**
 * Module de gestion des fonds de plan
 * @module backgroundLayer
 */
const libNamespace = 'backgroundLayer'

const LAYER_TYPES = [
  'osm',
  'bingmaps',
  'arcgis',
  'stamen',
  'stadiaMaps',
  'mapquest',
  'here',
  'mapguidetiled',
  'mapguideuntiled',
  'wmstiled',
  'wmsuntiled',
  'layerXYZ',
  'wmts',
  'vectorTile',
  'ign',
  'kml',
  'vector',
]

const BACKGROUND_LAYER_TYPE = 'background'
const HIGHLIGHT_MAP_MODE = 'highlight'

let timerDelayManageVisibility = null

/** Service gérant les couches de fonds de plan */
class BackgroundLayer {
  /** instance du viewer
   * @type {import("../core/core").default}
   */
  ctx = null
  /**  interraction clip (masque) */
  clipInteraction = null
  constructor (viewer, options) {
    viewer.BACKGROUNDLAYER_LOADED = true

    /** instance du viewer
     * @type Mapviewer
     */
    this.ctx = viewer

    Object.assign(this, options)

    this.ctx.Map.getLayers().on('add', this.manageVisibility.bind(this))

    // 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 => {
      if (layer.getSource() instanceof VectorSource) {
        const source = layer.getSource()

        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)
          }
        })
      }
    })
  }

  /**
   * Gestion de ce qui se passe quand un background change de visibilitée
   * @param {ol.layer} layer
   */
  manageVisibility (event) {
    const layerIdProperty = this.commonLayer.propertiesName.ID_LAYER

    if (this.viewer.commonLayer.isGroup(event.element)) {
      const listenerAddToGroupId = event.element.getLayers().on('add', this.manageVisibility.bind(this))
      const listenerId = event.element.on('change:visible', () => {
        if (timerDelayManageVisibility) {
          clearTimeout(timerDelayManageVisibility)
          timerDelayManageVisibility = null
        }

        timerDelayManageVisibility = setTimeout(_manageProjection.bind(this), 50)
      })

      const layerId = event.element.get(layerIdProperty)
      this.viewer.once(`removedLayer:${layerId}`, () => {
        // Retire les events de la Map et la View liés au layer
        unByKey(listenerAddToGroupId, event.element)
        unByKey(listenerId, event.element)
      })
      return
    }
    if (event.element.get(this.viewer.commonLayer.propertiesName.TYPE_LAYER) !== BACKGROUND_LAYER_TYPE) {
      return
    }
    // calque Background, let's go
    const layer = event.element
    function _manageProjection () {
      // calcule les calques Background visible pour récupérer la projection prioritaire
      const visibleBackgrounds = this.getLayers().filter(layer => {
        const group = this.viewer.commonLayer.getLayerGroup(layer)
        return layer.getVisible() && (!group || group.getVisible())
      })
      visibleBackgrounds.sort((a, b) => {
        const aPriority = a.get(this.commonLayer.propertiesName.PROJECTION_PRIORITY) || 0
        const bPriority = b.get(this.commonLayer.propertiesName.PROJECTION_PRIORITY) || 0
        return bPriority - aPriority
      })
      const newProjection = this.getProjection(visibleBackgrounds?.[0])
      if (this.viewer.setViewProjection(newProjection)) {
        // la projection a changé, on recharge les calques
        visibleBackgrounds.forEach(layer => {
          if (!(layer.getSource() instanceof VectorSource) && layer.getSource().refresh) {
            layer.getSource().refresh()
          }
        })
      }
    }
    // this.getLayers()
    const layerId = layer.get(layerIdProperty)
    // abonnement a la visibilité d'un calque pour
    const listenerId = layer.on('change:visible', () => {
      if (timerDelayManageVisibility) {
        clearTimeout(timerDelayManageVisibility)
        timerDelayManageVisibility = null
      }

      timerDelayManageVisibility = setTimeout(_manageProjection.bind(this), 50)
    })

    this.viewer.once(`removedLayer:${layerId}`, () => {
      // Retire les events de la Map et la View liés au layer
      unByKey(listenerId)
    })

    if (timerDelayManageVisibility) {
      clearTimeout(timerDelayManageVisibility)
      timerDelayManageVisibility = null
    }

    timerDelayManageVisibility = setTimeout(_manageProjection.bind(this), 50)
  }

  getProjection (layer) {
    if (!layer) {
      return null
    }
    return layer.getSource() instanceof OSM ||
      layer.getSource() instanceof BingMaps ||
      layer.getSource() instanceof StadiaMaps
      ? 'EPSG:3857'
      : layer.getSource().getProjection()
  }

  /**
  * Créé un layer avec comme source Open Street Map
  *
  * @param {string} idLayer Id du layer
  * @param {Object} options Options pour Bing Maps
  * @param {string} options.url Url personnalisée pour OSM
  * @param {Boolean?} [options.forceCanvas=false] force l'utilisation du moteur canvas plutot que webgl pour le rendu des tuiles
  */
  addOSM (idLayer, options = {}) {
    const tileOptions = {
      source: options.url ? new OSM({ url: `${options.url}/{z}/{x}/{y}` }) : new OSM(),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    }

    const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)
    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
    this.allTileLoaded(idLayer)
  }

  /**
  * Créer un layer avec comme source Bing Maps
  *
  * @param {string} idLayer Id du layer
  * @param {Object} options Options pour Bing Maps
  * @param {string | undefined} options.culture Code de la culture. Par defaut, "en-us"
  * @param {string} options.key API Key de Bing Maps
  * @param {string} options.imagerySet Type de l'imagerie
  * @param {number | undefined} options.maxZoom Niveau de zoom max. Par défaut niveau annocé par le service BingMaps (21)
  * @param {boolean | undefined} options.wrapX Séparer le monde horizontalement. Par défaut true
  * @param {Boolean?} [options.forceCanvas=false] force l'utilisation du moteur canvas plutot que webgl pour le rendu des tuiles
  */
  addBingMaps (idLayer, options = {}) {
    console.warn('Bing Map disparait en juin 2025, utiliser AzureMap')

    const tileOptions = {
      source: new BingMaps(options),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    }

    const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)
    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
    this.allTileLoaded(idLayer)
  }

  /**
  * Créer un layer avec comme source Azure Map
  *
  * @param {string} idLayer Id du layer
  * @param {Object} options Options pour Azure Map
  * @param {string?} options.language Code de la culture. Par defaut, "fr-FR" {@link https://learn.microsoft.com/fr-fr/azure/azure-maps/supported-languages|Microsoft liste des language}
  * @param {string} options.apiKey subscriptionKey de Azure Maps {@link https://learn.microsoft.com/fr-fr/azure/azure-maps/quick-demo-map-app|Microsoft créer compte}
  * @param {string?} [options.tilesetId = microsoft.imagery] Type de l'imagerie a afficher {@link https://learn.microsoft.com/fr-fr/rest/api/maps/render/get-map-tile?view=rest-maps-2024-04-01&tabs=HTTP#tilesetid|Microsoft liste des tileset}
  * @param {string?} [options.apiVersion = 2.0] Version de l'api azure
  * @param {Boolean?} [options.forceCanvas=false] force l'utilisation du moteur canvas plutot que webgl pour le rendu des tuiles
  *
  */
  addAzureMap (idLayer, options = {}) {
    if (!options.apiKey) {
      console.warn('addAzureMap jeton manquant, calque non ajouté')
      return
    }

    const subscriptionKey = options.apiKey
    const tilesetId = options?.tilesetId || 'microsoft.imagery'
    const language = options?.language || 'fr-FR'
    const apiVersion = options?.apiVersion || '2.0'

    const tileOptions = {
      source: new XYZ({
        url: `https://atlas.microsoft.com/map/tile?subscription-key=${subscriptionKey}&api-version=${apiVersion}&tilesetId=${tilesetId}&zoom={z}&x={x}&y={y}&language=${language}`,
      }),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    }

    const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)
    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
    this.allTileLoaded(idLayer)
  }

  /** Si le token n'est pas de type string, on l'inteprete */
  extractToken (token) {
    if (token === null || typeof token === 'string') {
      return token
    }
    let tokenFromStorage
    switch (token.type.toUpperCase()) {
      case 'LOCALSTORAGE':
        tokenFromStorage = localStorage.getItem(token.path)
        if (tokenFromStorage !== null && tokenFromStorage !== '') {
          try {
            tokenFromStorage = JSON.parse(tokenFromStorage)
            token = tokenFromStorage.token
          } catch (ex) {
            console.error("erreur parsing du token, format {token:'1234'} attendu, recu : ", token)
          }
        }
        break
    }

    return token
  }

  /**
  * Créé un layer avec une source ArcGIS
  *
  * @param {string} options Id du layer
  * @param {Object} options Tile ArcGIS Rest options
  * @param {string | undefined} options.url Url du service ArcGIS MapServer
  * - exemple : 'http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Specialty/ESRI_StateCityHighway_USA/MapServer'
  * @param {array<string> | undefined} options.urls Urls du service ArcGIS Rest. A utiliser à la place de url si le service support les urls multiples pour les requêtes d'export
  * @param {boolean | undefined} options.useLegacy Force l'utilisation de TileArcGISRest (si le calque n'est pas compatible tuiles)
  * @param {array<Attribution>} options.attributions Attributions
  * @param {null | string | undefined} options.crossOrigin L'attribut crossOrigin pour charger les images
  * @param {Object<string, *> | undefined} options.params Paramètres ArcGIS pouvant être utilisés mais non spécifiés
  * - Exemple : params= {token:'12345'}
  * - Exemple : params= {token: {type:'localStorage','path':'KARTEIS_ARCGIS_TOKEN'}} // recherche le token dans le localStorage[KARTEIS_ARCGIS_TOKEN]
  * @param {TileGrid | undefined} options.tileGrid Grille de tuilles
  * @param {ol.proj.ProjectionLike} options.projection Projection
  * @param {ol.TileLoadFunctionType | undefined} options.tileLoadFunction Fonction optionnelle pour charger une tuille d'une Url donnée
  * @param {boolean | undefined} options.wrapX Sépare le monde horizontalement. Par défaut "true"
  * @param {Boolean?} [options.forceCanvas=false] force l'utilisation du moteur canvas plutot que webgl pour le rendu des tuiles
  * @param {function} resolve Résolution de promesse a executer
  */
  addArcGis (idLayer, options, resolve) {
    // méthode de chargement pour les calques arcgisRest (méthode export)
    if (options.useLegacy) {
      if (options?.params?.token) {
        options.params.token = this.extractToken(options.params.token)
      }

      const tileOptions = {
        source: new TileArcGISRest(options),
        [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
        title: options.title,
        zIndex: options && options.zIndex,
      }

      const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)

      this.ctx.commonLayer.updateCommonProperties(layer, options)
      this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
      this.allTileLoaded(idLayer)
      if (typeof resolve === 'function') { resolve(null) }
      return
    }
    // nouvelle méthode: on lit le layerDefinition afin de déduire comment afficher le calque
    let token = options.params ? options.params.token : undefined
    token = token === undefined ? null : token
    token = this.extractToken(token)
    return Axios.get(
      options.url,
      {
        params: {
          f: 'json',
          token,
        },
      }
    ).then(response => response.data)
      .then(layerDefinition => {
        if (layerDefinition.error) {
          const message = `${JSON.stringify(layerDefinition)}`
          const size = 256
          const lineHeight = 30
          const canvas = document.createElement('canvas')
          canvas.width = size
          canvas.height = size
          const context = canvas.getContext('2d')
          context.strokeStyle = 'white'
          context.textAlign = 'center'
          context.font = '12px sans-serif'

          const tileOptions = {
            [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
            title: options.title,
            zIndex: options && options.zIndex,
            source: new DataTile({
              loader: function (z, x, y) {
                const half = size / 2
                context.clearRect(0, 0, size, size)
                context.fillStyle = 'rgba(255, 00, 0, 0.3)'
                context.fillRect(0, 0, size, size)
                context.fillStyle = 'black'
                context.fillText('Arcgis Layer Error', half, half - lineHeight)
                context.fillText(message, half, half)
                context.strokeRect(0, 0, size, size)
                const data = context.getImageData(0, 0, size, size).data
                // converting to Uint8Array for increased browser compatibility
                return new Uint8Array(data.buffer)
              },
              // disable opacity transition to avoid overlapping labels during tile loading
              transition: 0,
            }),
          }

          const errorLayer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)

          this.ctx.commonLayer.updateCommonProperties(errorLayer, options)
          this.ctx.commonLayer.addLayer(errorLayer, BACKGROUND_LAYER_TYPE)
          if (typeof resolve === 'function') { resolve(null) }
          return
        }
        const srid = 'EPSG:' + layerDefinition.spatialReference.latestWkid
        // todo : verifi qu'on a une projection connue du systeme
        /* if (!isProjectionLoaded(srid)) {
          console.log("Fond de plan : "+ params.url +"\nSRID EPSG:" + layerDefinition.data.extent.spatialReference.latestWkid + " non connu :  Système de projection du service non connu de l'application Dashboard.\nBascule vers OpenStreetMap")
          return loadOsmFondDePlan()
        } */

        // si le fond de plan est premium est nécessite un token, on l'utilise dans les requêtes suivantes
        const tokenTxt = token ? '?token=' + token : ''

        if (layerDefinition.capabilities.toUpperCase().split(',').indexOf('TILEMAP') !== -1 || layerDefinition.singleFusedMapCache) {
          // construit les paramètres de la grille openlayer en lisant les informations depuis le service
          // les résolutions (niveaux de zoom)

          const resolutions = []
          const tileInfo = layerDefinition.tileInfo
          if (tileInfo) {
            for (let i = 0, ii = tileInfo.lods.length; i < ii; ++i) {
              resolutions.push(tileInfo.lods[i].resolution)
            }
          }

          // la grille
          const tilegrid = new TileGrid({
            resolutions,
            origin: [tileInfo.origin.x, tileInfo.origin.y],
            tileSize: [tileInfo.cols, tileInfo.rows],
          })

          const layerOptions = JSON.parse(JSON.stringify(options))
          // doit-on laisser le crossOrigin la ou dans les options?
          layerOptions.crossOrigin = 'anonymous'
          layerOptions.url = options.url + '/tile/{z}/{y}/{x}' + tokenTxt
          layerOptions.maxZoom = resolutions.length
          layerOptions.tileGrid = tilegrid
          layerOptions.projection = srid

          const tileOptions = {
            source: new XYZ(layerOptions),
            [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
            title: options.title,
            zIndex: options && options.zIndex,
          }

          const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)

          this.ctx.commonLayer.updateCommonProperties(layer, options)
          this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
          this.allTileLoaded(idLayer)
          if (typeof resolve === 'function') { resolve(null) }
        } else if (layerDefinition.capabilities.toUpperCase().split(',').indexOf('MAP') !== -1) {
          // on a choisi une couche qui ne propose pas de tuile: on utilise la fonction Export du service ESRI
          options.useLegacy = true
          this.addArcGis(idLayer, options, resolve)
        } else {
          throw ('Fond de plan : ' + options.url + "\nCapatibilie Tilemap ou Map manquante : le fond de plan n'est pas un service de carte (trouvé: " + layerDefinition.capabilities + ').')
        }
      })
  }

  /**
  * Créer un calque tuilé suivant la norme WMTS
  * @param {string} idLayer
  * @param {object} options
  * @param {string} options.url url du getCapabilities
  * @param {object | undefined} [options.params = null] paramètres optionnels
  * @param {string | undefined} [options.params.layer=null] nom du calque a utiliser dans la couche WMTS (sinon utilise le premier trouvé)
  * @param {string | undefined} [options.params.tileMatrixSet=null] Nom du Tile Matrix Set a utiliser dans la couche WMTS (sinon utilise le premier trouvé)
  * @param {string | undefined} [options.params.token=null] Token pour utilisé un serveur ARCGIS protégé
  * @param {Boolean?} [options.forceCanvas=false] force l'utilisation du moteur canvas plutot que webgl pour le rendu des tuiles
  * - Exemple : params= {token: 'supertoken'}
  * - Exemple : params= {token: {type:'localStorage','path':'KARTEIS_ARCGIS_TOKEN'}} // recherche le token dans le localStorage[KARTEIS_ARCGIS_TOKEN]
  */
  async addWMTS (idLayer, options) {
    const tileOptions = {
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    }

    const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)

    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)

    let token = options?.params?.token || null
    token = this.extractToken(token)
    // lit les capacité du WMTS
    const response = await Axios.get(options.url, {
      params: {
        token,
      },
    })
    const parser = new WMTSCapabilities()
    const result = parser.read(response.data)

    // TODO Vérifier le token sur un cas réel : ajoute le token a chaque tuile
    if (token) {
      const tokenTxt = token ? '?token=' + token : ''
      result.Contents.Layer = result.Contents.Layer.map(layer => {
        layer.ResResourceURLponse = layer.ResourceURL.map((resourceURL) => {
          return { ...resourceURL, ...{ template: resourceURL.template + tokenTxt } }
        })
        return layer
      })
    }
    // utilise soit l'identifiant de calque et tilematrix depuis le yaml, soit le premier de la liste (souvent on en a 1)
    const wmtsOptions = optionsFromCapabilities(result,
      {
        layer: options?.params?.layer || result.Contents.Layer[0].Identifier,
        matrixSet: options?.params?.tileMatrixSet || result.Contents.Layer[0].TileMatrixSetLink[0].TileMatrixSet,
      }
    )

    layer.setSource(new WMTS(wmtsOptions))
  }

  /**
   * Créer un calque vectoriel tuilé
   * @link https://github.com/openlayers/ol-mapbox-style#applystyle
   * @param {string} idLayer
   * @param {Object} options
   * @param {String} options.styleUrl Url du fichier de style compatible MapBox
   * @param {String} [options.tileLoadUrl = null] Url des fichier pbf pour surchager la fonction tileLoad, doit contenir {z}/{y}/{x}
   * @param {Object} options.params paramètres
   * @param {String | String[]} options.params.sourceOrLayers source key or an array of layer ids from the Mapbox Style object. When a source key is provided, all layers for the specified source will be included in the style function. When layer ids are provided, they must be from layers that use the same source. When not provided or a falsey value, all layers using the first source specified in the glStyle will be rendered.
   * @param {String | Options} [options.params.optionsOrPath] Options. Alternatively the path of the style file (only required when a relative path is used for the "sprite" property of the style). @link https://github.com/openlayers/ol-mapbox-style#interfacestypesoptionsmd
   * @param {Number[]} [options.params.resolutions] Resolutions for mapping resolution to zoom level. Only needed when working with non-standard tile grids or projections.
   */
  async addVectorTile (idLayer, options) {
    const layer = new VectorTileLayer({
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
      declutter: true,
      renderMode: 'hybrid',
    })

    // TODO gerer le token mapbox? (peut-être suffisant dans optionsOrPath)
    let tokenManager
    if (options?.params?.token) {
      // TODO: il existe un tokenManager avec cette signature de token => on le réutilise et on ajoute le layerId dans les layers
      tokenManager = await this.ctx.tokenManagerPool.addTokenSource('layer', idLayer, options.params.token) // new TokenManager(options.params.token)
    }

    const styleUrl = options.styleUrl

    this.ctx.commonLayer.updateCommonProperties(layer, options)

    let optionsOrPath = options?.params?.optionsOrPath || {}
    const resolutions = options?.params?.optionsOrPath || undefined

    optionsOrPath = {

      transformRequest (url, type) {
        // Permet aux requête passant d'applyStyle d'être modifiées
        if (tokenManager) {
          url = tokenManager.addTokenToUrl(url)
          return new Request(url)
        }

        // https://github.com/openlayers/ol-mapbox-style#transformrequest
        // Si besoin on ajoutera dans les params des règles de transformation des url
        // exemple de besoin :
        // if (type === 'Tiles') {
        //   return new Request(
        //     url.replace('/tile/', '/VectorTileServer/tile/')
        //   );
        // }
      },
      ...optionsOrPath,

    }
    try {
      await applyStyle(layer, styleUrl, options.params.sourceOrLayers, optionsOrPath, resolutions)
    } catch (e) {
      throw new Error('VectorTiles: impossible de charger le calque VectorTiles', e)
    }
    if (options.tileLoadUrl) {
      const tileGrid = layer.getSource().getTileGrid()
      // on set TileLoadUrl pour les service ESRI car ils ne génèrent pas les tuile pour tous les niveaux de zoom,
      // si on ne trouve pas une tuile on remonte d'un niveau de zoom
      async function loadTile (tileCoord, extent, format, projection, nbTry = 0) {
        let url = options.tileLoadUrl
          .replace('{z}', tileCoord[0])
          .replace('{x}', tileCoord[1])
          .replace('{y}', tileCoord[2])
        if (tokenManager) {
          url = tokenManager.addTokenToUrl(url)
        }

        try {
          const response = await fetch(url)
          // tuile non trouvée, on remonte d'un niveau
          if (response.status === 404) {
            const tileCenter = getCenter(extent)
            const newtileCoord = tileGrid.getTileCoordForCoordAndZ(
              tileCenter,
              tileCoord[0] - 1
            )

            if (tileCoord <= 0) {
              return []
            }

            const newExtent = tileGrid.getTileCoordExtent(newtileCoord)
            // console.log('old', tileCoord, extent, 'new', newtileCoord, newExtent);
            return await loadTile(newtileCoord, newExtent, format, projection, nbTry++)
          } else {
            const data = await response.arrayBuffer()
            // tuile trouvée, on lit le pbf et on renvoit les features
            // const format = tile.getFormat(); // ol/format/MVT configured as source format
            // TODO tester avec du changement de projection
            const features = format.readFeatures(data, {
              extent,
              featureProjection: projection,
            })
            return features
          }
        } catch (err) {
          layer.getSource().removeLoadedExtent(extent)
          console.log(err)
          return []
        }
      }

      layer.getSource().setTileLoadFunction(function (tile, url) {
        tile.setLoader(function (extent, resolution, projection) {
          loadTile(tile.tileCoord, extent, tile.getFormat(), projection, 0).then(
            function (features) {
              tile.setFeatures(features)
            }
          )
        })
      })
    }

    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
  }

  /**
  * Créé un layer avec une source Stamen
  *
  * @param {string} idLayer Id du layer
  * @param {Object} options Options pour Stamen
  * @param {string} options.layer Nom du layer sur Stamen
  * @param {number | undefined} options.minZoom Zoom minimum
  * @param {number | undefined} options.maxZoom Zoom maximum
  * @param {ol.TileLoadFunctionType | undefined} options.tileLoadFunction Fonction optionnelle pour charger l'url donnée
  * @param {string | undefined} options.url Template de l'url. Doit inclure {x}, {y} ou {-y}, et {z}
  */
  addStamen (idLayer, options) {
    const newNames = {
      toner: 'stamen_toner',
      'toner-lite': 'stamen_toner_lite',
      terrain: 'stamen_terrain',
      watercolor: 'stamen_watercolor',
      'toner-background': 'stamen_toner_background',
      'toner-lines': 'stamen_toner_lines',
      'toner-labels': 'stamen_toner_labels',
      'terrain-background': 'stamen_terrain_background',
      'terrain-lines': 'stamen_terrain_lines',
      'terrain-labels': 'stamen_terrain_labels',
    }

    const unsuportedStamen = ['toner-hybrid']

    if (unsuportedStamen.includes(options.layer) || newNames?.[options.layer] === undefined) {
      console.error(`Le calquel Stamen de type ${options.layer} n'est pas supporté`)
      return
    }

    const stadiaMapsName = newNames[options.layer]
    console.warn(`Stamen n'est plus supporté, veuillez utiliser "stadiaMaps" avec options.layer=${stadiaMapsName}\nVeuillez visiter https://docs.stadiamaps.com/guides/migrating-from-stamen-map-tiles/`)

    options.layer = stadiaMapsName

    this.addStadiaMaps(idLayer, options)
  }

  /**
  * Créé un layer avec une source StadiaMaps
  *
  * @param {string} idLayer Id du layer
  * @param {Object} options Options pour StadiaMaps
  * @param {string} options.layer Nom du layer sur StadiaMaps
  * @param {string?} options.apiKey clé d'API
  * @param {number | undefined} options.minZoom Zoom minimum
  * @param {number | undefined} options.maxZoom Zoom maximum
  * @param {ol.TileLoadFunctionType | undefined} options.tileLoadFunction Fonction optionnelle pour charger l'url donnée
  * @param {string | undefined} options.url Template de l'url. Doit inclure {x}, {y} ou {-y}, et {z}
  * @param {Boolean?} [options.forceCanvas=false] force l'utilisation du moteur canvas plutot que webgl pour le rendu des tuiles
  */
  addStadiaMaps (idLayer, options) {
    const tileOptions = {
      source: new StadiaMaps(options),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    }

    const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)

    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
    this.allTileLoaded(idLayer)
  }

  /**
  * Créé un layer avec une source Here
  * documentation officielle : @link https://developer.here.com/documentation/raster-tile-api/api-reference.html
  * Attention erreur HTTP 429 fréquente si on a pas un plan payant
  * Si on détecte un ancien mode de connexion on utilise addHereLegacy
  * @param {string} [idLayer] Id du layer
  * @param {Object} options
  * @param {String} options.apiKey jeton d'api
  * @param {String} [options.version=3] version de l'api Here (non utilisé, pour de futur mmaj)
  * @param {String} [options.baseUrl=https://maps.hereapi.com/v3] Url d'appel au service
  * @param {('base'|'background'|'blank'|'label')} [options.resource=base] Resource (base = background + label)
  * @param {String} [options.projection=mc] projection (mc - Mercator Projection)
  * @param {('jpeg'|'png'|'png8')} [options.format=png] format d'image
  * @param {('explore.day'|'explore.night'|'explore.satellite.day'|'satellite.day')} [options.style=explore.day] Style de la carte
  * @param {Object} [options.queryParams] Paramètres optionnels d'url, voir documentation officielle, exemple: { pview:IN, mv=in123jp45, lang:en,lang2:en }
  * @param {Boolean?} [options.forceCanvas=false] force l'utilisation du moteur canvas plutot que webgl pour le rendu des tuiles
  * @returns {Object} Le fond de plan HERE
  */
  addHere (idLayer, options) {
    // on a des options legacy
    if (options.app_key || options.app_id) {
      console.warn('addHere avec des options obsolete, utilisation de addHereLegacy')
      return this.addHereLegacy(idLayer, options)
    }

    if (!options.apiKey) {
      console.warn('addHere jeton manquant, calque non ajouté')
      return
    }

    // on va construire l'url en fonction des options
    const baseUrl = options.baseUrl || 'https://maps.hereapi.com/v3'
    const resource = options.resource || 'base'
    const projection = options.projection || 'mc'
    const format = options.format || 'png'

    // par défaut on
    let queryParams = {
      apiKey: options.apiKey,
      lang: 'fr',
      lang2: 'fr',
      style: options?.style,
    }

    queryParams = {
      ...queryParams,
      ...options?.queryParams || {},
    }

    // on clean les clés sans valeur
    queryParams = pickBy(queryParams, (value) => { return value && value.toString().trim() !== '' })

    // on a forcement une queryString, au moins pour apiKey
    const queryString = '?' + new URLSearchParams(queryParams).toString()

    const url = `${baseUrl}/${resource}/${projection}/{z}/{x}/{y}/${format}` + queryString

    const tileOptions = {
      source: new XYZ({
        url,
        projection: 'EPSG:3857',
        attributions: '© 2023 HERE <a href="https://legal.here.com/en-gb/terms/here-wego-here-application-and-here-maps-service-terms" target="_blank" title="Terms of Use" style="color:#333;text-decoration: underline;">Terms of Use</a>',
        maxZoom: 20, // Zoom max disponible dans Here
      }),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    }

    const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)

    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
    this.allTileLoaded(idLayer)
  }

  /**
  * Créé un layer avec une source Here
  * Pour utiliser le service renseigner au choix : app_key ou le couple app_id/app_code (ancien mode d'authenfication)
  * @param {string} idLayer Id du layer
  * @param {Object} options   Options pour l'affichage du fond de plan
  * @param {string} options.app_key Key de l'appication pour la connexion (nouveau mode d'authentification)
  * @param {string} options.app_id Id de l'appication pour la connexion (ancien mode d'authenfication)
  * @param {string} options.app_code Code de l'application pour la connexion (ancien mode d'authenfication)
  * @param {string} options.baseUrl Url d'appel au service. Par défaut https://{1-4}.base.maps.cit.api.here.com/maptile/2.1/maptile/newest/normal.day
  * @param {Boolean?} [options.forceCanvas=false] force l'utilisation du moteur canvas plutot que webgl pour le rendu des tuiles
  */
  addHereLegacy (idLayer, options) {
    // this.addHere('dsfd', { apiKey: 'sds' })
    if (!options || !(options.app_key || (options.app_id && options.app_code))) {
      throw '<app_key> or <app_id, app_code> is required'
    }

    let connexionId = ''
    let hostname = ''
    if (options.app_key) {
      connexionId = '?apiKey=' + options.app_key
      hostname = '{1-4}.base.maps.ls.hereapi.com'
    } else {
      connexionId = '?app_id=' + options.app_id + '&app_code=' + options.app_code
      hostname = '{1-4}.base.maps.cit.api.here.com'
    }

    if (!options.baseUrl) {
      options.baseUrl = 'https://' + hostname + '/maptile/2.1/maptile/newest/normal.day'
    }

    const tileOptions = {
      source: new XYZ({
        url: options.baseUrl + '/{z}/{x}/{y}/256/png8' + connexionId,
        projection: 'EPSG:3857',
        attributions: [
          new Attribution({
            html: '© 2023 HERE <a href="https://legal.here.com/en-gb/terms/here-wego-here-application-and-here-maps-service-terms" target="_blank" title="Terms of Use" style="color:#333;text-decoration: underline;">Terms of Use</a>',
            collapsible: false,
          }),
        ],
        maxZoom: 20, // Zoom max disponible dans Here
      }),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    }

    const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)

    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
    this.allTileLoaded(idLayer)
  }

  /**
   * Créé un layer avec une source IGN WMTS
   * @param {string} idLayer Id du layer
   * @param {Object} options   Options pour l'affichage du fond de plan
   * @param {string} options.url Url de la ressource (ex: https://wxs.ign.fr/choisirgeoportail/geoportail/wmts)
   * @param {string} options.layer Nom du layer à utiliser (ex: GEOGRAPHICALGRIDSYSTEMS.MAPS)
   * @param {string} [options.style='normal'] Style d'affichage de la couche (ex: https://wxs.ign.fr/choisirgeoportail/geoportail/wmts)
   * @param {('image/jpeg'|'image/png')} [options.format='image/jpeg'] format d'image
   * @param {Boolean?} [options.forceCanvas=false] force l'utilisation du moteur canvas plutot que webgl pour le rendu des tuiles
   */
  addIGN (idLayer, options) {
    const defaultOptions = {
      matrixSet: 'PM',
      format: 'image/jpeg',
      style: 'normal',
      crossOrigin: 'anonymous',
      tileGrid: new TilegridWMTS({
        origin: [-20037508, 20037508], // topLeftCorner,
        matrixIds: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19'], // ids des TileMatrix
        resolutions: [
          156543.03392804103,
          78271.5169640205,
          39135.75848201024,
          19567.879241005125,
          9783.939620502562,
          4891.969810251281,
          2445.9849051256406,
          1222.9924525628203,
          611.4962262814101,
          305.74811314070485,
          152.87405657035254,
          76.43702828517625,
          38.218514142588134,
          19.109257071294063,
          9.554628535647034,
          4.777314267823517,
          2.3886571339117584,
          1.1943285669558792,
          0.5971642834779396,
          0.29858214173896974,
        ],
      }),
    }

    const wmtsOptions = {
      ...defaultOptions,
      ...options,
    }

    const tileOptions = {
      source: new WMTS(wmtsOptions),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    }

    const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)

    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
    this.allTileLoaded(idLayer)
  }

  /**
  * Créé un layer tuilé pour une source mapguide
  *
  * @param {string}  idLayer Id du layer
  * @param {Object}  options Options pour la création du layer
  * @param {Object}  options.MAPDEFINITION Mpadefinition de la carte. Requis
  * @param {string}  options.USERNAME Utilisateur pour la connexion mapguide. Par défaut 'Anonymous'
  * @param {string}  Options.CLIENTAGENT Client agent mapguide. Par défaut 'MapGuide Developer'
  * @param {number}  options.DPI Nombre de dpi pour de l'image
  * @param {boolean} options.useProxy Passage par un proxy mapagent-proxy.php
  * @param {Boolean?} [options.forceCanvas=false] force l'utilisation du moteur canvas plutot que webgl pour le rendu des tuiles
  */
  addMapguideTiled (idLayer, options) {
    const params = {
      USERNAME: options.USERNAME || 'Anonymous',
      OPERATION: 'GETMAPIMAGE',
      VERSION: '2.0.0',
      LOCALE: 'en',
      CLIENTAGENT: options.CLIENTAGENT || 'MapGuide%20Developer',
      CLIP: 1,
      SETDISPLAYDPI: options.DPI || 96,
      SETDISPLAYWIDTH: 256,
      SETDISPLAYHEIGHT: 256,
      SETVIEWSCALE: '{z}',
      SETVIEWCENTERX: '{x}',
      SETVIEWCENTERY: '{y}',
      FORMAT: 'PNG',
      MAPDEFINITION: options.MAPDEFINITION,
      BEHAVIOR: 2,
    }

    // TODO Migration ol6 https://github.com/openlayers/openlayers/blob/main/changelog/upgrade-notes.md#new-internal-tile-coordinates
    // ndhe: je crois que c'est fait, a tester..
    const tileOptions = {
      source: new TileImage({
        tileUrlFunction: (tileCoord, ratio, projection) => {
          // Récupère les coordonées dans le bon système de projection
          // On ajoute 0.5 au numéro de tuile car on veut leur centre
          const lon = (tileCoord[1] + 0.5) / Math.pow(2, tileCoord[0]) * 360 - 180
          const lat = Math.atan(Math.sin(Math.PI * (1 - 2 * -(tileCoord[2] + 0.5) / Math.pow(2, tileCoord[0])))) * 180 / Math.PI
          const coord = proj.fromLonLat([lon, lat], projection)
          const scale = (96 * 39.37 * (156543.03 * Math.cos(lat) / Math.pow(2, tileCoord[0])))

          const tileUrl = options.url + '?' + Object.keys(params)
            .map(key => key + '=' + params[key])
            .join('&')
            .replace('{x}', coord[0])
            .replace('{y}', coord[1])
            .replace('{z}', scale)
            // Construction et retour de l'url
          return options.useProxy
            ? options.url.replace('mapagent.fcgi', 'mapagent-proxy.php?') + encodeURIComponent(tileUrl)
            : tileUrl
        },
      }),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    }

    const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)

    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
  }

  /**
  * Créé un layer non tuilé pour une source mapguide
  *
  * @param {string} idLayer Id du layer
  * @param {Object} options Options pour la création du layer
  * @param {string} options.mapagent Url du mapAgent de mapguide
  * @param {boolean} options.useOverlay --
  * @param {number} options.meterPerUnit Nombre de mètres par unitée de mesure
  * @param {Object} options.params Paramètres de connexion à mapguide
  * @param {string} options.params.MAPNAME Nom de la map
  * @param {string} options.params.SESSION Identifiant de la session mapguide
  * @param {string} options.params.CLIENTAGENT Clientgent de mapguide
  */
  addMapguideUnTiled (idLayer, options) {
    const layer = new Image({
      source: new ImageMapGuide(options),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    })
    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
  }

  getTileGrid (tileSize) {
    const projection = proj.get(this.ctx.Map.getView().getProjection())
    const extent = projection.getExtent()
    const width = Extent.getWidth(extent)
    const height = Extent.getHeight(extent)
    const maxResolution = Math.max(width / tileSize[0], height / tileSize[1])
    const resolutions = Array.from(new Array(22), (_, index) =>
      maxResolution / Math.pow(2, index))
    return new TileGrid({ extent, resolutions, tileSize })
  }

  /**
  * Créé un layer tuilé pour une source WMS
  *
  * @param {string} idLayer Id du layer
  * @param {Object} options Options pour la création du layer
  * @param {Array<Attribution> | unefined} options.attributions Attributions
  * @param {Object<string, *>} options.params Paramètre WMS Requis. Au minimum LAYERS est requis. Par défaut STYLE vaut '', VERSION vaut 1.3.0. WIDTH, HEIGHT, BBOX, CRS et SRS seront attribués dynamiquement
  * @param {null | string | undefined} options.crossOrigin L'attribut crossOrigin pour charger les images
  * @param {number | undefined} options.gutter
  * @param {boolean | undefined} options.hidpi Utilise ol.Map#pixelRation lors du chargement des images. True par défaut
  * @param {TileGrid | unedfined} options.tileGrid Grille de tuilles
  * @param {number | undefined} options.maxZoom Zoom Maximum
  * @param {ol.proj.ProjectionLike} options.projection Projection
  * @param {ol.source.wms.ServerType | string | undefined} options.serverType Type de serveur WMS (mapserver, geoserver ou qgis) nécessaire si hidpi true
  * @param {ol.TileLoadFunctionType | undefined} options.imageLoadFunction Fonction optionnelle pour charger une image d'une URL donnée
  * @param {string | undefined} options.url Url du service WMS
  * @param {Array<string> | undefined} options.urls Urls du service WMS. A utiliser à la place de url si le service WMS supporte plusieurs urls pour les requêtes GetMap
  * @param {boolean | undefined} options.wrapX Séparer le monde horizontalement. Par défaut True
  * @param {Boolean?} [options.forceCanvas=false] force l'utilisation du moteur canvas plutot que webgl pour le rendu des tuiles
  */
  addWMSTiled (idLayer, options) {
    // Update ol 7.0, si probleme:
    // Updating parameters in ol/source/ImageWMS and ol/source/TileWMS
    // The updateParams() method is the only way to update WMS parameters. Changes made directly to the params object passed as a constructor option will have no effect.

    const tileOptions = {
      source: new TileWMS(options && {
        ...options,
        tileGrid: options.tileSize && this.getTileGrid(options.tileSize),
      }),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    }

    const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)

    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
    this.allTileLoaded(idLayer)
  }

  /**
  * Créé un layer non tuilé pour une source WMS
  *
  * @param {string} idLayer Id du layer
  * @param {Object} options Options pour la création du layer
  * @param {Array<Attributions>} options.attribution Attributions
  * @param {Object<string, *>} options.params Paramètre WMS requis. Au minimum LAYERS est requis. Par défaut STYLE vaut '', VERSION vaut 1.3.0. WIDTH, HEIGHT, BBOX, CRS et SRS seront attribués dynamiquement
  * @param {null | string | undefined} options.crossOrigin L'attribut crossOrigin pour charger les images
  * @param {boolean | undefined} options.hidpi Utilise ol.Map#pixelRation lors du chargement des images. True par défaut
  * @param {ol.proj.ProjectionLike} options.projection Projection
  * @param {ol.source.wms.ServerType | string | undefined} options.serverType Tyle de serveur WMS (mapserver, geoserver ou qgis) nécessaire si hidpi true
  * @param {ol.TileLoadFunctionType | undefined} options.imageLoadFunction Fonction optionnnelle pour charger une image d'une URL donnée
  * @param {string | undefined} options.url Urldu service WMS
  * @param {number | undefined} options.ratio Ratio. Si 1 demande les images pour la taille de la carte, 2 demande les images pour le double de WIDTH et HEIGHT de la carte. Par défaut 1.5
  * @param {Array<number> | undefined} options.resolution Si spécifié, demande uniquement pour celle-ci
  */
  addWMSUnTiled (idLayer, options) {
    // Update ol 7.0, si probleme:
    // Updating parameters in ol/source/ImageWMS and ol/source/TileWMS
    // The updateParams() method is the only way to update WMS parameters. Changes made directly to the params object passed as a constructor option will have no effect.
    const layer = new Image({
      source: new ImageWMS(options),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    })
    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
  }

  /**
  * Créé un layer tuilé pour une source web de tuile
  *
  * @param {string} idLayer Id du layer
  * @param {Object} options Options pour la création du layer
  * @param {string | undefined} options.url Schema d'url du service de tuile XYZ (ex: 'http://wgs-users.s3.amazonaws.com/cus/fonds/ems_gris/{z}/{x}/{y}.png')
  * @param {Boolean?} [options.forceCanvas=false] force l'utilisation du moteur canvas plutot que webgl pour le rendu des tuiles
  */
  addLayerXYZ (idLayer, options) {
    // Si on défini une grille custom pour le tuilé
    if (options.tileGrid) {
      options.tileGrid = new TileGrid(options.tileGrid)
    }

    const tileOptions = {
      source: new XYZ(options),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    }

    const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)

    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
  }

  /**
  * Créé un layer de fallback xyz
  *
  * @param {string} idLayer Id du layer
  * @param {Object} options Options pour la création du layer
  * @param {Boolean?} [options.forceCanvas=false] force l'utilisation du moteur canvas plutot que webgl pour le rendu des tuiles
  */
  addLayerFallbackXYZ (idLayer, options) {
    const tileOptions = {
      source: new FallbackXYZ(options),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    }

    const layer = options?.forceCanvas ? new Tile(tileOptions) : new WebGLTile(tileOptions)

    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)
  }

  /**
 * Créé un layer avec une source KML
 * @param {string} nameLayer Nom du layer
 * @param {Object} options   Options pour l'affichage du fond de plan
 * @param {string} options.url Url de la ressource (ex: https://wxs.ign.fr/choisirgeoportail/geoportail/wmts)
 * @param {ol.proj.ProjectionLike} [options.projection='EPSG:4326'] Projection des données KML, utiliser 'XY' pour le pas reprojecter les données
 */
  addKml (idLayer, options) {
    const layer = new VectorLayer({
      source: new VectorSource(),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    })

    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)

    Axios.get(options.url).then(response => {
      const format = new KML()
      const dataProjection = options?.projection || 'EPSG:4326'
      const features = format.readFeatures(response.data, dataProjection === 'XY'
        ? {}
        : {
            dataProjection: 'EPSG:4326',
            featureProjection: this.ctx.getMap().getView().getProjection(),
          })
      layer.getSource().addFeatures(features)
    })
  }

  /**
   * Créé un layer avec une source KML
   * @param {string} nameLayer Nom du layer
   * @param {Object} options   Options pour l'affichage du fond de plan
   * @param {Object} options.file
   * @param {('wfs'|'geojson'|'shp')} options.file.type
   * @param {string} [options.file.projection=EPSG:3857] projection pour interroger le service
   * @param {string} options.file.url Url de la ressource (ex: https://wxs.ign.fr/choisirgeoportail/geoportail/wmts)
   * @param {Object} options.file.wfs
   * @param {string} options.file.wfs.geometryName nom du champs geométrie du service
   * @param {Object} options.file.wfs.params paramètre du service wfs
   * @param {Object} options.file.wfs.params paramètre du service wfs
   * @param {string} options.file.wfs.params.typename Nom de la couche du service WFS à exploiter
   * @param {string} options.file.wfs.params.* autres paramètres du service WFS
   * @param {string} options.file.wfs.cqlFilter filtre cql
   * @param {Object<string , (string|Array)>} options.file.wfs.filters objet represant des filtres a générer (exemple {code_insee:[9420,8521]}). Ignoré si cqlFilter est défini
   * @param {Object} options.style
   * @param {string} options.style.url url du fichier de style
   * @param {('qml'|'mapbox'|'sld'|'ol')} options.style.type format du fichier de style
   */
  async addVector (idLayer, options) {
    const dataProjection = options.file?.projection || 'EPSG:3857'
    let featureProjection = this.ctx.getMap().getView().getProjection()

    this.viewer.getMap().on('change:view', () => {
      featureProjection = this.ctx.getMap().getView().getProjection()
    })

    const wfsSource = options.file.type === 'wfs' &&
      getWfsVectorSource({
        ...options.file.wfs,
        url: options.file.url,
        projection: dataProjection,
      })

    // Création de la source de données
    const layer = new VectorLayer({
      source: wfsSource || new VectorSource(),
      [this.commonLayer.propertiesName.ID_LAYER]: idLayer,
      title: options.title,
      zIndex: options && options.zIndex,
    })

    // Ajout de la couche à la carte
    this.ctx.commonLayer.updateCommonProperties(layer, options)
    this.ctx.commonLayer.addLayer(layer, BACKGROUND_LAYER_TYPE)

    // chargement des features en fonction du type de données
    try {
      switch (options.file.type) {
        case 'geojson': {
          Axios.get(options.file.url).then(response => {
            const features = geoJsonToFeature(response.data, dataProjection, featureProjection)
            layer.getSource().addFeatures(features)
          })
          break
        }
        case 'shp': {
          const shapefileDataParser = new ShapefileDataParser()
          shapefileDataParser.readData(options.file.url)
            .then(response => {
              const features = geoJsonToFeature(response.exampleFeatures, dataProjection, featureProjection)
              layer.getSource().addFeatures(features)
            })
          break
        }
        case 'wfs':
          // rien a faire, mais on ne veut pas planter dans default :)
          break
        default:
          throw new Error('[background-layer]addVector: options.file.type non défini')
      }
    } catch (err) {
      console.error('[background-layer]addVector:Erreur de lecture du fichier source', err)
      throw err
    }

    let readStyle = null
    let styleTxt = null
    // gestion du style : le style peut soit être fourni via une url, soit via une donnée brute dans options.style.data
    if (options.style.url) {
      try {
        const response = await fetch(options.style.url)
        if (!response.ok) {
          console.warn('[background-layer]addVector: Impossible de charger le style à partir de l\'url', options.style.url, 'status:', response.status)
          return
        }

        styleTxt = await response.text()
      } catch (err) {
        console.warn('[background-layer]addVector: Impossible de charger le style à partir de l\'url', options.style.url, 'erreur:', err)
      }
    } else if (options.style.data) {
      styleTxt = options.style.encoded ? base64ToUtf8(options.style.data) : options.style.data
      if (typeof styleTxt !== 'string') {
        styleTxt = JSON.stringify(styleTxt)
      }
    }

    if (!styleTxt) {
      console.warn('[background-layer]addVector: Impossible de charger le style, aucune donnée de style trouvée')
      return
    }
    const olStyleParser = new OpenLayersParser()
    try {
      switch (options.style.type) {
        case 'qml': {
          const qgisStyleParser = new QgisStyleParser()
          readStyle = await qgisStyleParser.readStyle(styleTxt)
          break
        }
        case 'sld': {
          const sldStyleParser = new SLDParser({
            sldVersion: '1.1.0',
            builderOptions: {
              format: true,
            },
          })
          readStyle = await sldStyleParser.readStyle(styleTxt)
          break
        }

        case 'ol':
          readStyle = await olStyleParser.readStyle(JSON.parse(styleTxt))
          break
        case 'mapbox': {
          const mapboxStyleParser = new MapboxStyleParser({
            pretty: true,
          })
          readStyle = await mapboxStyleParser.readStyle(JSON.parse(styleTxt))
          break
        }
        default:
          throw new Error('[background-layer]addVector: options.style.type non défini')
      }
    } catch (err) {
      console.error('[background-layer]addVector:Erreur de lecture du fichier style', err)
      throw err
    }

    // application du style au layer
    const { output: style } = readStyle
    const { output: olStyle } = await olStyleParser.writeStyle(style)
    layer.setStyle(olStyle)
  }

  /**
  * Ajoute un fond de plan a la carte
  *
  * @param {string} idLayer Id du layer
  * @param {('osm'|'bingmaps'|'arcgis'|'stamen'|'stadiaMaps'|'mapquest'|'here'|'mapguidetiled'|'mapguideuntiled'|'wmstiled'|'wmsuntiled'|'layerXYZ'|'wmts'|'vectorTile'|'ign'|'kml'|'vector')} type Type du fond de plan
  * @param {Object} options Options pour la création du layer (voir les options des différents type de calque dans l'api)
  * @param {string} options.sdcardId Si cette option est set, vérifi qu'on a une sdcard via cordova et basculer sur un layer XYZ utilisant la sdcard
  *
  */
  addBackgroundLayer (idLayer, type, options) {
    // todo quand on aura besoin, si le changement de fond de plan entraine un changement de projection utiliser la méthode switchView
    // puis prévenir l'appli qu'on l'a fait :)

    // si on ajoute un calque avec une option "sdcardId"
    const layerAvailableOnSdcard = options.sdcardId && window?.cordova?.file?.externalSdCardRootDirectory && SdCard.pathExist(options.sdcardId)
    if (layerAvailableOnSdcard) {
      console.log('> sdcard', { layerAvailableOnSdcard, options })
      options = {
        ...options,
        ui: {
          ...options.ui,
          rightIcon: 'kmapv-icon xs kmapv-icon-sdcard',
        },
        url: `${window.cordova.file.externalSdCardRootDirectory}/${options.sdcardId}/{z}/{x}/{y}.png`,
        type: 'layerXYZ',
      }
    }
    const optionsWithCrossOrigin = {
      ...options,
      crossOrigin: 'anonymous',
    }
    return new Promise((resolve, reject) => {
      try {
        switch (type) {
          case 'osm':
            this.addOSM(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'bingmaps':
            this.addBingMaps(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'azure':
            this.addAzureMap(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'arcgis':
            this.addArcGis(idLayer, optionsWithCrossOrigin, resolve)
            break
          case 'stamen':
            this.addStamen(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'stadiaMaps':
            this.addStadiaMaps(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'here':
            this.addHere(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'hereLegacy':
            this.addHereLegacy(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'mapguidetiled':
            this.addMapguideTiled(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'mapguideuntiled':
            this.addMapguideUnTiled(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'wmstiled':
            this.addWMSTiled(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'wmsuntiled':
            this.addWMSUnTiled(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'layerXYZ':
            this.addLayerXYZ(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'layerFallbackXYZ':
            this.addLayerFallbackXYZ(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'wmts':
            this.addWMTS(idLayer, optionsWithCrossOrigin).then(() => resolve(null)).catch((err) => reject(err))
            break
          case 'vectorTile':
            this.addVectorTile(idLayer, optionsWithCrossOrigin).then(() => resolve(null)).catch((err) => reject(err))
            break
          case 'ign':
            this.addIGN(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'kml':
            this.addKml(idLayer, optionsWithCrossOrigin)
            resolve(null)
            break
          case 'vector':
            this.addVector(idLayer, optionsWithCrossOrigin).then(() => resolve(null)).catch((err) => reject(err))
            break
          default:
            throw new Error('gs-gmapv backgroud layer type not managed for <' + idLayer + '>')
        }
      } catch (err) {
        console.error('erreur ajout background layer', err)
      }
    })
  }

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

  /**
   * Retourne la liste des types de fonds de plan géré par le service
   *
   * @return {array<string>} Liste des type de layer de fond de plan
   */
  getLayerType () {
    return LAYER_TYPES
  }

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

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

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

  /**
  * Permet de supprimer un layer
  *
  * @param {string} idLayer Id du layer
  * @fires remove:[idLayer] Lancé avant la suppression du layer
  * @fires removed:[idLayer] Lancé après la suppression du layer
  */
  removeLayer (idLayer) {
    if (this.clipInteraction && this.layerExist(idLayer)) {
      this.clipInteraction.removeLayer(this.getLayer(idLayer))
    }
    this.ctx.commonLayer.removeLayer(idLayer)
  }

  /**
  * Permet de masquer un layer de fond de plan
  *
  * @param {string} idLayer Id du layer à masquer
  */
  hideLayer (idLayer) {
    return this.ctx.commonLayer.hideLayer(idLayer)
  }

  /**
  * Permet d'afficher un layer de fond de plan
  *
  * @param {string} idLayer Id du layer à afficher
  */
  showLayer (idLayer) {
    return this.ctx.commonLayer.showLayer(idLayer)
  }

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

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

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

  /**
  * Permet de savoir si un ou tous les fonds de plans ont chargés toutes leurs tuilles
  *
  * @param  {string | undefined} idLayer Id du layer à controler. Si undefined, verifie tous les layers
  * @return {boolean} True si toutes les tuilles chargées du ou de tous les layers
  */
  allTileLoaded (idLayer) {
    // Si premier appel, ajoute la variable en instance
    if (!this.tileonload) {
      this.tileonload = { }
    }

    // Si on passe pas d'id de layer, on check tous les layers
    if (!idLayer) {
      for (const k in this.tileonload) {
        if (this.tileonload[k] !== 0) {
          return false
        }
      }
      return true
    }

    // Si la fonction à déjà été appelé pour la couche, retourne le resultat
    if (idLayer in this.tileonload) {
      return this.tileonload[idLayer] === 0
    }

    // Si le layer n'existe pas, retourne undefined
    if (!this.layerExist(idLayer)) {
      return
    }

    // Récupère le layer de fond de plan
    const l = this.getLayer(idLayer)

    // Uniquement pour les layers tuillés
    if (l.constructor !== Tile && l.constructor !== WebGLTile) {
      return
    }

    // Instancie le compteur à 0
    this.tileonload[idLayer] = 0

    // Incrémente le nombre à chaque début de chargement
    const eventStart = l.getSource().on('tileloadstart', () => {
      this.tileonload[idLayer]++
    })

    // Décrémente le nombre à chaque fin de chargement et lève allTileLoaded si le chargement de TOUS les layers sont terminés
    const eventEnd = l.getSource().on('tileloadend', () => {
      this.tileonload[idLayer]--
      // Lorsque ce layer est charger ainsi que tous les autres, lance un event
      if (this.tileonload[idLayer] === 0 && this.allTileLoaded()) {
        this.ctx.dispatchEvent('alltileloaded')
      }
    })

    // Avant la suppression du layer, supprime les events ci-dessus
    this.ctx.once('removeLayer:' + idLayer, () => {
      unByKey(eventStart)
      unByKey(eventEnd)
    })
  }

  /**
   * Permet de communiquer avec le service worker
   *
   * @param  {Object} message  Message envoyé au SW
   * @return {Promise}         Promesse résolue lors de la réponse du SW
   */
  sendToSW (message) {
    return new Promise((resolve, reject) => {
      // Si aucun service worker ne controle la page
      if (!navigator.serviceWorker.controller) {
        reject('no Service Worker controller')
      }

      const messageChannel = new MessageChannel()

      messageChannel.port1.onmessage = event => {
        event.data.error ? reject(event.data.error) : resolve(event.data)
      }

      navigator.serviceWorker.controller.postMessage(message, [messageChannel.port2])
    })
  }

  /**
   * Permet de savoir si le service worker de mise en cache de tuile est présent
   *
   * @return {Promise} Promesse retournant true si le SW est prêt
   */
  isSWReady () {
    // Si on n'obtiens pas de réponse en 200ms, on considère qu'il n'y en aura pas
    return Promise.race([
      this.sendToSW({ type: 'isReady' }),
      new Promise((resolve, reject) => setTimeout(reject, 2000, 'Timeout')),
    ])
  }

  /**
   * Permet de récupérer toutes les urls des tuiles pour un extent données sur
   * plusieurs niveaux de zoom
   *
   * @param  {number | string} idLayer Identifiant du layer
   * @param  {Extent}       extent  Extent des urls à récupérer
   * @param  {Array<number>}   zooms   Niveaux de zoom des tuiles à récupérer
   * @return {Array<string>}           Tableau des urls des tuiles
   */
  getTilesUrls (idLayer, extent, zooms) {
    const layer = this.getLayer(idLayer)
    if (!layer) {
      return false
    }

    const source = layer.getSource()

    // On s'assure que le layer à une source tuilée
    if (!source || !source.getTileUrlFunction) {
      return false
    }

    const tileUrlFunction = source.getTileUrlFunction()
    let tileGrid = source.getTileGrid()
    if (!tileGrid && source.getTileGridForProjection) {
      tileGrid = source.getTileGridForProjection(this.ctx.Map.getView().getProjection())
    }
    const projection = this.ctx.Map.getView().getProjection()
    const pixelRatio = projection.getMetersPerUnit()
    const urls = []

    // Pour chaque niveau de zoom demandé
    for (let i = 0, l = zooms.length; i < l; ++i) {
      const z = zooms[i]
      // Récupère le range des tuiles de l'extent pour le zoom
      const tileRange = tileGrid.getTileRangeForExtentAndZ(
        extent, z)

      // Créer les urls pour l'ensemble du range reçu
      for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
        for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
          urls.push(tileUrlFunction([z, x, y], pixelRatio, projection))
        }
      }
    }
    return urls
  }

  /**
   * Permet de mettre en cache des tuile d'un layer sur une extent et certains
   * niveaux de zoom. Attention, il n'y a pas de limite de nombre de tuiles
   * possible en cache. Il est recommandé de ne pas mettre plus de 1000 tuiles
   * en cache pour des layers détaillée ou 3000 pour des layers plus sobre
   * (Stamen toner-lite)
   *
   * Lors de la mise en cache, si des tuiles ne sont pas récupérer, on les
   * ignores et le traitement se poursuit
   *
   * @param  {string | number} idLayer Identifiant du layer
   * @param  {Extent}       extent  Extent des tuiles à mettre en cache
   * @param  {Array<number>}   zooms   Niveaux de zoom à mettre en cache
   * @param  {boolean}         force   Force la mise à jour si la tuile est déjà en cache
   * @return {Promise}                 Promesse qui retourne true une fois le
   * traitement terminé (mais pas forcément complet)
   */
  precacheTiles (idLayer, extent, zooms, force, onRequestDone) {
    // Appelée a chaque fin de traitement d'une tuile
    const sendProgress = typeof onRequestDone === 'function'
    // Pour pouvoir être lancé, on s'assure que le SW contrôle l'application
    return this.isSWReady().then(() => {
      // Récupère toutes les urls nécessaire
      const urls = this.getTilesUrls(idLayer, extent, zooms)
      const report = { number: 0, cached: 0, alreadyCached: 0, error: 0 }
      // Permet d'executer les requêtes par groupes
      const sendPart = (partLength) => {
        const part = urls.splice(0, partLength)
        // Lance un promise all sur le groupe, le sendToSW n'est pas sensé retourner
        // d'exception si une requête echoue donc promise all peut-être utilisé
        return Promise.all(part.map(url => {
          return this.sendToSW({ type: 'cacheRequest', url, force }).then(response => {
            if (sendProgress) {
              onRequestDone(response.status)
            }
            return response
          }).catch(() => {})
        })).then(responses => {
          // Traitement des réponses du groupe
          report.number += responses.length
          report.cached += responses.filter(e => e.status === 'cached').length
          report.alreadyCached += responses.filter(e => e.status === 'alreadyCached').length
          report.error += responses.filter(e => e.status === 'error').length
          // Une fois que le groupe entier à fini, lance un nouveau groupe
          return urls.length ? sendPart(partLength) : Promise.resolve(report)
        })
      }

      // Toutes les requêtes sont lancées par tranche de 10
      // Permet de pas surcharger le navigateur en lui en balançant 2000 d'un coup
      return sendPart(10)
    })
  }

  /**
   * Permet de vider le cache des tuiles
   *
   * @return {Promise} Promesse retournant true si le cache a été vidé
   */
  clearCacheTiles () {
    return this.isSWReady().then(() => {
      return this.sendToSW({ type: 'clearCache' })
    })
  }

  getSizeCache () {
    return this.isSWReady().then(() => {
      return this.sendToSW({ type: 'getSizeCache' })
    })
  }

  _onHighlightClick ({ coordinate }) {
    const radius = this.clipInteraction.getRadius() * this.ctx.getResolution()
    this.addHighlight(coordinate, radius)
  }

  /**
   * Permet d'utiliser un layer en tant que highlight
   * Attention ne fonctionne pas avec des calques webgl, le calque doit avoir éété ajouter avec l'options forceCanvas = true
   *
   * @param {string | number} idLayer Nom du layer
   * @param {boolean}         active  Permet de gérer l'état du highlight sur le layer
   * @param {number}          radius  Rayon du highlight en pixel. Par défaut 100
   */
  setHighlight (idLayer, active = true, radius = 100) {
    const layer = this.getLayer(idLayer)

    // Si le layer n'existe pas ou que l'on retire un layer
    // alors qu'il n'y a pas d'interaction, on ne fait rien
    if (!layer || (!this.clipInteraction && !active)) {
      return null
    }

    // Création de l'interaction si elle n'existe pas encore
    if (!this.clipInteraction) {
      this.clipInteraction = new MultiClipInteraction({
        radius,
        layers: layer,
      })
      this.ctx.Map.addInteraction(this.clipInteraction)
      const onSingleclik = event => this._onHighlightClick(event)
      let singleClickListener
      this.ctx.commonLayer.createMapMode({
        name: HIGHLIGHT_MAP_MODE,
        onactive: () => {
          this.clipInteraction.setFollowPointer(true)
          singleClickListener = this.ctx.on('singleclick', onSingleclik)
        },
        oninactive: () => {
          this.clipInteraction.setFollowPointer(false)
          unByKey(singleClickListener)
        },
      })
    } else {
      // Dans tout les cas, on retire pour éviter les doublons
      this.clipInteraction.removeLayer(layer)
      // Si active, on ajoute le layer dans l'interaction
      if (active) {
        this.clipInteraction.addLayer(layer)
      }
      this.clipInteraction.setRadius(radius)
    }
  }

  /**
   * Permet d'ajouter un highlight à une position et pour une taille
   * @param {Array<number>} coordinate Coordonées du centre du highlight
   * @param {number}        radius     Rayon du highlight en taille de la projection de la carte
   */
  addHighlight (coordinate, radius) {
    if (this.clipInteraction) {
      this.clipInteraction.addClip(coordinate, radius)
      this.ctx.dispatchEvent('addHighlight', { coordinate, radius })
    }
  }

  setFollowPointerHighlight (isFollow) {
    if (this.clipInteraction) {
      this.ctx.commonLayer.setMapMode(isFollow ? HIGHLIGHT_MAP_MODE : undefined)
    }
  }

  /**
   * Supprime tous les highlight persistés sur la carte
   */
  clearHighlight () {
    if (this.clipInteraction) {
      this.clipInteraction.clear()
      this.ctx.dispatchEvent('highlightRemoveAll')
    }
  }

  /**
   * Supprime le dernier highlight créé sur la carte
   */
  removeLastHighlight () {
    if (this.clipInteraction) {
      this.clipInteraction.removeLast()
      this.ctx.dispatchEvent('highlightRemove')
    }
  }

  getHighlightRadius () {
    return this.clipInteraction && this.clipInteraction.getRadius()
  }

  setHighlightRadius (radius) {
    return this.clipInteraction && this.clipInteraction.setRadius(radius)
  }

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

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

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

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