Source: components/tools/map-tool-measure-height.js

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 Overlay from 'ol/Overlay'
import { image2world, getImageXY } from '@dataforsyningen/saul'
import { unByKey } from 'ol/Observable'
import LineString from 'ol/geom/LineString'
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 vertical distances in an image
 */
export class MeasureHeightTool extends HTMLElement {

  // properties
  mode = 'measureheight'
  overlayIdCounter = 1
  viewport
  map
  colorSetting = configuration.COLOR_SETTINGS.heightColor
  style = new Style({
    stroke: new Stroke({
      color: this.colorSetting,
      width: 3
    }),
    image: new CircleStyle({
      radius: 4,
      stroke: new Stroke({
        color: '#FF5252',
        width: 1
      })
    })
  })
  source = new VectorSource()
  layer = new VectorLayer({
    source: this.source,
    style: this.style,
    zIndex: 10
  })
  sketch
  helpTooltipElement
  helpTooltip
  measureTooltipElement
  measureTooltip
  draw
  axisFunc
  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('measureheight', 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 = 'height-btn'
    this.button_element.className = 'btn-height-measure quiet'
    this.button_element.title = 'Mål højde'
    this.button_element.innerHTML = `<svg><use href="${ svgSprites }#ruler-vertical"/></svg>`
    this.button_element.setAttribute('data-mode', 'measureheight')
    this.append(this.button_element)
  }

  pointerMoveHandler(event) {
    if (event.dragging) {
      return
    }
    let helpMsg = 'Klik på terræn for at måle højde'
    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,
      geometryFunction: (coords, geom) => {
        if (!geom) {
          geom = new LineString([])
        }
        if (this.axisFunc) {
          const adjusted_coordinate = this.axisFunc(coords[0], coords[1])
          geom.setCoordinates([
            coords[0],
            [
              adjusted_coordinate[0],
              adjusted_coordinate[1]
            ]
          ])
        } else {
          geom.setCoordinates(coords)
        }
        return geom
      }
    })
    this.map.addInteraction(this.draw)

    this.createHelpTooltip()
    this.createMeasureTooltip()

    this.draw.on('drawstart', (event) => {
      // set sketch
      this.sketch = event.feature
      this.axisFunc = this.generateVerticalAxisFunction(event.feature.getGeometry().getCoordinates()[0], state.items[this.viewport.dataset.itemkey])

      // 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

      this.measureTooltipElement.className = 'ol-tooltip ol-tooltip-static'
      this.measureTooltipElement.title = 'Klik for at slette måling'
      listener = this.sketch.getGeometry().on('change', (ev) => {
        const geom = ev.target
        const new_coords = geom.getCoordinates()
        const corrected_coord = this.axisFunc(new_coords[0], new_coords[1])
        this.measureTooltipElement.innerHTML = `${ corrected_coord[2] }m`
        this.measureTooltip.setOffset([0, -7])
        this.measureTooltip.setPosition(this.calcTooltipPosition(geom))
      })

    })

    this.draw.on('drawend', () => {

      this.drawAdjustedLine(this.axisFunc, this.calcTooltipPosition, this.measureTooltipElement, this.measureTooltip)

      // unset sketch
      this.sketch = null
      // unset tooltip so that a new one can be created
      this.measureTooltipElement = null
      this.createMeasureTooltip()
      unByKey(listener)
    })
  }

  drawAdjustedLine(axisFunction, tooltipPositionFunction, tooltipElement, tooltip) {

    const geom = this.sketch.getGeometry()
    const new_coords = geom.getCoordinates()

    // Calculate new xy in order to constrain to image height axis
    const corrected_coord = axisFunction(new_coords[0], new_coords[1])
    new_coords[1] = [corrected_coord[0], corrected_coord[1]]
    // Snap to height axis
    geom.setCoordinates(new_coords)
    this.sketch.setGeometry(geom)

    tooltipElement.innerHTML = `${corrected_coord[2]}m`
    tooltipElement.className = 'ol-tooltip ol-tooltip-static'
    tooltip.setOffset([0, -7])
    tooltip.setPosition(tooltipPositionFunction(geom))
  }

  /**
   * 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
    })

    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()
  }

  generateVerticalAxisFunction(coord, image_item) {
    const world0 = image2world(image_item, coord[0], coord[1], 0)
    const world1 = image2world(image_item, coord[0], coord[1], 10)
    const image0 = getImageXY(image_item, world0[0], world0[1])
    const image1 = getImageXY(image_item, world1[0], world1[1])
    const skew_factor = [(image1[0] - image0[0])/10, (image1[1] - image0[1])/10]

    // Return a function that takes two image coordinates and returns the second coordinate with Y axis skew adjusted
    return function(image_coor_1, image_coor_2) {
      const s = skew_factor
      const delta_y = image_coor_2[1] - image_coor_1[1]
      const ratio_y = delta_y / s[1]
      const delta_x = ratio_y * s[0] // We assume x and y ratios are equal
      const x = image_coor_1[0] + delta_x
      return [x, image_coor_2[1], Math.abs(ratio_y).toFixed(1)]
    }
  }

}