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