import { Vector as VectorSource } from 'ol/source'
import { Vector as VectorLayer } from 'ol/layer'
import {Circle as CircleStyle, Stroke, Style } from 'ol/style'
import Draw from 'ol/interaction/Draw'
import { getDistance } from 'ol/sphere'
import Overlay from 'ol/Overlay'
import { getWorldXYZ, createTranslator } from '@dataforsyningen/saul'
import { unByKey } from 'ol/Observable'
import { configuration } from "../../modules/configuration"
import { state } from '../../state/index.js'
import svgSprites from '@dataforsyningen/designsystem/assets/icons.svg'
import { findAncestor } from '../../modules/utilities.js'
import { awaitMap, setModeChangeHandler, setLineRemoveHandler, setToggleHandler } from './map-tool-measure-shared.js'
const featureIdentifiers = []
/**
* Enables user to measure horizontal distances in an image
*/
export class MeasureWidthTool extends HTMLElement {
// properties
mode = 'measurewidth'
overlayIdCounter = 1
coorTranslator = createTranslator()
viewport
map
colorSetting = configuration.COLOR_SETTINGS.widthColor
style = new Style({
stroke: new Stroke({
color: this.colorSetting,
width: 3
}),
image: new CircleStyle({
radius: 4,
stroke: new Stroke({
color: '#3EDDC6',
width: 1
})
})
})
source = new VectorSource()
layer = new VectorLayer({
source: this.source,
style: this.style,
zIndex: 10
})
sketch
helpTooltipElement
helpTooltip
measureTooltipElement
measureTooltip
draw
modeListenDisposer
lineRemoveDisposer
toggleDisposer
constructor() {
super()
}
connectedCallback() {
this.createDOM()
this.viewport = findAncestor(this, 'skraafoto-viewport')
awaitMap(this.viewport).then((mapObj) => {
this.map = mapObj
this.map.addLayer(this.layer)
this.modeListenDisposer = setModeChangeHandler(this)
// Removes drawn lines on image change
this.lineRemoveDisposer = setLineRemoveHandler(this)
})
this.toggleDisposer = setToggleHandler('measurewidth', this.querySelector('button'))
}
disconnectedCallback() {
this.modeListenDisposer()
this.lineRemoveDisposer()
this.toggleDisposer()
}
// Methods
createDOM() {
this.button_element = document.createElement('button')
this.button_element.style.borderRadius = '0'
this.button_element.id = 'length-btn'
this.button_element.className = 'btn-width-measure quiet'
this.button_element.title = 'Mål afstand'
this.button_element.innerHTML = `<svg><use href="${ svgSprites }#ruler-horizontal"/></svg>`
this.button_element.setAttribute('data-mode', 'measurewidth')
this.append(this.button_element)
}
pointerMoveHandler(event) {
if (event.dragging) {
return
}
let helpMsg = 'Klik på terræn for at måle afstand'
if (this.sketch) {
helpMsg = 'Klik for at afslutte måling'
}
this.helpTooltipElement.innerHTML = helpMsg
this.helpTooltip.setPosition(event.coordinate)
this.helpTooltipElement.classList.remove('hidden')
}
mouseOutHandler() {
this.helpTooltipElement.classList.add('hidden')
}
addInteraction() {
let listener
this.draw = new Draw({
source: this.source,
type: 'LineString',
style: this.style,
maxPoints: 2,
minPoints: 2
})
this.map.addInteraction(this.draw)
this.createHelpTooltip()
this.createMeasureTooltip()
this.draw.on('drawstart', (event) => {
// Set sketch
this.sketch = event.feature
// Store references to the feature and overlay
const tooltipId = `tooltip-${this.overlayIdCounter++}`
this.measureTooltipElement.setAttribute('data-tooltip-id', tooltipId)
this.measureTooltipElement.setAttribute('data-tooltip', 'measure')
const featureOverlayPair = {
feature: this.sketch,
overlay: this.measureTooltip,
}
featureIdentifiers[tooltipId] = featureOverlayPair
let tooltipCoord = event.coordinate
listener = this.sketch.getGeometry().on('change', async (ev) => {
const geom = ev.target
let output
output = await this.calculateDistance(geom.flatCoordinates)
tooltipCoord = geom.getLastCoordinate()
this.measureTooltipElement.innerHTML = output
this.measureTooltip.setPosition(tooltipCoord)
})
})
this.draw.on('drawend', async () => {
const geom = this.sketch.getGeometry()
this.measureTooltipElement.innerHTML = await this.calculateDistance(geom.flatCoordinates)
this.measureTooltipElement.className = 'ol-tooltip ol-tooltip-static'
this.measureTooltipElement.title = 'Klik for at slette måling'
this.measureTooltip.setOffset([0, -7])
this.measureTooltip.setPosition(this.calcTooltipPosition(geom))
// Unset sketch
this.sketch = null
// Unset tooltip so that a new one can be created
this.measureTooltipElement = null
this.createMeasureTooltip()
unByKey(listener)
})
}
/**
* Creates a new help tooltip
*/
createHelpTooltip() {
if (this.helpTooltipElement) {
this.helpTooltipElement.remove()
}
this.helpTooltipElement = document.createElement('div')
this.helpTooltipElement.className = 'ol-tooltip hidden'
this.helpTooltip = new Overlay({
element: this.helpTooltipElement,
offset: [15, 0],
positioning: 'center-left'
})
this.map.addOverlay(this.helpTooltip)
}
/**
* Creates a new measure tooltip
*/
createMeasureTooltip() {
// Generate a unique identifier for the tooltip
const tooltipId = `tooltip-${this.overlayIdCounter++}`
if (this.measureTooltipElement) {
this.measureTooltipElement.remove()
}
this.measureTooltipElement = document.createElement('div')
this.measureTooltipElement.className = 'ol-tooltip ol-tooltip-measure'
this.measureTooltipElement.setAttribute('data-tooltip-id', tooltipId)
this.measureTooltip = new Overlay({
element: this.measureTooltipElement,
offset: [0, -15],
positioning: 'bottom-center',
stopEvent: false,
insertFirst: false
})
// Add a mouseenter event listener to show a cross when hovering over the tooltip
this.measureTooltipElement.addEventListener('mouseenter', () => {
this.measureTooltipElement.innerHTML = 'X'; // Display a cross or any other symbol you prefer
this.measureTooltipElement.style.cursor = 'pointer';
});
// Add a mouseleave event listener to revert to the measurement text when mouse leaves
this.measureTooltipElement.addEventListener('mouseleave', () => {
this.measureTooltipElement.innerHTML = 'Click to remove measurement';
this.measureTooltipElement.style.cursor = 'auto';
});
this.measureTooltipElement.addEventListener('click', (event) => {
event.stopPropagation() // Prevent the click event from propagating to the map
// Remove the current drawing associated with the clicked tooltip
const clickedTooltipId = event.currentTarget.getAttribute('data-tooltip-id')
const featureToRemove = featureIdentifiers[clickedTooltipId]
if (featureToRemove) {
this.source.removeFeature(featureToRemove.feature) // Remove the feature from the source
this.map.removeOverlay(featureToRemove.overlay) // Remove the overlay from the map
this.draw.setActive(false) // Disable the draw interaction
}
this.draw.setActive(true) // Re-enable the draw interaction
})
// Store references to the feature and overlay
featureIdentifiers[tooltipId] = this.measureTooltip
this.map.addOverlay(this.measureTooltip)
}
calcTooltipPosition(geometry) {
return geometry.getFlatMidpoint()
}
async calculateDistance(coords) {
const world_coords = []
for (let n = 0; coords.length > n; n = n+2) {
const new_coord = await getWorldXYZ({
image: state.items[this.viewport.dataset.itemkey],
terrain: state.terrain.data,
xy: [coords[n], coords[n + 1]]
})
// Since `getDistance` works with WGS84 coordinates, we must translate the coords
const wgs84_coords = this.coorTranslator.inverse([new_coord[0], new_coord[1]])
world_coords.push(wgs84_coords)
}
const distance = getDistance(world_coords[0], world_coords[1])
return distance.toFixed(1) + 'm'
}
}