Source: drawing/drawing.js

import Feature from 'ol/Feature'
import Point from 'ol/geom/Point'
import GeometryCollection from 'ol/geom/GeometryCollection'

import Circle from 'ol/style/Circle'
import Fill from 'ol/style/Fill'
import Stroke from 'ol/style/Stroke'

import { getVectorContext } from 'ol/render'

/**
 * Module de gestion d'une couche de dessin permettant les annotations
 * @module drawing
 */
const libNamespace = 'drawing'

/**
 * Exception lancé si demande de dessin avec la fin d'un autre
 */
class MultiDrawingException extends Error {
  constructor () {
    super()
    this.message = 'An other drawing is not finish'
    this.name = 'MultiDrawingException'
  }
}

/**
 * Service de gestion d'une couche de dessin permettant les annotations
 */
class Drawing {
  constructor (viewer, options) {
    viewer.DRAWING_LOADED = true

    this.ctx = viewer
    this.drawingStarted = false

    Object.assign(this, options)
  }

  /** Le style du cercle présent sous le pointeur. */
  static pointStyle = new Circle({
    radius: 5,
    fill: new Fill({
      color: [0, 153, 255, 1],
      width: 3,
    }),
    stroke: new Stroke({
      color: [255, 255, 255, 1],
      width: 2,
    }),
  })

  /**
   * Traitement commun au début d'un dessin
   */
  validStartDraw () {
    if (this.drawingStarted) {
      throw new MultiDrawingException()
    }

    // Désactive les ToolTip et la sélection
    if (this.ctx.DATALAYER_LOADED) {
      this.ctx.dataLayer.changeMapMode('modify')
    }
    if (this.ctx.TOOLTIPS_LOADED) {
      this.ctx.tooltips.disabledTooltip()
    }
    this.drawingStarted = true
  }

  /**
   * Traitement commun à la fin d'un dessin
   */
  validEndDraw () {
    // Réactive les ToolTip et la sélection
    if (this.ctx.DATALAYER_LOADED) {
      this.ctx.dataLayer.changeMapMode('select')
    }
    if (this.ctx.TOOLTIPS_LOADED) {
      this.ctx.tooltips.enabledTooltip()
    }
    this.drawingStarted = false
    // Vide la fonction d'annulation
    this.cancelDraw = () => {}
  };

  /**
   * Permet de dessiner un point et de récupérer la feature
   * @param  {string | number}    id       Identifiant de la feature à créer
   * @param  {Object<string,*>}   param    Propriétés à ajouter à la feature
   * @param  {Function}           callback Fonction de callback avec en paramètre la feature (ou false si annulé)
   * @param  {Object}             context  Context d'appel du callback
   */
  drawPoint (id, param, callback, context) {
    this.validStartDraw()

    let point = null

    // Permet de créer le point et de générer le rendu de la carte
    const displaySnap = coordinate => {
      if (point === null) {
        point = new Point(coordinate)
      } else {
        point.setCoordinates(coordinate)
      }

      this.ctx.Map.render()
    }

    // A chaque déplacement du curseur, calcule la position du point
    const onPointerMove = ev => {
      if (ev.dragging) {
        return
      }
      displaySnap(this.ctx.Map.getEventCoordinate(ev.originalEvent))
    }

    // Avant la génération de la carte par OpenLayers
    const onPostCompose = ev => {
      // Si le point existe, l'ajoute sur une couche temporaire
      const vectorContext = getVectorContext(ev)
      if (point !== null && vectorContext) {
        vectorContext.setImageStyle(Drawing.pointStyle)
        vectorContext.drawPoint(point)
      }
    }

    // Lors du clique de confirmation de position
    const onValidDrawingClick = () => {
      this.validEndDraw()

      this.ctx.Map.un('pointermove', onPointerMove)
      this.ctx.Map.un('postrender', onPostCompose)
      this.ctx.Map.un('click', onValidDrawingClick)

      if (typeof callback === 'function') {
        param = param || {}
        param.geometry = point
        param.id = id // Pour compatibilité. Devrait être supprimé sinon.

        const feature = new Feature(param)
        feature.setId(id)
        callback.call(context || this, feature)
      }
    }

    // Permet d'annuler le dessin
    this.cancelDraw = () => {
      this.validEndDraw()

      point = null
      this.ctx.Map.renderSync()

      this.ctx.Map.un('pointermove', onPointerMove)
      this.ctx.Map.un('postrender', onPostCompose)
      this.ctx.Map.un('click', onValidDrawingClick)

      if (typeof callback === 'function') {
        callback.call(context || this, false)
      }
    }

    // Lance les fonctions précédente en fonction de l'événement levé
    this.ctx.Map.on('pointermove', onPointerMove)
    this.ctx.Map.on('postrender', onPostCompose)
    this.ctx.Map.on('click', onValidDrawingClick)
  }

  /**
   * Permet de dessiner un point avec accroche sur objet et de récupérer la feature
   * @param  {string | number}   id              Identifiant de la feature à créer
   * @param  {Object}   param           Propriétés à ajouter à la feature
   * @param  {Array<ol.Feature>}   closestFeatures Liste des features sur lesquelles peut s'accrocher le point
   * @param  {number}   hangsLength     Distance en pixel pour l'accroche du point
   * @param  {Function} callback        Fonction de callback avec en paramètre la feature (ou false si annulé)
   * @param  {Object}   context         Context d'appel de la fonction
   */
  drawPointWithClosest (id, param, closestFeatures, hangsLength, callback, context) {
    this.validStartDraw()

    let point = null

    // Création d'une collection de geometries pour l'accroche
    let closestGeometries = []
    closestFeatures.forEach(feature => {
      closestGeometries.push(feature.getGeometry())
    })
    closestGeometries = new GeometryCollection(closestGeometries)

    // Permet d'afficher le point et de vérifier l'accroche
    const displaySnap = coordinate => {
      let closestPoint = coordinate

      if (closestFeatures.length !== 0) {
        // Recherche le point le plus proche
        closestPoint = closestGeometries.getClosestPoint(coordinate)

        // Récupère les coordonnées du point et du curseur en pixel
        const closestPointPx = this.ctx.Map.getPixelFromCoordinate(closestPoint)
        const coordinatePx = this.ctx.Map.getPixelFromCoordinate(coordinate)

        // Calcule la distance entre les deux points
        const closestLength = Math.abs((closestPointPx[0] + closestPointPx[1]) - (coordinatePx[0] + coordinatePx[1]))

        // Si superieur à hangsLength, pas d'accroche
        if (closestLength > hangsLength) {
          closestPoint = coordinate
        }
      }

      if (point === null) {
        point = new Point(coordinate)
      } else {
        point.setCoordinates(coordinate)
      }

      this.ctx.Map.render()
    }

    // A chaque déplacement du pointeur
    const onPointerMove = ev => {
      if (ev.dragging) {
        return
      }
      displaySnap(this.ctx.Map.getEventCoordinate(ev.originalEvent))
    }

    // Avant le calcule du rendu de la carte
    const onPostCompose = ev => {
      const vectorContext = getVectorContext(ev)
      if (point !== null && vectorContext) {
        vectorContext.setImageStyle(Drawing.pointStyle)
        vectorContext.drawPoint(point)
      }
    }

    // Au clique de confirmation du point
    const onValidDrawingClick = () => {
      this.validEndDraw(this)

      this.ctx.Map.un('pointermove', onPointerMove)
      this.ctx.Map.un('postrender', onPostCompose)
      this.ctx.Map.un('click', onValidDrawingClick)

      if (typeof callback === 'function') {
        param = param || {}
        param.geometry = point
        param.id = id // Pour compatibilité. Devrait être supprimé sinon.

        const feature = new Feature(param)
        feature.setId(id)

        callback.call(context || this, feature)
      }
    }

    // Permet d'annuler le dessin
    this.cancelDraw = () => {
      this.validEndDraw(this)

      point = null
      this.ctx.Map.renderSync()

      this.ctx.Map.un('pointermove', onPointerMove)
      this.ctx.Map.un('postrender', onPostCompose)
      this.ctx.Map.un('click', onValidDrawingClick)

      if (typeof callback === 'function') {
        callback.call(context || this, false)
      }
    }

    // Lance les fonctions précédente en fonction de l'événement levé
    this.ctx.Map.on('pointermove', onPointerMove)
    this.ctx.Map.on('postrender', onPostCompose)
    this.ctx.Map.on('click', onValidDrawingClick)
  }

  /**
   * Permet d'annuler la création d'un dessin. Lance le callback avec false
   */
  cancelDraw () {
    // Générée dans les fonctions de dessin
  }
}

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

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