Source: control/ControlModifyTools.js

import { unByKey } from 'ol/Observable'

import OlControl from 'ol/control/Control'

import OlExtElement from 'ol-ext/util/element'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import ControlCreateTools from './ControlCreateTools'
import GeometryCollection from 'ol/geom/GeometryCollection'
import MultiLineString from 'ol/geom/MultiLineString'
import MultiPoint from 'ol/geom/MultiPoint'
import MultiPolygon from 'ol/geom/MultiPolygon'
import Point from 'ol/geom/Point'
import Feature from 'ol/Feature'
import Select from 'ol/interaction/Select'
import Transform from 'ol-ext/interaction/Transform'
import Modify from 'ol/interaction/Modify'
import { toContext } from 'ol/render'
import GeoJSON from 'ol/format/GeoJSON'
import { getCenter } from 'ol/extent'

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

import { always, never, singleClick } from 'ol/events/condition'

import { resizeAndMoveFeature } from '../tools/services/geometry-utils'
import { createInteractiveHelp } from './ControlUtils'

/** Controle interaction pour la modification */

const white = [255, 255, 255, 0.5]
const sketchColor = [106, 106, 106, 1]
const selectColor = [0, 153, 255, 1]
const deleteColor = [204, 0, 0, 1]
const width = 3

const fill = new Fill({ color: white })
const stroke = new Stroke({ color: sketchColor })
const templateStyle = new Style({
  fill,
  stroke,
  image: new Circle({
    radius: 10,
    fill,
    stroke,
  }),
})

const editStyle = [
  // polygone
  new Style({
    fill: new Fill({
      color: white,
    }),
  }),
  // lignes
  new Style({
    stroke: new Stroke({
      color: white,
      width: width + 2,
    }),
  }),
  new Style({
    stroke: new Stroke({
      color: sketchColor,
      width,
      lineDash: [10, 5],
    }),
  }),
  // points de construction
  new Style({
    image: new Circle({
      radius: width * 2,
      fill: new Fill({
        color: white,
      }),
      stroke: new Stroke({
        color: sketchColor,
        width,
        lineDash: [5, 2],
      }),
    }),
  }),
]

const selectStyle = [
  // polygone
  new Style({
    fill: new Fill({
      color: white,
    }),
  }),
  // lignes
  new Style({
    stroke: new Stroke({
      color: white,
      width: width + 2,
    }),
  }),
  new Style({
    stroke: new Stroke({
      color: selectColor,
      width,
      lineDash: [10, 5],
    }),
  }),
  // points de construction
  new Style({
    image: new Circle({
      radius: width * 2,
      fill: new Fill({
        color: white,
      }),
      stroke: new Stroke({
        color: selectColor,
        width,
        lineDash: [5, 2],
      }),
    }),
  }),
]

const deleteStyle = [
  // points de construction
  new Style({
    image: new Circle({
      radius: width * 2,
      fill: new Fill({
        color: [240, 0, 0, 1],
      }),
      stroke: new Stroke({
        color: deleteColor,
        width,
      }),
    }),
  }),
]

/**
 * Outil de modifications de geométrie
 * @param {Object} options
 * @param {Object} options.viewer Instance de kmapviewer
 * @param {string} options.className classe de la barre de creation
 * @param {Feature} options.feature Feature a modifier
 */
class ControModifyTools extends OlControl {
  constructor (options) {
  // propose une ihm avec navigation et des interactions afin d'aider l'utilisateur a saisir
  // menumain : interface de creation de point, ligne, template, validation...
  // menuEdit : interface proposé lorsque l'utilisateur sélectionne une sous-géométrie (ou géom simple) : mise a l'echelle, rotate..
  // menuTemplate : interfacer proposé lorsque l'utilisateur veux ajouter depuis un template de geom

    if (!options) options = {}

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

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

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

    if (!options.feature) {
      console.error('[ControlModifyTools] options.feature non présent')
      return
    }

    if (!options.viewer) {
      console.error('[ControlModifyTools] options.viewer non présent')
      return
    }

    this.viewer = options.viewer
    this.geometryType = options.geometryType || options.feature.getGeometry().getType() || 'Point'
    this.digitalizeOptions = options.digitalizeOptions || {}
    this.toolId = options.toolId || 'modify-geometry-tool'
    this.createTemplates = options.createTemplates || {}

    this._hideToolbox = options.hideToolbox || false
    this._autoValidate = options.autoValidate || false

    // restrictions d'outils disponibles:
    this.modifyTools = options.modifyTools || []

    // feature en cours d'édition
    /** @type {ol/Feature} */
    this.feature = options.feature
    this.initialFeature = options.feature.clone()
    // méthodes et mapmode (interactions)
    this.methods = options.methods || {}
    this.mapModes = options.mapModes || {}

    const geomTitle = {
      Point: 'le point',
      LineString: 'la ligne',
      Polygon: 'la forme',
      GeometryCollection: 'la multigéométrie',
      MultiPoint: 'les points',
      MultiLineString: 'les lignes',
      MultiPolygon: 'les polygones',

    }[this.geometryType]

    this.mode = this.feature.getGeometry() ? 'MODIFIER' : 'CREER'
    const title = options.title || `${this.mode === 'CREER' ? 'Créer' : 'Modifier'} ${geomTitle}`

    // boutons du menu principal
    const addPointHtml = '<i class="kmapv-icon kmapv-icon-map-marker" title="Ajouter un point"></i><span>Point</span>'
    const addLineHtml = '<i class="kmapv-icon kmapv-icon-vector-polyline kmapv-icon-miniplus" title="Ajouter une ligne"></i><span>Ligne</span>'
    const addPolygonHtml = '<i class="kmapv-icon kmapv-icon-vector-polygon kmapv-icon-miniplus" title="Ajouter une forme"></i><span>Forme</span>'
    const addTemplateHtml = '<i class="kmapv-icon kmapv-icon-package-variant" title="Ajouter une forme prédéfinie"></i><span>Forme pred.</span>'
    const selectAllHtml = '<i class="kmapv-icon kmapv-icon-vector-selection" title="Sélectionner tous les éléments"></i><span>Sel. tout</span>'
    const validHtml = '<i class="kmapv-icon kmapv-icon-valid"></i><span>Valider</span>'
    const cancelHtml = '<i class="kmapv-icon kmapv-icon-cancel"></i><span>Annuler</span>'

    // boutons du menu d'edition
    const moveHtml = '<i class="kmapv-icon kmapv-icon-translate" title="Déplacement"></i><span>Dépl.</span>'
    const rotateHtml = '<i class="kmapv-icon kmapv-icon-rotate" title="Rotation"></i><span>Rot.</span>'
    const scaleHtml = '<i class="kmapv-icon kmapv-icon-scale" title="Mise à l\'échelle"></i><span>Echelle</span>'
    const modifyHtml = '<i class="kmapv-icon kmapv-icon-vector-square-plus" title="Déplacer ou ajouter un sommet"></i><span>Mod.</span>'
    const removeVertexHtml = '<i class="kmapv-icon kmapv-icon-vector-square-minus" title="Retirer un sommet"></i><span>- Pt</span>'
    const removeHtml = '<i class="kmapv-icon kmapv-icon-delete" title="Supprimer l\'élement sélectionné"></i><span>Suppr.</span>'
    const clearSelection = '<i class="kmapv-icon kmapv-icon-clear-selection" title="Désélectionner"></i><span>Désélectionner</span>'

    // #region boutons

    // boutons du menu principal (utile pour les multigeometries)
    this.menuMain = OlExtElement.create('DIV', {
      className: 'tools',
    })

    this.addPointBtn = addButton.call(this, addPointHtml, () => { addGeometry.call(this, { ...options, geometryType: 'Point' }) })
    this.addLineBtn = addButton.call(this, addLineHtml, () => { addGeometry.call(this, { ...options, geometryType: 'LineString' }) })
    this.addPolygonBtn = addButton.call(this, addPolygonHtml, () => { addGeometry.call(this, { ...options, geometryType: 'Polygon' }) })

    this.addTemplateBtn = addButton.call(this, addTemplateHtml, () => {
      onClickClearSelection.call(this)
      this.showMenu('template')
    })

    this.selectAllBtn = addButton.call(this, selectAllHtml, onClickSelectAll.bind(this))
    this.validBtn = addButton.call(this, validHtml, onClickValid.bind(this))
    this.validBtn.style.display = 'none'
    this.cancelBtn = addButton.call(this, cancelHtml, onClickCancel.bind(this))

    // on conditionne les boutons
    if (['GeometryCollection', 'MultiPoint'].includes(this.geometryType)) { this.menuMain.appendChild(this.addPointBtn) }
    if (['GeometryCollection', 'MultiLineString'].includes(this.geometryType)) { this.menuMain.appendChild(this.addLineBtn) }
    if (['GeometryCollection', 'MultiPolygon'].includes(this.geometryType)) { this.menuMain.appendChild(this.addPolygonBtn) }

    if (this.createTemplates?.features && this.createTemplates.features.length > 0) {
      this.menuMain.appendChild(this.addTemplateBtn)
    }
    this.menuMain.appendChild(this.selectAllBtn)
    // if (['GeometryCollection', 'MultiPoint', 'MultiPolygon', 'MultiLineString'].includes(this.geometryType)) { this.menuMain.appendChild(this.selectAllBtn) }

    if (this.isMultiGeometry) {
      // dans le cas de géométries simple ce bouton sera dispo directement sur le menu d'edit
      this.menuMain.appendChild(this.validBtn)
      this.menuMain.appendChild(this.cancelBtn)
    }
    // boutons menu d'édition d'élement sélectionné

    this.menuEdit = OlExtElement.create('DIV', {
      className: 'tools',
      style: { display: 'none' },
    })

    this.moveBtn = addButton.call(this, moveHtml, onClickTransform.bind(this, 'move'))
    this.rotateBtn = addButton.call(this, rotateHtml, onClickTransform.bind(this, 'rotate'))
    this.scaleBtn = addButton.call(this, scaleHtml, onClickTransform.bind(this, 'scale'))
    this.modifyVertexBtn = addButton.call(this, modifyHtml, onClickTransform.bind(this, 'modifyVertex'))
    this.removeVertexBtn = addButton.call(this, removeVertexHtml, onClickTransform.bind(this, 'removeVertex'))
    this.removeBtn = addButton.call(this, removeHtml, onClickRemove.bind(this))
    this.clearSelectionBtn = addButton.call(this, clearSelection, onClickClearSelection.bind(this))

    this.menuEdit.appendChild(this.moveBtn)
    this.menuEdit.appendChild(this.rotateBtn)
    this.menuEdit.appendChild(this.scaleBtn)
    this.menuEdit.appendChild(this.modifyVertexBtn)
    this.menuEdit.appendChild(this.removeVertexBtn)

    // les bouton de suppression unitaire ou déselection ne sont utile que pour les multigéométrie
    if (this.isMultiGeometry) {
      this.menuEdit.appendChild(this.removeBtn)
      this.menuEdit.appendChild(this.clearSelectionBtn)
    } else {
      // dans le cas de géométries simple ce bouton sera dispo directement sur le menu d'edit
      this.menuEdit.appendChild(this.validBtn)
      this.menuEdit.appendChild(this.cancelBtn)
    }

    // header du menu template
    this.headerTemplateMenu = OlExtElement.create('DIV', {
      className: 'header',
      style: { display: 'none' },
      html: '<div>Forme prédéfinie</div>',
    })
    const closeTemplateMenuButton = OlExtElement.create('I', {
      className: 'kmapv-icon xs kmapv-icon-cancel',
    })
    closeTemplateMenuButton.setAttribute('title', 'Fermer ce menu')
    this.headerTemplateMenu.appendChild(closeTemplateMenuButton)

    closeTemplateMenuButton.addEventListener('click', () => {
      this.showMenu('main')
    })

    this.menuTemplate = OlExtElement.create('DIV', {
      className: 'tools templates',
      style: { display: 'none' },
    })

    addTemplates.call(this, this.menuTemplate, this.createTemplates)
    // #endregion

    // #region aide interractive
    const mainHelpText = ['']

    // on conditionne les boutons
    if (['GeometryCollection', 'Point', 'MultiPoint'].includes(this.geometryType)) { mainHelpText.push(`${addPointHtml} Ajouter un point au dessin<br/>`) }
    if (['GeometryCollection', 'LineString', 'MultiLineString'].includes(this.geometryType)) { mainHelpText.push(`${addLineHtml} Ajouter une ligne au dessin<br/>`) }
    if (['GeometryCollection', 'Polygon', 'MultiPolygon'].includes(this.geometryType)) { mainHelpText.push(`${addPolygonHtml} Ajouter une forme au dessin<br/>`) }

    if (this.createTemplates?.features && this.createTemplates.features.length > 0) {
      mainHelpText.push(`${addTemplateHtml} Ajouter une forme prédéfinie au dessin<br/>`)
    }

    if (this.isMultiGeometry) { mainHelpText.push(`${selectAllHtml} Sélectionner tous les éléments du dessin<br/>`) }

    mainHelpText.push(`${validHtml} Valider les modifications<br/>`)
    mainHelpText.push(`${cancelHtml} Annuler`)

    const { helpTitle, helpDiv } = createInteractiveHelp({
      title,
      helpTexts: mainHelpText,
    })

    helpTitle.addEventListener('click', () => {
      this.modifyHelpDiv.classList.toggle('kmapv-icon-expanded')
    })

    this.helpTitle = helpTitle
    this.mainHelpDiv = helpDiv

    function updateModifyHelp () {
      const visibleTools = this.visibleTools
      const modifyHelpText = ['']
      // modifyHelpText.push('Opérations sur l\'élément sélectionné<br/>')
      if (visibleTools.find(({ tool }) => tool === 'move')) {
        modifyHelpText.push(`${moveHtml} Déplacer<br/>`)
      }
      if (visibleTools.find(({ tool }) => tool === 'rotate')) {
        modifyHelpText.push(`${rotateHtml} Rotation autour du centre<br/>`)
      }
      if (visibleTools.find(({ tool }) => tool === 'scaale')) {
        modifyHelpText.push(`${scaleHtml} Aggrandir ou rétrécir l'élement<br/>`)
      }
      if (visibleTools.find(({ tool }) => tool === 'modifyVertex')) {
        modifyHelpText.push(`${modifyHtml} Déplacer ou ajouter un sommet<br/>`)
      }
      if (visibleTools.find(({ tool }) => tool === 'removeVertex')) {
        modifyHelpText.push(`${removeVertexHtml} Cliquer sur un sommet pour le retirer<br/>`)
      }

      if (this.isMultiGeometry) {
        modifyHelpText.push(`${removeHtml} Supprimer la geometrie sélectionnée<br/>`)
        modifyHelpText.push(`${clearSelection} Désélectionner<br/>`)
      } else {
        modifyHelpText.push(`${validHtml} Valider les modifications<br/>`)
        modifyHelpText.push(`${cancelHtml} Annuler`)
      }

      this.modifyHelpDiv.innerHTML = `<div class="help">${modifyHelpText.join('')}</div>`
    }
    this.modifyHelpDiv = OlExtElement.create('DIV', {
      className: 'kmapv-collapsible',
      style: { display: 'none' },
    })

    // #endregion

    // #region controle openlayer, on ajoute les divs de contenu dans l'ordre

    element.appendChild(this.helpTitle)
    element.appendChild(this.headerTemplateMenu)
    element.appendChild(this.mainHelpDiv)
    element.appendChild(this.modifyHelpDiv)

    element.appendChild(this.menuMain)
    element.appendChild(this.menuEdit)
    element.appendChild(this.menuTemplate)
    // #endregion

    // #region Calque et interraction nécessaire a cet outil
    /**
     * @type {VectorLayer}
     */
    this.modifyFeatureLayer = new VectorLayer({
      [this.viewer.commonLayer.propertiesName.ID_LAYER]: '__MODIFY_FEATURE_LAYER',
      zIndex: 10000,
      source: new VectorSource(),
      style: editStyle,
      displayInLayerSwitcher: false,
    })
    this.viewer.commonLayer.addLayer(this.modifyFeatureLayer, 'SYSTEM')
    this.viewer.getFeatureSnapper().addSource({ groups: [{ name: 'KMAPV', edge: true, vertex: true }], source: this.modifyFeatureLayer.getSource() })
    const events = []

    // affiche le bouton valider seulement si on a des features sur le calque de modif
    // (a voir si on voudrait une option de géométrie nullable...)
    events.push(this.modifyFeatureLayer.getSource().on('addfeature', () => {
      this.validBtn.style.display = null
      // Si on est sur un type de geométrie unitaire on masque les bouton d'ajout dés qu'on a une seule feature
      // pas besoin de gérer le type de geometrie en profondeur (si on est sur "point" le bouton this.addpoint n'a pas été ajouté au dom)
      if (!this.isMultiGeometry) {
      // Si on est en mode creation, on valide direct
        if (this.mode === 'CREER') {
          onClickValid.call(this)
        }

        this.addPointBtn.style.display = 'none'
        this.addLineBtn.style.display = 'none'
        this.addPolygonBtn.style.display = 'none'
      }
    }))

    events.push(this.modifyFeatureLayer.getSource().on('removefeature', () => {
      const featureCount = this.modifyFeatureLayer.getSource().getFeatures().length
      this.validBtn.style.display = featureCount > 0 ? null : 'none'

      // Si on est sur un type de geométrie unitaire on montre les bouton d'ajout dés qu'on a plus de feature
      if (!this.isMultiGeometry && featureCount === 0) {
        this.addPointBtn.style.display = null
        this.addLineBtn.style.display = null
        this.addPolygonBtn.style.display = null
      }
    }))

    // Interaction "select" sur le calque de la feature
    // Permet d'afficher ou non le bouton "removeBtn" et de gérer quoi supprimer..
    this.selectInteraction = new Select({
      layers: [this.modifyFeatureLayer],
      hitTolerance: this.viewer.hitTolerance,
      style: selectStyle,
      condition: (mapEvt) => {
        return !this.isMultiGeometry ? never() : singleClick(mapEvt)
      },
      toggleCondition: always,
    })

    this.transformInteraction = new Transform({
      selection: false,
      enableRotatedTransform: false,
      hitTolerance: this.viewer.hitTolerance,
      translateFeature: false,
      scale: false,
      rotate: false,
      keepAspectRatio: always,
      keepRectangle: false,
      translate: false,
      stretch: false,
    })

    this.modifyFeatureInteraction = new Modify({
      features: this.selectInteraction.getFeatures(),
    // condition: always,
    })

    this.removeVertexInteraction = new Modify({
      features: this.selectInteraction.getFeatures(),
      deleteCondition: always,
      insertVertexCondition: never,
      // condition: never,
      style: deleteStyle,
    })

    this.modifyFeatureInteraction.setActive(false)
    this.transformInteraction.setActive(false)
    this.removeVertexInteraction.setActive(false)

    this.viewer.Map.addInteraction(this.selectInteraction)
    this.viewer.Map.addInteraction(this.transformInteraction)
    this.viewer.Map.addInteraction(this.modifyFeatureInteraction)
    this.viewer.Map.addInteraction(this.removeVertexInteraction)

    events.push(this.transformInteraction.on(['rotateend', 'translateend', 'scaleend'], (evt) => {
      if (this.autoValidate) {
        const element = evt.target.getMap().getTargetElement()
        onClickValid.call(this)
        // transform ne remet pas le curseur par défaut quand on termine la modification:
        element.style.cursor = 'default'
      }
    }))

    events.push(this.modifyFeatureInteraction.on('modifyend', () => {
      if (this.autoValidate) {
        onClickValid.call(this)
      }
    }))
    events.push(this.removeVertexInteraction.on('modifyend', () => {
      if (this.autoValidate) {
        onClickValid.call(this)
      }
    }))
    events.push(this.selectInteraction.on('select', (evt) => {
      // bouton visible suivant l'élement sélectionné
      const nbSelect = this.selectInteraction.getFeatures().getLength()
      updateModifyHelp.call(this)
      // si il y a un élemet sélectionné, on montre le menu Edition, sinon le menu principal
      this.showMenu(nbSelect === 0 ? 'main' : 'edit')

      this.modifyToolsVisibility.forEach(({ tool, visible }) => {
        this[`${tool}Btn`].style.display = visible ? null : 'none'
      })

      // un seul outil de modification dispo, on le lance
      onClickTransform.call(this, this.visibleTools.length === 1 ? this.visibleTools[0].tool : null)
    }))

    events.push(this.viewer.on('change:mapmode', onChangeMapMode.bind(this)))

    // lorsque ce controle est retiré de la carte, on fait le ménage dans les choses qu'il a créée
    events.push(this.viewer.Map.getControls().on('remove', (ev) => {
      if (ev.element === this) {
      // le calque de modification
        this.viewer.getFeatureSnapper().removeSource(this.modifyFeatureLayer.getSource())
        this.viewer.commonLayer.removeLayer('__MODIFY_FEATURE_LAYER', 'SYSTEM')
        // suppression des interactions du control
        this.viewer.Map.removeInteraction(this.selectInteraction)
        this.viewer.Map.removeInteraction(this.transformInteraction)
        this.viewer.Map.removeInteraction(this.modifyFeatureInteraction)
        this.viewer.Map.removeInteraction(this.removeVertexInteraction)

        // remet la bonne visibilité de la feature
        if (this.featureWasVisible) { this.viewer.dataLayer.showFeature(this.feature) }
        // retire les ecouteurs d'évènements nécessaire au control
        events.forEach(unByKey)
        // et l'eventuel interraction de dessin présente
        removeInterraction.call(this)
      }
    }))

    // #endregion

    // #region gestion de la feature existante
    // masque la feature de son calque en gardant son état original
    this.featureWasVisible = this.feature.get(this.viewer.dataLayer.propertiesName.VISIBLE_FEATURE) !== false
    this.viewer.dataLayer.hideFeature(this.feature)

    // Découper la géométrie de la feature en plein de petite géométrie a placer sur le calque d'édition:
    deaggregateAndAddFeature.call(this, this.feature)
    // #endregion

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

    this.viewer.dataLayer.changeMapMode('select', this.toolId)
    // si on est sur une geometrie simple (point / ligne / polygone)
    if (this.mode === 'CREER' && !this.isMultiGeometry) {
      addGeometry.call(this, options)
    } else if (this.mode === 'MODIFIER' && this.modifyFeatureLayer.getSource().getFeatures().length === 1) {
      onClickSelectAll.call(this)
    }
  }

  isToolboxVisible (name) {
    // la toolbox est visible l'utilisateur n'a pas demandé de la masquer
    // ou si on a plus d'un outil a dispo
    if (!this._hideToolbox ||
      this.visibleTools.length > 1 ||
      this.isMultiGeometry
    ) {
      return true
    }

    return false
  }

  get isMultiGeometry () {
    return ['GeometryCollection', 'MultiPoint', 'MultiPolygon', 'MultiLineString'].includes(this.geometryType)
  }

  get modifyToolsVisibility () {
    // Outils de modifications visibles sont fonction de la selection
    // et du type de geometrie en entrée
    const possibleTools = {
      move: true,
      rotate: true,
      scale: true,
      modifyVertex: true,
      removeVertex: true,
    }
    const selectedFeatures = this?.selectInteraction?.getFeatures() || null

    const nbSelect = selectedFeatures?.getLength() || 0
    if (nbSelect === 0) {
      Object.keys(possibleTools).forEach(key => { possibleTools[key] = false })
    } else if (nbSelect === 1 && selectedFeatures.getArray()[0].getGeometry().getType() === 'Point') {
      possibleTools.scale = false
      possibleTools.modifyVertex = false
      possibleTools.removeVertex = false
      possibleTools.rotate = false
    }

    return Object.entries(possibleTools).map(([tool, visible]) => {
      return {
        tool,
        visible: this.modifyTools.length > 0 && !this.modifyTools.includes(tool) ? false : visible,
      }
    })
  }

  get visibleTools () {
    return this.modifyToolsVisibility.filter(({ visible }) => visible)
  }

  get autoValidate () {
    return this._autoValidate || !this.isToolboxVisible()
  }
}

/** Set the control visibility
 * @param {boolean} visibility
 */
ControModifyTools.prototype.setVisible = function (visibility) {
  if (visibility) this.element.style.display = ''
  else this.element.style.display = 'none'
}

/** Get the control visibility
 * @return {boolean} b
 */
ControModifyTools.prototype.getVisible = function () {
  return this.element.style.display !== 'none'
}

ControModifyTools.prototype.showMenu = function (name) {
  this.helpTitle.style.display = 'none'
  this.menuMain.style.display = 'none'
  this.mainHelpDiv.style.display = 'none'
  this.menuEdit.style.display = 'none'
  this.modifyHelpDiv.style.display = 'none'
  this.menuTemplate.style.display = 'none'
  this.headerTemplateMenu.style.display = 'none'

  if (!this.isToolboxVisible(name)) {
    return
  }

  switch (name) {
    case 'main':
      this.helpTitle.style.display = null
      this.menuMain.style.display = null
      this.mainHelpDiv.style.display = null
      break
    case 'edit':
      this.helpTitle.style.display = null
      this.menuEdit.style.display = null
      this.modifyHelpDiv.style.display = null
      break
    case 'template':
      this.headerTemplateMenu.style.display = null
      this.menuTemplate.style.display = null
      break
  }
}

/**
 * Découper la géométrie de la feature en plein de petite géométrie a placer sur le calque d'édition:
 * @param {ol/feature} feature
 */
const deaggregateAndAddFeature = function (feature) {
  let features = []
  try {
    if (feature.getGeometry()) {
      switch (feature.getGeometry().getType()) {
        case 'Point':
        case 'LineString':
        case 'Polygon':
          features = [new Feature({ geometry: feature.getGeometry() })]
          break
        case 'GeometryCollection':
          features = feature.getGeometry().getGeometries().map((geometry) =>
            new Feature({ geometry })
          )
          break
        case 'MultiPoint':
          features = feature.getGeometry().getCoordinates().map((coordinates) =>
            new Feature({ geometry: new Point(coordinates) })
          )
          break
        case 'MultiLineString':
          features = feature.getGeometry().getLineStrings().map((lineString) =>
            new Feature({ geometry: lineString })
          )
          break
        case 'MultiPolygon':
          features = feature.getGeometry().getPolygons().map((polygon) =>
            new Feature({ geometry: polygon })
          )
          break
      }

      this.modifyFeatureLayer.getSource().addFeatures(features)
    }
  } catch (ex) {
    console.log(ex)
    return []
  }
  return features
}

const addButton = function (html, onClick) {
  const btn = document.createElement('button')
  btn.type = 'button'
  btn.innerHTML = html
  btn.addEventListener('click', onClick)
  return btn
}

// genère l'imagette de l'outil de creation via template
const getImageDateFromFeature = function (feature, imageWidth) {
  const reader = new GeoJSON()
  const originalFeature = reader.readFeature(feature)
  const extent = originalFeature.getGeometry().getExtent()
  const maxWidth = extent[2] - extent[0]
  const maxHeigth = extent[3] - extent[1]

  const maxWidthOrHeight = Math.max(maxHeigth, maxWidth)
  const factor = 1 / (maxWidthOrHeight / imageWidth)

  const imageHeight = maxWidth > maxHeigth ? maxHeigth * factor + 1 : imageWidth
  feature = JSON.parse(JSON.stringify(feature))

  feature = resizeAndMoveFeature(feature, factor, [0, 0])

  const featureToWrite = reader.readFeature(feature)

  const canvas = document.createElement('canvas')

  const vectorContext = toContext(canvas.getContext('2d'), {
    size: [imageWidth, imageHeight],
  })

  vectorContext.setStyle(templateStyle)
  vectorContext.drawGeometry(featureToWrite.getGeometry())

  const image = document.createElement('img')
  const dataURL = canvas.toDataURL()
  image.src = dataURL
  return image
}

/** génère l'ihm d'ajout de template en lisant les templates de creation */
const addTemplates = function (element, optionTemplate) {
  if (this.createTemplates?.features && this.createTemplates.features.length > 0) {
    const ul = document.createElement('ul')
    optionTemplate.features.forEach((feature, index) => {
      const templ = document.createElement('li')
      const image = getImageDateFromFeature(feature, 150)

      const label = document.createElement('span')
      label.innerHTML = feature.display.label || 'Libellé {display.label} non trouvé sur le template'

      templ.appendChild(image)
      templ.appendChild(label)

      templ.addEventListener('click', onClickAddFeatureTemplate.bind(this, index))
      ul.appendChild(templ)
    })

    const templ = document.createElement('li')
    templ.innerHTML = 'Annuler'
    templ.addEventListener('click', () => { this.showMenu('main') })
    ul.appendChild(templ)
    element.appendChild(ul)
  }
}

const onChangeMapMode = function (mapMode) {
  if (mapMode.toolId !== this.toolId) {
    console.log('[ControlmodifyTools] : changeMapMode appelé par un autre outil, on quitte la modification de geometrie')
    onClickCancel.call(this)
  }
}

// gestion des interaction de addGeometry et onClickAddFeatureTemplate
let onDrawEndListener = null
let onDrawAbortListener = null
let createTool = null
const removeInterraction = function () {
  unByKey(onDrawAbortListener)
  unByKey(onDrawEndListener)
  this.selectInteraction.setActive(true)
  this.setVisible(true)
  if (createTool) {
    this.viewer.Map.removeControl(createTool)
  }
  this.viewer.dataLayer.changeMapMode('select', this.toolId)
  onDrawEndListener = null
  onDrawAbortListener = null
  createTool = null
}

const addGeometry = function (options) {
  // Ajout d'une geométrie a la multi-géométrie: on utilise nos contrôle habituel
  // afin d'ajouter des morceau de geom a notre calque d'édition

  if (!this.mapModes[options.geometryType]) {
    throw new Error(`[ControlModifyTools] invalidTypeFeature ${options.geometryType}`)
  }

  onClickClearSelection.call(this)
  this.selectInteraction.setActive(false)

  this.viewer.dataLayer.changeMapMode(this.mapModes[options.geometryType], this.toolId)
  const mapMode = this.viewer.commonLayer.getCurrentMapMode()
  const [createInteraction] = mapMode.interactions

  // on utilise l'event OL plutot que viewer car meilleure gestion du scope
  onDrawEndListener = createInteraction.on('drawend', (ev) => {
    // a chaque fin d'interaction, on ajoute la géométrie au calque, la méthode valid reconstruira l'objet a renvoyer au createFeature
    this.modifyFeatureLayer.getSource().addFeature(ev.feature)
    // on remet en mapmode normal
    removeInterraction.call(this)
  })
  onDrawAbortListener = createInteraction.on('drawabort', (ev) => {
    // si on est en mode création d'un object a géométrie unique, pas besoin de cancel deux fois, on quitte
    if (this.mode === 'CREER' && !this.isMultiGeometry) {
      onClickCancel.call(this)
    }
    // on remet en mapmode normal
    removeInterraction.call(this)
  })

  const createToolOptions = {
    ...options,
    // la boite d'outils de création ne fait qu'utiliser l'interaction drawtouch venant de datalayer, on fourni la bonne
    interaction: createInteraction,
  }

  this.setVisible(false)
  if (options.padding) { createInteraction.setPadding(options.padding) }
  createInteraction.setDigitalizeOptions(createToolOptions.digitalizeOptions)
  createTool = new ControlCreateTools(createToolOptions)
  this.viewer.Map.addControl(createTool)
}

/** Ajout d'une multigéométrie a partir d'un template */
const onClickAddFeatureTemplate = function (index) {
  // Ajout d'une geométrie a la multi-géométrie: on utilise nos contrôle habituel afin de positionner le point puis on va
  // afin d'ajouter des morceau de geom a notre calque d'édition

  onClickClearSelection.call(this)
  this.selectInteraction.setActive(false)

  this.viewer.dataLayer.changeMapMode(this.mapModes.Point, this.toolId)
  const mapMode = this.viewer.commonLayer.getCurrentMapMode()
  const [createInteraction] = mapMode.interactions

  // on utilise l'event OL plutot que viewer car meilleure gestion du scope
  onDrawEndListener = createInteraction.on('drawend', (ev) => {
    // a chaque fin d'interaction, on ajoute la géométrie au calque, la méthode valid reconstruira l'objet a renvoyer au createFeature
    const jsonFeature = JSON.parse(JSON.stringify(this.createTemplates.features[index]))
    const reader = new GeoJSON()
    // calcule le point d'insertion de la geométrie

    const feature = reader.readFeature(jsonFeature)
    const toPoint = ev.feature.getGeometry().getCoordinates()
    const geomCenter = getCenter(feature.getGeometry().getExtent())
    // on considère toujours le template de feature en [0,0]
    const deltaX = toPoint[0] - geomCenter[0]
    const deltaY = toPoint[1] - geomCenter[1]

    feature.getGeometry().translate(deltaX, deltaY)
    const features = deaggregateAndAddFeature.call(this, feature)

    // si les propriétés du template n'existent pas sur la feature, on les initialise. Cela servira lors de la création
    const editingFeatureProperties = Object.keys(this.feature.getProperties())
    Object.entries(feature.getProperties()).forEach(([property, value]) => {
      if (!editingFeatureProperties.includes(property)) { this.feature.set(property, value) }
    })

    // on remet en mapmode normal
    removeInterraction.call(this)
    // on sélectionne les features afin que l'utilisateur ait l'ihm de translation, rotation..
    selectFeature.call(this, features)
  })
  onDrawAbortListener = createInteraction.on('drawabort', (ev) => {
    // si on est en mode création d'un object a géométrie unique, pas besoin de cancel deux fois, on quitte
    /* if (this.mode === 'CREER' && ['Point', 'LineString', 'Polygon'].includes(this.geometryType)) {
      onClickCancel.call(this)
    } */
    // on remet en mapmode normal
    removeInterraction.call(this)
  })

  const createToolOptions = {
    // la boite d'outils de création ne fait qu'utiliser l'interaction drawtouch venant de datalayer, on fourni la bonne
    viewer: this.viewer,
    geometryType: 'Point',
    interaction: createInteraction,
    title: 'Point d\'insertion',
  }

  this.setVisible(false)

  createTool = new ControlCreateTools(createToolOptions)
  this.viewer.Map.addControl(createTool)
}

// on va plutot lever des event qu'avoir des méthodes en entrée..
const onClickValid = function () {
  onClickTransform.call(this, null)
  // this.viewer.un('change:mapmode', onChangeMapMode, self)
  // lire les morceaux de features depuis le calque,
  // Si on est sur un type simple on ne prend que la première geométrie:
  // sinon on reconstruit une géométrie a partir des features
  let geometry = null
  const features = this.modifyFeatureLayer.getSource().getFeatures()
  if (features.length > 0) {
    switch (this.geometryType) {
      case
        'Point':
      case 'LineString':
      case 'Polygon':
        geometry = features[0].getGeometry()
        break
      case 'GeometryCollection':
        geometry = new GeometryCollection(features.map((feature) => feature.getGeometry()))
        break
      case 'MultiPoint':
        geometry = new MultiPoint(features.map((feature) => feature.getGeometry()))
        break
      case 'MultiLineString':
        geometry = new MultiLineString(features.map((feature) => feature.getGeometry()))
        break
      case 'MultiPolygon':
        geometry = new MultiPolygon(features.map((feature) => feature.getGeometry()))
        break
    }
  }
  this.feature.setGeometry(geometry)

  if (this.methods.validate) { this.methods.validate(this.feature) } else {
    console.warn('[ControlModifyTools] initialisé sans la méthode validate')
  }
  if (this.featureWasVisible) { this.viewer.dataLayer.showFeature(this.feature) }
}

const onClickCancel = function () {
  onClickTransform.call(this, null)
  this.feature.setGeometry(this.initialFeature.getGeometry())
  if (this.methods.cancel) { this.methods.cancel() } else {
    console.warn('[ControlModifyTools] initialisé sans la méthode cancel')
  }
  if (this.featureWasVisible) { this.viewer.dataLayer.showFeature(this.feature) }
}

const onClickRemove = function () {
  this.selectInteraction.getFeatures().forEach((feature) => this.modifyFeatureLayer.getSource().removeFeature(feature))
  // leve un event fantome pour mahj l'ihm du bouton remove
  onClickClearSelection.call(this)
}

// leve un event fantome pour mahj l'ihm du bouton remove
const onClickClearSelection = function () {
  this.selectInteraction.getFeatures().clear()
  this.selectInteraction.dispatchEvent(
    {
      type: 'select',
      selected: [],
      deselected: [],
      mapBrowserEvent: null,
    }
  )

  onClickTransform.call(this, null)
}

/** Choisi un des modes de d'edition d'une feature */
const onClickTransform = function (mode) {
  this.transformInteraction.set('translate', false)
  this.transformInteraction.set('rotate', false)
  this.transformInteraction.set('scale', false)
  this.transformInteraction.set('stretch', false)
  this.modifyFeatureInteraction.setActive(false)
  this.transformInteraction.setActive(false)
  this.removeVertexInteraction.setActive(false)

  this.moveBtn.classList.toggle('active', mode === 'move')
  this.rotateBtn.classList.toggle('active', mode === 'rotate')
  this.scaleBtn.classList.toggle('active', mode === 'scale')
  this.modifyVertexBtn.classList.toggle('active', mode === 'modifyVertex')
  this.removeVertexBtn.classList.toggle('active', mode === 'removeVertex')

  switch (mode) {
    case 'move':
      this.transformInteraction.setActive(true)
      this.transformInteraction.set('translate', true)
      this.transformInteraction.setSelection(this.selectInteraction.getFeatures())
      break
    case 'rotate':
      this.transformInteraction.setActive(true)
      this.transformInteraction.set('rotate', true)
      this.transformInteraction.setSelection(this.selectInteraction.getFeatures())
      break
    case 'scale':
      this.transformInteraction.setActive(true)
      this.transformInteraction.set('scale', true)
      this.transformInteraction.set('stretch', true)
      this.transformInteraction.setSelection(this.selectInteraction.getFeatures())
      break
    case 'modifyVertex':
      this.modifyFeatureInteraction.setActive(true)
      break
    case 'removeVertex':
      this.removeVertexInteraction.setActive(true)
      break
    default:
  }
}

/** Sélectionne tous les morceaux de feature de la feature en cours d'edition */
const onClickSelectAll = function () {
  selectFeature.call(this, this.modifyFeatureLayer.getSource().getFeatures())
}

const selectFeature = function (features) {
  features.forEach((feature) => {
    this.selectInteraction.getFeatures().push(feature)
  })

  this.selectInteraction.dispatchEvent(
    {
      type: 'select',
      selected: this.selectInteraction.getFeatures(),
      deselected: [],
      mapBrowserEvent: null,
    }
  )
  // onClickTransform.call(this, null)
}

export default ControModifyTools