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