Source: control/ControlRecordTrace.js

import Button from 'ol-ext/control/Button'
import OlExtElement from 'ol-ext/util/element'

import { unByKey } from 'ol/Observable'
import OlControl from 'ol/control/Control'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import Feature from 'ol/Feature'
import { Fill, Stroke, Style, Circle } from 'ol/style'

import { createButton, createInteractiveHelp } from './ControlUtils'
import { convertCoordinates, createLineString, createPoint } from '../tools/mapviewer-services'

const layerName = '__kmapv_control_record_trace'
let horizontalToolbarVisility = false

const texts = {
  helpTitle: 'Enregistrement de trace',
  helptooltip: 'Afficher / Masquer l\'aide',
  help1: 'Afficher / Masquer la trace',
  help2: 'Terminer et enregistrer la trace :',
  help3: 'Annuler et stopper l\'enregistrement',
  help4: 'Masquer cette fenêtre',
  help5: 'Ne stoppe pas l\'enregistrement',
}

const styles = [

  new Style({
    stroke: new Stroke({ color: 'rgba(255,255,255,0.6)', width: 6 }),
  }),
  new Style({
    image: new Circle({
      radius: 8,
      fill: new Fill({ color: 'rgba(255,70,0,1)' }),
      stroke: new Stroke({
        color: 'rgba(255,70,0,1)',
        width: 2,
      }),
    }),
    stroke: new Stroke({
      color: 'rgba(255,70,0,1)',
      width: 2,
      lineDash: [10, 10],
    }),

  })]

/** Renvois la position du GPS si elle est disponible et différente de la dernière enregistrée */
function getPosition () {
  if (!this.viewer?.geoLocation?.isAvailable()) {
    return false
  }

  const lastPosition = this.positions.length > 0 ? this.positions[this.positions.length - 1].position : [null, null]
  const newPosition = this.viewer.geoLocation.getPositionGps()
  if (lastPosition[0] !== newPosition[0] || lastPosition[1] !== newPosition[1]) {
    return newPosition
  }

  return false
}

/** Enregistre la position */
function recordPosition (options) {
  if (this.timeout !== null) {
    clearTimeout(this.timeout)
    this.timeout = null
  }
  const position = getPosition.call(this)
  if (position) {
    this.positions.push({
      position: position.map((coord) => options?.precision ? parseFloat(parseFloat(coord).toFixed(options.precision)) : coord),
      time: options?.horodatage ? Date.now() : null,
    })

    if (this.saveKey) {
      localStorage.setItem(this.saveKey, JSON.stringify(this.positions))
    }
    if (this.showTraceOnMap) {
      showTraceOnMap.call(this)
      // affiche sur la carte
    }
  }
  const interval = options?.interval || 1
  this.timeout = setTimeout(() => { recordPosition.call(this, options) }, interval * 1000)
}

function showTraceOnMap () {
  const traceOnMap = createLineString(
    convertCoordinates(this.positions.map((pos) => pos.position), 'EPSG:4326', this.viewer.Map.getView().getProjection())
  )
  this.traceOnMap.setGeometry(traceOnMap)
  this.lastPoint.setGeometry(createPoint(traceOnMap.getLastCoordinate()))
}

function hideTraceOnMap () {
  this.traceOnMap.setGeometry(null)
  this.lastPoint.setGeometry(null)
}

function clearStore () {
  if (this.saveKey) {
    localStorage.removeItem(this.saveKey)
  }
}

/** gestion de l'enregistrement, utilise un deuxieme controle pour les boutons d'ihm'
 * @param {Object} options
 * @param {Object} options.viewer Instance de kmapviewer
 * @param {string} options.className classe de la barre de layers
 * @param {Element|string} html contenu du bouton
 * @param {string} icon icone si html non saisie (utilisé comme <i class='icon'/>)
*/
class ControlRecordTrace extends Button {
  constructor (options) {
    options = options || {}
    let self = null

    super({
      name: 'kmapv-control-record-trace',
      html: options.html ? options.html : (options.icon ? `<i class="${options.icon}"></i>` : '<i class="kmapv-icon kmapv-icon-radio-button-checked red-blink"></i>'),
      title: options.title,
      handleClick: () => {
        horizontalToolbarVisility = self.viewer.horizontalToolbar.getVisible()
        self.viewer.horizontalToolbar.setVisible(false)
        self.viewer.dataLayer.changeMapMode('select', 'select-single')
        self.controls = new ControlRecordTraceControls({ ...options, recordControl: self })
        self.viewer.Map.addControl(self.controls)
      },
    })
    self = this

    this.viewer = options.viewer || null
    this.className = (options.className || '') + ' kmapv-control-record-trace'

    horizontalToolbarVisility = this.viewer.horizontalToolbar.getVisible()

    /** enregistrement en cours */
    this.positions = []
    this.timeout = null
    this.saveKey = null

    /** affichage */
    this.showTraceOnMap = typeof options?.showTraceOnMap === 'boolean' ? options?.showTraceOnMap : false
    this.traceOnMap = new Feature()
    this.lastPoint = new Feature()

    // lorsque ce controle est retiré de la carte, on arrête d'écouter les event
    const listenerRemoveControl = this.viewer.Map.getControls().on('remove', (ev) => {
      if (ev.element === this) {
        this.layer.getSource().removeFeature(this.lastPoint)
        this.layer.getSource().removeFeature(this.traceOnMap)
        // TODO remettre la barre horizontal dans son état d'origine
        this.viewer.Map.removeControl(this.controls)
        this.viewer.horizontalToolbar.setVisible(horizontalToolbarVisility)
        hideTraceOnMap.call(this)
        unByKey(listenerRemoveControl)
      }
      if (ev.element === this.controls) {
        this.controls = null
      }
    })

    // #region Calque et interraction nécessaire a cet outil

    // sinon on le créer une seule fois dans l'appli
    if (this.viewer.commonLayer.layerExist(layerName, 'SYSTEM')) {
    // remet tjs le calque au dessus
      this.viewer.commonLayer.upLayer(layerName, 'SYSTEM')
      this.layer = this.viewer.commonLayer.getLayer(layerName, 'SYSTEM')
    } else {
    /**
    * @type {VectorLayer}
    */
      this.layer = new VectorLayer({
        [this.viewer.commonLayer.propertiesName.ID_LAYER]: layerName,
        source: new VectorSource(),
        style: styles,
        displayInLayerSwitcher: false,
        zIndex: Infinity,
      })
      this.viewer.commonLayer.addLayer(this.layer, 'SYSTEM')
    }
    this.layer.getSource().addFeature(this.lastPoint)
    this.layer.getSource().addFeature(this.traceOnMap)

  // #endregion
  }
}

/**
 * Démarre l'enregistrement
 * @param {Object} options
 * @param {Boolean} options.interval interval d'enregistrement des points
 * @param {Number} options.precision Nombre de chiffres après la virgule
 * @param {Number} options.horodatage interval d'enregistrement des points
 * @param {String} [options.saveKey] interval d'enregistrement des points
 */
ControlRecordTrace.prototype.startRecording = function (options) {
  /** Enregistement de la trace en localstorage afin de reprendre si on recharge la page */
  this.saveKey = options?.saveKey || null
  // vérifi si on avait déjà des positions enregistrées afin de les restaurer
  if (this.saveKey) {
    const positions = localStorage.getItem(this.saveKey)
    this.positions = positions ? JSON.parse(positions) : []
    if (this.showTraceOnMap) {
      showTraceOnMap.call(this)
    }
  }
  // démarre l'intervale de recording
  recordPosition.call(this, options)
}

/** Pause, on masque les outil on stop le timer.
 * TODO changer l'icone / plus compliqué que ca, il faudra faire un "multiposition" */
/* ControlRecordTrace.prototype.pauseRecording = function () {
  clearTimeout(this.timeout)
  this.viewer.Map.removeControl(this.controls)
} */

/** Annulation, on retire tout */
ControlRecordTrace.prototype.cancelRecording = function () {
  clearTimeout(this.timeout)
  this.viewer.dispatchEvent('record-trace:cancel')
  clearStore.call(this)
  this.viewer.Map.removeControl(this.controls)
  this.viewer.Map.removeControl(this)
}

/**
 * Validation, lance l'event, renvoit le résultat et retirer le controle
 * @param {Object} options
 * @param {Boolean} options.silent ne lance pas l'event
 * @returns {object} {positions, feature}
 */
ControlRecordTrace.prototype.validateRecording = function (options) {
  // renvoit les positions et la geometry des positions au format de la cartez
  const rtnValue = {
    positions: this.positions,
    feature: new Feature({
      geometry: createLineString(
        convertCoordinates(
          this.positions.map((pos) => pos.position), 'EPSG:4326',
          this.viewer.Map.getView().getProjection())
      ),
    }),
  }
  clearTimeout(this.timeout)

  if (!options?.silent) {
    this.viewer.dispatchEvent('record-trace:validate', rtnValue)
  }

  clearStore.call(this)

  this.viewer.Map.removeControl(this.controls)
  this.viewer.Map.removeControl(this)
  return rtnValue
}

/** On affiche / masque la trace sur la carte */
ControlRecordTrace.prototype.toggleTraceOnMap = function (visible = null) {
  if (visible !== null) {
    this.showTraceOnMap = visible
  } else {
    this.showTraceOnMap = !this.showTraceOnMap
  }

  if (this.showTraceOnMap) {
    showTraceOnMap.call(this)
  } else {
    hideTraceOnMap.call(this)
  }
}

/** Controles utilisateurs pour piloter l'enregistrement */
class ControlRecordTraceControls extends OlControl {
  constructor (options) {
    options = options || {}

    const className = options.className !== undefined ? options.className : ''

    const classNames = (className || '') + ' kmapv-bottom-tools kmap-record-trace-controls' + (options.target ? '' : ' ol-unselectable ol-control')
    const element = OlExtElement.create('DIV', {
      className: classNames,
    })

    super({
      element,
      target: options.target,
    })
    this.buildIhm(element)

    this.viewer = options.viewer || null

    this.recordControl = options.recordControl || null

  /* OlControl.call(this, {
    element: element,
    target: options.target,
  }) */
  }
}

ControlRecordTraceControls.prototype.close = function () {
  this.viewer.Map.removeControl(this)
  this.viewer.horizontalToolbar.setVisible(horizontalToolbarVisility)
}

ControlRecordTraceControls.prototype.buildIhm = function (element) {
  const visibleHtml = '<i class="kmapv-icon kmapv-icon-eye"></i><span>Visible</span>'
  const notVisibleHtml = '<i class="kmapv-icon kmapv-icon-eye-off"></i><span>Masqué</span>'
  const validHtml = '<i class="kmapv-icon kmapv-icon-valid"></i><span>Terminer</span>'
  const cancelHtml = '<i class="kmapv-icon kmapv-icon-delete"></i><span>Annuler</span>'
  const closeHtml = '<i class="kmapv-icon kmapv-icon-cancel"></i><span>Fermer</span>'

  // #region aide interractive
  const helpTexts = [
  `${visibleHtml} ${texts.help1}`,
  `${validHtml} ${texts.help2}`,
  `${cancelHtml} ${texts.help3}`,
  `${closeHtml} ${texts.help4}`,
  `<div class="subtitle">${texts.help5}</div>`,
  ]

  const { helpTitle, helpDiv } = createInteractiveHelp({
    title: texts.helpTitle,
    buttonTitle: texts.helptooltip,
    helpTexts,
    joinText: '<br/>',
  })

  element.appendChild(helpTitle)
  element.appendChild(helpDiv)
  // #endregion

  // #region boutons
  const btnDiv = OlExtElement.create('DIV', {
    className: 'tools',
  })
  this.showHideTraceBtn = createButton(notVisibleHtml, () => {
    this.recordControl.toggleTraceOnMap()
    this.showHideTraceBtn.innerHTML = this.recordControl.showTraceOnMap ? visibleHtml : notVisibleHtml
  })
  btnDiv.appendChild(this.showHideTraceBtn)
  btnDiv.appendChild(createButton(validHtml, () => { this.recordControl.validateRecording() }))
  btnDiv.appendChild(createButton(cancelHtml, () => { this.recordControl.cancelRecording() }))
  btnDiv.appendChild(createButton(closeHtml, () => { this.close() }))

  element.appendChild(btnDiv)
  // #endregion
}

export default ControlRecordTrace