import { unByKey } from 'ol/Observable'
import OlControl from 'ol/control/Control'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import { RegularShape, Fill, Stroke, Style, Text } from 'ol/style'
import Point from 'ol/geom/Point'
import Feature from 'ol/Feature'
import OlExtElement from 'ol-ext/util/element'
import { createButton, createInteractiveHelp } from './ControlUtils'
import ControlCreateTools from './ControlCreateTools'
const layerName = '__kmapv_control_triangulation'
const stroke = new Stroke({ color: 'red', width: 2 })
const fill = new Fill({ color: 'red' })
const texts = {
errorMinDist: 'La distance entre les points est trop courte',
errorMaxDist: 'La distance entre les points est trop éloignée',
errorPointManquant: 'Veuillez choisir les points de relevé',
errorRayonNaN: 'La distance saisie n\'est pas valide',
helpTitle: 'Outil de triangulation',
helptooltip: 'Afficher / Masquer l\'aide',
help1: 'Cet outil calcule les points distants de deux positions choisi',
help2: 'Pour chaque points de relevé :',
help3: 'Utiliser <i class="kmapv-icon kmapv-icon-map-marker"></i> pour choisir la position',
help4: 'Saisir la distance du point à retrouver',
help5: 'Lancer le calcul',
help6: 'Effacer tous les résultats',
help7: 'Arrêter d\'utiliser l\'outil',
point: 'Repère',
distance: 'Distance',
valid: 'Valider',
clear: 'Effacer',
cancel: 'Annuler',
}
const styles = {
cross: new Style({
image: new RegularShape({
fill,
stroke,
points: 4,
radius: 10,
radius2: 0,
angle: 0,
}),
}),
label: new Style({
text: new Text({
textAlign: 'start',
textBaseline: 'top',
font: '14px sans-serif',
text: 'gg',
offsetX: 5,
offsetY: 5,
fill,
}),
}),
x: new Style({
image: new RegularShape({
fill,
stroke,
points: 4,
radius: 10,
radius2: 0,
angle: Math.PI / 4,
}),
}),
}
/** Control de création de deux point par triangulation
* L'utilisateur choisi deux point et deux distance
* L'outil pose deux nouveaux points par calcul de l'intersection de deux cercles
*
* formules:
* http://math.15873.pagesperso-orange.fr/IntCercl.html
* https://www.geogebra.org/m/sfasuhk7
*
* code fonctionnel:
* https://codepen.io/BenjaminS/pen/ZWqVRy?editors=1111
*
*/
class ControlTriangulation extends OlControl {
constructor (options) {
const className = options.className !== undefined ? options.className : ''
const classNames = (className || '') + ' kmapv-bottom-tools kmap-triangulation-tools' + (options.target ? '' : ' ol-unselectable ol-control')
const element = OlExtElement.create('DIV', {
className: classNames,
})
super({
element,
target: options.target,
})
if (!options.viewer) {
console.error('[ControlTriangulation] options.viewer non présent')
return
}
this.viewer = options.viewer
this.snapOptions = options.snapOptions
this.toolId = options.toolId || 'triangulation-tool'
// #region build de l'ihm
this.buildIhm(element)
// #endregion
// créer un calque spécifique triangulation si il n'existe pas (ou utiliser celui recu dans les options)
// #region Calque et interraction nécessaire a cet outil
if (options.layer) {
// on a fourni un layer sur lequel trianguler
this.layer = options.layer
} else {
// 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.x,
displayInLayerSwitcher: false,
})
this.viewer.commonLayer.addLayer(this.layer, 'SYSTEM')
this.viewer.getFeatureSnapper().addSource({ groups: [{ name: 'KMAPV', edge: true, vertex: true }], source: this.layer.getSource() })
}
}
// #endregion
// garde l'état d'origine de la barre horizontal
this.horizontalToolbarVisility = this.viewer.horizontalToolbar.getVisible()
this.viewer.horizontalToolbar.setVisible(false)
// etant donné qu'on va se placer en bas on masque la barre horizontal du viewer puis on le remttra quand le control sera retiré
// 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) {
// remettre la barre horizontal dans son état d'origine
this.viewer.horizontalToolbar.setVisible(this.horizontalToolbarVisility)
unByKey(listenerRemoveControl)
}
})
/* OlControl.call(this, {
element: element,
target: options.target,
}) */
}
}
/** Set the control visibility
* @param {boolean} visibility
*/
ControlTriangulation.prototype.setVisible = function (visibility) {
if (visibility) this.element.style.display = ''
else this.element.style.display = 'none'
}
/** construction de la toolbar */
ControlTriangulation.prototype.buildIhm = function (element) {
const validHtml = `<i class="kmapv-icon kmapv-icon-valid"></i><span>${texts.valid}</span>`
const clearHtml = `<i class="kmapv-icon kmapv-icon-clear"></i><span>${texts.clear}</span>`
const cancelHtml = `<i class="kmapv-icon kmapv-icon-cancel"></i><span>${texts.cancel}</span>`
this.menuMain = OlExtElement.create('DIV', {
className: 'tools',
})
// #region aide interractive
const helpTexts = [
texts.help1,
`<b>${texts.help2}</b>`,
texts.help3,
texts.help4,
`${validHtml} ${texts.help5}`,
`${clearHtml} ${texts.help6}`,
`${cancelHtml} ${texts.help7}`]
const { helpTitle, helpDiv } = createInteractiveHelp({
title: texts.helpTitle,
buttonTitle: texts.helptooltip,
helpTexts,
joinText: '<br/>',
})
element.appendChild(helpTitle)
element.appendChild(helpDiv)
// #endregion
this.errorPlace = OlExtElement.create('label', { style: { color: 'red' } })
element.appendChild(this.errorPlace)
const inputsContainer = OlExtElement.create('div', {
className: 'input-container',
})
const input1Container = OlExtElement.create('span', { className: 'input-wrapper' })
input1Container.appendChild(OlExtElement.create('span', { text: texts.distance }))
this.input1 = document.createElement('input')
this.input1.setAttribute('type', 'number')
this.input1.setAttribute('value', 1)
this.input1.setAttribute('min', '0')
input1Container.appendChild(this.input1)
const ligne1Container = OlExtElement.create('div', { })
ligne1Container.appendChild(OlExtElement.create('label', { text: `${texts.point} 1 : ` }))
ligne1Container.appendChild(input1Container)
ligne1Container.appendChild(createButton('<i class="kmapv-icon kmapv-icon-plus"></i>', () => this.input1.stepUp()))
ligne1Container.appendChild(createButton('<i class="kmapv-icon kmapv-icon-minus"></i>', () => this.input1.stepDown()))
ligne1Container.appendChild(createButton('<i class="kmapv-icon kmapv-icon-map-marker"></i>', onClickAddPoint.bind(this, '1')))
const input2Container = OlExtElement.create('span', { className: 'input-wrapper' })
input2Container.appendChild(OlExtElement.create('span', { text: texts.distance }))
this.input2 = document.createElement('input')
this.input2.setAttribute('type', 'number')
this.input2.setAttribute('value', 1)
this.input2.setAttribute('min', '0')
input2Container.appendChild(this.input2)
const ligne2Container = OlExtElement.create('div', { })
ligne2Container.appendChild(OlExtElement.create('label', { text: `${texts.point} 2 : ` }))
ligne2Container.appendChild(input2Container)
ligne2Container.appendChild(createButton('<i class="kmapv-icon kmapv-icon-plus"></i>', () => this.input2.stepUp()))
ligne2Container.appendChild(createButton('<i class="kmapv-icon kmapv-icon-minus"></i>', () => this.input2.stepDown()))
ligne2Container.appendChild(createButton('<i class="kmapv-icon kmapv-icon-map-marker"></i>', onClickAddPoint.bind(this, '2')))
inputsContainer.appendChild(ligne1Container)
inputsContainer.appendChild(ligne2Container)
const validBtn = createButton(validHtml, onClickValid.bind(this))
const clearBtn = createButton(clearHtml, onClickClear.bind(this))
const cancelBtn = createButton(cancelHtml, onClickCancel.bind(this))
this.menuMain.appendChild(inputsContainer)
this.menuMain.appendChild(validBtn)
this.menuMain.appendChild(clearBtn)
this.menuMain.appendChild(cancelBtn)
element.appendChild(this.menuMain)
}
// gestion des interaction
let onDrawEndListener = null
let onDrawAbortListener = null
let createTool = null
const removeInterraction = function () {
unByKey(onDrawAbortListener)
unByKey(onDrawEndListener)
this.setVisible(true)
if (createTool) {
this.viewer.Map.removeControl(createTool)
}
this.viewer.dataLayer.changeMapMode('select', this.toolId)
onDrawEndListener = null
onDrawAbortListener = null
createTool = null
}
/** Ajout d'un un point temporaire pour la triangulation */
const onClickAddPoint = function (numPoint) {
// 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
this.viewer.dataLayer.changeMapMode('_drawPointTouch', 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) => {
const id = 'point_depart_' + numPoint
if (this.layer.getSource().getFeatureById(id)) {
this.layer.getSource().getFeatureById(id).setGeometry(ev.feature.getGeometry())
} else {
ev.feature.setId(id)
ev.feature.set('numPoint', numPoint)
// ev.feature.set('type', 'point_construction')
ev.feature.setStyle((feature) => {
styles.label.getText().setText(numPoint)
return [
styles.cross,
styles.label,
]
})
this.layer.getSource().addFeature(ev.feature)
}
// on remet en mapmode normal
removeInterraction.call(this)
})
onDrawAbortListener = createInteraction.on('drawabort', (ev) => {
// 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 ' + numPoint,
digitalizeOptions: {
snapOptions: this.snapOptions,
},
}
this.setVisible(false)
createTool = new ControlCreateTools(createToolOptions)
this.viewer.Map.addControl(createTool)
}
const onClickCancel = function () {
this.viewer.Map.removeControl(this)
}
const onClickClear = function () {
this.layer.getSource().clear()
}
const onClickValid = function () {
this.errorPlace.textContent = null
const pt0 = this.layer.getSource().getFeatures().find((feature) => feature.get('numPoint') === '1')
const pt1 = this.layer.getSource().getFeatures().find((feature) => feature.get('numPoint') === '2')
const r0 = this.input1.value
const r1 = this.input2.value
if (!pt0 || !pt1) {
this.errorPlace.textContent = texts.errorPointManquant
return
}
if (r0 === null || r1 === null || r0 === '' || r1 === '' || isNaN(r0) || isNaN(r1)) {
this.errorPlace.textContent = texts.errorRayonNaN
return
}
const pt0Coords = pt0.getGeometry().getCoordinates()
const pt1Coords = pt1.getGeometry().getCoordinates()
const rtn = intersection(pt0Coords[0], pt0Coords[1], parseFloat(r0), pt1Coords[0], pt1Coords[1], parseFloat(r1))
if (Array.isArray(rtn)) {
this.layer.getSource().addFeature(new Feature(new Point([rtn[0], rtn[1]])))
this.layer.getSource().addFeature(new Feature(new Point([rtn[2], rtn[3]])))
} else {
this.errorPlace.textContent = rtn
}
}
function intersection (x0, y0, r0, x1, y1, r1) {
/* dx and dy are the vertical and horizontal distances between
* the circle centers.
*/
const dx = x1 - x0
const dy = y1 - y0
/* Determine the straight-line distance between the centers. */
const d = Math.sqrt((dy * dy) + (dx * dx))
/* Check for solvability. */
if (d > (r0 + r1)) {
/* no solution. circles do not intersect. */
return texts.errorMinDist
}
if (d < Math.abs(r0 - r1)) {
/* no solution. one circle is contained in the other */
return texts.errorMaxDist
}
/* 'point 2' is the point where the line through the circle
* intersection points crosses the line between the circle
* centers.
*/
/* Determine the distance from point 0 to point 2. */
const a = ((r0 * r0) - (r1 * r1) + (d * d)) / (2.0 * d)
/* Determine the coordinates of point 2. */
const x2 = x0 + (dx * a / d)
const y2 = y0 + (dy * a / d)
/* Determine the distance from point 2 to either of the
* intersection points.
*/
const h = Math.sqrt((r0 * r0) - (a * a))
/* Now determine the offsets of the intersection points from
* point 2.
*/
const rx = -dy * (h / d)
const ry = dx * (h / d)
/* Determine the absolute intersection points. */
const xi = x2 + rx
const xiPrime = x2 - rx
const yi = y2 + ry
const yiPrime = y2 - ry
return [xi, yi, xiPrime, yiPrime]
}
export default ControlTriangulation