Source: control/cad.js

import { Style, Stroke } from 'ol/style';
import { Point, LineString, Polygon, MultiPoint, Circle } from 'ol/geom';
import Feature from 'ol/Feature';
import Vector from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { Pointer, Snap } from 'ol/interaction';
import { distance as euclideanDistance } from 'ol/coordinate';
import { OverlayOp } from 'jsts/org/locationtech/jts/operation/overlay';
import { getUid } from 'ol/util';
import Control from './control';
import cadSVG from '../../img/cad.svg';
import { SnapEvent, SnapEventType } from '../event';
import {
  parser,
  getProjectedPoint,
  getEquationOfLine,
  getShiftedMultiPoint,
  getIntersectedLinesAndPoint,
  isSameLines,
  defaultSnapStyles,
  VH_LINE_KEY,
  SNAP_POINT_KEY,
  SNAP_FEATURE_TYPE_PROPERTY,
  SEGMENT_LINE_KEY,
  ORTHO_LINE_KEY,
  CUSTOM_LINE_KEY,
} from '../helper';

/**
 * Control with snapping functionality for geometry alignment.
 * @extends {Control}
 * @alias ole.CadControl
 */
class CadControl extends Control {
  /**
   * @param {Object} [options] Tool options.
   * @param {Function} [options.drawCustomSnapLines] Allow to draw more snapping lines using selected coordinates.
   * @param {Function} [options.filter] Returns an array containing the features
   *   to include for CAD (takes the source as a single argument).
   * @param {Function} [options.extentFilter] An optional spatial filter for the features to snap with. Returns an ol.Extent which will be used by the source.getFeaturesinExtent method.
   * @param {Function} [options.lineFilter] An optional filter for the generated snapping lines
   *   array (takes the lines and cursor coordinate as arguments and returns the new line array)
   * @param {Number} [options.nbClosestFeatures] Number of features to use for snapping (closest first). Default is 5.
   * @param {Number} [options.limitGuidesPerFeature] A feature with complex geometry may generate
   *   a large number of lines. This option can be used to limit the guides to the n closest segments. Deafult is 3.
   * @param {Number} [options.snapTolerance] Snap tolerance in pixel
   *   for snap lines. Default is 10.
   * @param {Boolean} [options.showSnapLines] Whether to show
   *   snap lines (default is true).
   * @param {Boolean} [options.showSnapPoints] Whether to show
   *  snap points around the closest feature.
   * @param {Boolean} [options.showOrthoLines] Whether to show
   *   snap lines that arae perpendicular to segment (default is true).
   * @param {Boolean} [options.showSegmentLines] Whether to show
   *   snap lines that extends a segment (default is true).
   * @param {Boolean} [options.showVerticalAndHorizontalLines] Whether to show vertical
   *   and horizontal lines for each snappable point (default is true).
   * @param {Boolean} [options.snapLinesOrder] Define order of display of snap lines,
   *   must be an array containing the following values 'ortho', 'segment', 'vh'. Default is ['ortho', 'segment', 'vh', 'custom'].
   * @param {Number} [options.snapPointDist] Distance of the
   *   snap points (default is 30).
   * @param {Boolean} [options.useMapUnits] Whether to use map units
   *   as measurement for point snapping. Default is false (pixel are used).
   * @param {ol.VectorSource} [options.source] The vector source to retrieve the snappable features from.
   * @param {ol.style.Style.StyleLike} [options.snapStyle] Style used for the snap layer.
   * @param {ol.style.Style.StyleLike} [options.linesStyle] Style used for the lines layer.
   *
   */
  constructor(options = {}) {
    super({
      title: 'CAD control',
      className: 'ole-control-cad',
      image: cadSVG,
      showSnapPoints: true,
      showSnapLines: false,
      showOrthoLines: true,
      showSegmentLines: true,
      showVerticalAndHorizontalLines: true,
      snapPointDist: 10,
      snapLinesOrder: ['ortho', 'segment', 'vh'],
      ...options,
    });

    /**
     * Interaction for handling move events.
     * @type {ol.interaction.Pointer}
     * @private
     */
    this.pointerInteraction = new Pointer({
      handleMoveEvent: this.onMove.bind(this),
    });

    /**
     * Layer for drawing snapping geometries.
     * @type {ol.layer.Vector}
     * @private
     */
    this.snapLayer = new Vector({
      source: new VectorSource(),
      style: options.snapStyle || [
        defaultSnapStyles[VH_LINE_KEY],
        defaultSnapStyles[SNAP_POINT_KEY],
      ],
    });

    /**
     * Layer for colored lines indicating
     * intersection point between snapping lines.
     * @type {ol.layer.Vector}
     * @private
     */
    this.linesLayer = new Vector({
      source: new VectorSource(),
      style: options.linesStyle || [
        new Style({
          stroke: new Stroke({
            width: 1,
            lineDash: [5, 10],
            color: '#FF530D',
          }),
        }),
      ],
    });

    /**
     * Function to draw more snapping lines.
     * @type {Function}
     * @private
     */
    this.drawCustomSnapLines = options.drawCustomSnapLines;

    /**
     * Number of features to use for snapping (closest first). Default is 5.
     * @type {Number}
     * @private
     */
    this.nbClosestFeatures =
      options.nbClosestFeatures === undefined ? 5 : options.nbClosestFeatures;

    /**
     * Number of guide lines to limit cad to generate for a single feature.
     * this.nbClosestFeatures * this.limitGuidesPerFeature will be the maximum
     * number of guides shown at any time. Default is 3.
     * @type {Number}
     * @private
     */
    this.limitGuidesPerFeature =
      options.limitGuidesPerFeature === undefined
        ? 3
        : options.limitGuidesPerFeature;

    /**
     * Snap tolerance in pixel.
     * @type {Number}
     * @private
     */
    this.snapTolerance =
      options.snapTolerance === undefined ? 10 : options.snapTolerance;

    /**
     * Filter the features to snap with.
     * @type {Function}
     * @private
     */
    this.filter = options.filter || (() => true);

    /**
     * Filter the features spatially.
     */
    this.extentFilter =
      options.extentFilter ||
      (() => [-Infinity, -Infinity, Infinity, Infinity]);

    /**
     * Filter the generated line list
     */
    this.lineFilter = options.lineFilter;

    /**
     * Interaction for snapping
     * @type {ol.interaction.Snap}
     * @private
     */
    this.snapInteraction = new Snap({
      pixelTolerance: this.snapTolerance,
      source: this.snapLayer.getSource(),
    });

    this.standalone = false;

    this.handleInteractionAdd = this.handleInteractionAdd.bind(this);
  }

  /**
   * @inheritdoc
   */
  getDialogTemplate() {
    const distLabel = this.properties.useMapUnits ? 'map units' : 'px';

    return `
      <div>
        <input
          id="aux-cb"
          type="radio"
          name="radioBtn"
          ${this.properties.showSnapLines ? 'checked' : ''}
        >
        <label>Show snap lines</label>
      </div>
      <div>
        <input
          id="dist-cb"
          type="radio"
          name="radioBtn"
          ${this.properties.showSnapPoints ? 'checked' : ''}
        >
        <label>Show snap points. Distance (${distLabel}):</label>
        <input type="text" id="width-input"
          value="${this.properties.snapPointDist}">
      </div>
    `;
  }

  handleInteractionAdd(evt) {
    const pos = evt.target.getArray().indexOf(this.snapInteraction);

    if (
      this.snapInteraction.getActive() &&
      pos > -1 &&
      pos !== evt.target.getLength() - 1
    ) {
      this.deactivate(true);
      this.activate(true);
    }
  }

  /**
   * @inheritdoc
   */
  setMap(map) {
    if (this.map) {
      this.map.getInteractions().un('add', this.handleInteractionAdd);
    }

    super.setMap(map);

    // Ensure that the snap interaction is at the last position
    // as it must be the first to handle the  pointermove event.
    if (this.map) {
      this.map.getInteractions().on('add', this.handleInteractionAdd);
    }
  }

  /**
   * Handle move event.
   * @private
   * @param {ol.MapBrowserEvent} evt Move event.
   */
  onMove(evt) {
    const features = this.getClosestFeatures(
      evt.coordinate,
      this.nbClosestFeatures,
    );

    this.linesLayer.getSource().clear();
    this.snapLayer.getSource().clear();

    this.pointerInteraction.dispatchEvent(
      new SnapEvent(SnapEventType.SNAP, features.length ? features : null, evt),
    );

    if (this.properties.showSnapLines) {
      this.drawSnapLines(evt.coordinate, features);
    }

    if (this.properties.showSnapPoints && features.length) {
      this.drawSnapPoints(evt.coordinate, features[0]);
    }
  }

  /**
   * Returns a list of the {num} closest features
   * to a given coordinate.
   * @private
   * @param {ol.Coordinate} coordinate Coordinate.
   * @param {Number} nbFeatures Number of features to search.
   * @returns {Array.<ol.Feature>} List of closest features.
   */
  getClosestFeatures(coordinate, nbFeatures = 1) {
    const editFeature = this.editor.getEditFeature();
    const drawFeature = this.editor.getDrawFeature();
    const currentFeatures = [editFeature, drawFeature].filter((f) => !!f);

    const cacheDist = {};
    const dist = (f) => {
      const uid = getUid(f);
      if (!cacheDist[uid]) {
        const cCoord = f.getGeometry().getClosestPoint(coordinate);
        const dx = cCoord[0] - coordinate[0];
        const dy = cCoord[1] - coordinate[1];
        cacheDist[uid] = dx * dx + dy * dy;
      }
      return cacheDist[uid];
    };
    const sortByDistance = (a, b) => dist(a) - dist(b);

    let features = this.source
      .getFeaturesInExtent(this.extentFilter())
      .filter(
        (feature) => this.filter(feature) && !currentFeatures.includes(feature),
      )
      .sort(sortByDistance)
      .slice(0, nbFeatures);

    // When using showSnapPoints, return all features except edit/draw features
    if (this.properties.showSnapPoints) {
      return features;
    }

    // When using showSnapLines, return all features but edit/draw features are
    // cloned to remove the node at the mouse position.
    currentFeatures.filter(this.filter).forEach((feature) => {
      const geom = feature.getGeometry();

      if (!(geom instanceof Circle) && !(geom instanceof Point)) {
        const snapGeom = getShiftedMultiPoint(geom, coordinate);
        const isPolygon = geom instanceof Polygon;
        const snapFeature = feature.clone();
        snapFeature
          .getGeometry()
          .setCoordinates(
            isPolygon ? [snapGeom.getCoordinates()] : snapGeom.getCoordinates(),
          );
        features = [snapFeature, ...features];
      }
    });

    return features;
  }

  /**
   * Returns an extent array, considers the map rotation.
   * @private
   * @param {ol.Geometry} geometry An OL geometry.
   * @returns {Array.<number>} extent array.
   */
  getRotatedExtent(geometry, coordinate) {
    const coordinates =
      geometry instanceof Polygon
        ? geometry.getCoordinates()[0]
        : geometry.getCoordinates();

    if (!coordinates.length) {
      // Polygons initially return a geometry with an empty coordinate array, so we need to catch it
      return [coordinate];
    }

    // Get the extreme X and Y using pixel values so the rotation is considered
    const xMin = coordinates.reduce((finalMin, coord) => {
      const pixelCurrent = this.map.getPixelFromCoordinate(coord);
      const pixelFinal = this.map.getPixelFromCoordinate(
        finalMin || coordinates[0],
      );
      return pixelCurrent[0] <= pixelFinal[0] ? coord : finalMin;
    });
    const xMax = coordinates.reduce((finalMax, coord) => {
      const pixelCurrent = this.map.getPixelFromCoordinate(coord);
      const pixelFinal = this.map.getPixelFromCoordinate(
        finalMax || coordinates[0],
      );
      return pixelCurrent[0] >= pixelFinal[0] ? coord : finalMax;
    });
    const yMin = coordinates.reduce((finalMin, coord) => {
      const pixelCurrent = this.map.getPixelFromCoordinate(coord);
      const pixelFinal = this.map.getPixelFromCoordinate(
        finalMin || coordinates[0],
      );
      return pixelCurrent[1] <= pixelFinal[1] ? coord : finalMin;
    });
    const yMax = coordinates.reduce((finalMax, coord) => {
      const pixelCurrent = this.map.getPixelFromCoordinate(coord);
      const pixelFinal = this.map.getPixelFromCoordinate(
        finalMax || coordinates[0],
      );
      return pixelCurrent[1] >= pixelFinal[1] ? coord : finalMax;
    });

    // Create four infinite lines through the extremes X and Y and rotate them
    const minVertLine = new LineString([
      [xMin[0], -20037508.342789],
      [xMin[0], 20037508.342789],
    ]);
    minVertLine.rotate(this.map.getView().getRotation(), xMin);
    const maxVertLine = new LineString([
      [xMax[0], -20037508.342789],
      [xMax[0], 20037508.342789],
    ]);
    maxVertLine.rotate(this.map.getView().getRotation(), xMax);
    const minHoriLine = new LineString([
      [-20037508.342789, yMin[1]],
      [20037508.342789, yMin[1]],
    ]);
    minHoriLine.rotate(this.map.getView().getRotation(), yMin);
    const maxHoriLine = new LineString([
      [-20037508.342789, yMax[1]],
      [20037508.342789, yMax[1]],
    ]);
    maxHoriLine.rotate(this.map.getView().getRotation(), yMax);

    // Use intersection points of the four lines to get the extent
    const intersectTopLeft = OverlayOp.intersection(
      parser.read(minVertLine),
      parser.read(minHoriLine),
    );
    const intersectBottomLeft = OverlayOp.intersection(
      parser.read(minVertLine),
      parser.read(maxHoriLine),
    );
    const intersectTopRight = OverlayOp.intersection(
      parser.read(maxVertLine),
      parser.read(minHoriLine),
    );
    const intersectBottomRight = OverlayOp.intersection(
      parser.read(maxVertLine),
      parser.read(maxHoriLine),
    );

    return [
      [intersectTopLeft.getCoordinate().x, intersectTopLeft.getCoordinate().y],
      [
        intersectBottomLeft.getCoordinate().x,
        intersectBottomLeft.getCoordinate().y,
      ],
      [
        intersectTopRight.getCoordinate().x,
        intersectTopRight.getCoordinate().y,
      ],
      [
        intersectBottomRight.getCoordinate().x,
        intersectBottomRight.getCoordinate().y,
      ],
    ];
  }

  // Calculate lines that are vertical or horizontal to a coordinate.
  getVerticalAndHorizontalLines(coordinate, snapCoords) {
    // Draw snaplines when cursor vertically or horizontally aligns with a snap feature.
    // We draw only on vertical and one horizontal line to avoid crowded lines when polygons or lines have a lot of coordinates.
    const halfTol = this.snapTolerance / 2;
    const doubleTol = this.snapTolerance * 2;
    const mousePx = this.map.getPixelFromCoordinate(coordinate);
    const [mouseX, mouseY] = mousePx;
    let vLine;
    let hLine;
    let closerDistanceWithVLine = Infinity;
    let closerDistanceWithHLine = Infinity;
    for (let i = 0; i < snapCoords.length; i += 1) {
      const snapCoord = snapCoords[i];
      const snapPx = this.map.getPixelFromCoordinate(snapCoords[i]);
      const [snapX, snapY] = snapPx;
      const drawVLine = mouseX > snapX - halfTol && mouseX < snapX + halfTol;
      const drawHLine = mouseY > snapY - halfTol && mouseY < snapY + halfTol;

      const distanceWithVLine = Math.abs(mouseX - snapX);
      const distanceWithHLine = Math.abs(mouseY - snapY);

      if (
        (drawVLine && distanceWithVLine > closerDistanceWithVLine) ||
        (drawHLine && distanceWithHLine > closerDistanceWithHLine)
      ) {
        // eslint-disable-next-line no-continue
        continue;
      }

      let newPt;

      if (drawVLine) {
        closerDistanceWithVLine = distanceWithVLine;
        const newY = mouseY + (mouseY < snapY ? -doubleTol : doubleTol);
        newPt = this.map.getCoordinateFromPixel([snapX, newY]);
      } else if (drawHLine) {
        closerDistanceWithHLine = distanceWithHLine;
        const newX = mouseX + (mouseX < snapX ? -doubleTol : doubleTol);
        newPt = this.map.getCoordinateFromPixel([newX, snapY]);
      }

      if (newPt) {
        const lineCoords = [newPt, snapCoord];
        const geom = new LineString(lineCoords);
        const feature = new Feature(geom);

        feature.set(SNAP_FEATURE_TYPE_PROPERTY, VH_LINE_KEY);

        if (drawVLine) {
          vLine = feature;
        }

        if (drawHLine) {
          hLine = feature;
        }
      }
    }

    const lines = [];

    if (hLine) {
      lines.push(hLine);
    }

    if (vLine && vLine !== hLine) {
      lines.push(vLine);
    }

    return lines;
  }

  /**
   * For each segment, we calculate lines that extends it.
   */
  getSegmentLines(coordinate, snapCoords, snapCoordsBefore) {
    const mousePx = this.map.getPixelFromCoordinate(coordinate);
    const doubleTol = this.snapTolerance * 2;
    const lines = [];

    for (let i = 0; i < snapCoords.length; i += 1) {
      if (!snapCoordsBefore[i]) {
        // eslint-disable-next-line no-continue
        continue;
      }
      const snapCoordBefore = snapCoordsBefore[i];
      const snapCoord = snapCoords[i];
      const snapPxBefore = this.map.getPixelFromCoordinate(snapCoordBefore);
      const snapPx = this.map.getPixelFromCoordinate(snapCoord);

      const [snapX] = snapPx;

      // Calculate projected point
      const projMousePx = getProjectedPoint(mousePx, snapPxBefore, snapPx);
      const distance = euclideanDistance(mousePx, projMousePx);

      let newPt;

      if (distance <= this.snapTolerance) {
        const [projMouseX, projMouseY] = projMousePx;
        // lineFunc is undefined when it's a vertical line
        const lineFunc = getEquationOfLine(snapPxBefore, snapPx);
        const newX = projMouseX + (projMouseX < snapX ? -doubleTol : doubleTol);
        if (lineFunc) {
          newPt = this.map.getCoordinateFromPixel([
            newX,
            lineFunc ? lineFunc(newX) : projMouseY,
          ]);
        }
      }

      if (newPt) {
        const lineCoords = [snapCoordBefore, snapCoord, newPt];
        const geom = new LineString(lineCoords);
        const feature = new Feature(geom);
        feature.set(SNAP_FEATURE_TYPE_PROPERTY, SEGMENT_LINE_KEY);
        lines.push(feature);
      }
    }
    return lines;
  }

  /**
   * For each segment, we calculate lines that are perpendicular.
   */
  getOrthoLines(coordinate, snapCoords, snapCoordsBefore) {
    const mousePx = this.map.getPixelFromCoordinate(coordinate);
    const doubleTol = this.snapTolerance * 2;
    const lines = [];

    for (let i = 0; i < snapCoords.length; i += 1) {
      if (!snapCoordsBefore[i]) {
        // eslint-disable-next-line no-continue
        continue;
      }
      const snapCoordBefore = snapCoordsBefore[i];
      const snapCoord = snapCoords[i];
      const snapPxBefore = this.map.getPixelFromCoordinate(snapCoordBefore);
      const snapPx = this.map.getPixelFromCoordinate(snapCoord);

      const orthoLine1 = new LineString([snapPxBefore, snapPx]);
      orthoLine1.rotate((90 * Math.PI) / 180, snapPxBefore);

      const orthoLine2 = new LineString([snapPx, snapPxBefore]);
      orthoLine2.rotate((90 * Math.PI) / 180, snapPx);

      [orthoLine1, orthoLine2].forEach((line) => {
        const [anchorPx, last] = line.getCoordinates();
        const projMousePx = getProjectedPoint(mousePx, anchorPx, last);
        const distance = euclideanDistance(mousePx, projMousePx);

        let newPt;
        if (distance <= this.snapTolerance) {
          const [projMouseX, projMouseY] = projMousePx;
          // lineFunc is undefined when it's a vertical line
          const lineFunc = getEquationOfLine(anchorPx, projMousePx);
          const newX =
            projMouseX + (projMouseX < anchorPx[0] ? -doubleTol : doubleTol);
          if (lineFunc) {
            newPt = this.map.getCoordinateFromPixel([
              newX,
              lineFunc ? lineFunc(newX) : projMouseY,
            ]);
          }
        }

        if (newPt) {
          const coords = [this.map.getCoordinateFromPixel(anchorPx), newPt];
          const geom = new LineString(coords);
          const feature = new Feature(geom);
          feature.set(SNAP_FEATURE_TYPE_PROPERTY, ORTHO_LINE_KEY);
          lines.push(feature);
        }
      });
    }
    return lines;
  }

  /**
   * Draws snap lines by building the extent for
   * a pair of features.
   * @private
   * @param {ol.Coordinate} coordinate Mouse pointer coordinate.
   * @param {Array.<ol.Feature>} features List of features.
   */
  drawSnapLines(coordinate, features) {
    // First get all snap points: neighbouring feature vertices and extent corners
    const snapCoordsBefore = []; // store the direct before point in the coordinate array
    const snapCoords = [];
    const snapCoordsAfter = []; // store the direct next point in the coordinate array

    // Parse the list of coords, we will add the ortho and segment lines which
    // are closest to the users cursor up until the limit of `this.limitGuidesPerFeature`
    const parseCoordList = (coords) => {
      // Check the euclidean distance between coord at index and the
      // included vertices so far. Create an array of `this.limitGuidesPerFeature`
      // closest items.
      const sortedCoords = coords
        .slice()
        .sort(
          (a, b) =>
            euclideanDistance(a, coordinate) - euclideanDistance(b, coordinate),
        );
      const includedCoords = sortedCoords.slice(0, this.limitGuidesPerFeature);

      includedCoords.forEach((index) => {
        snapCoordsBefore.push(coords[index - 1]);
        snapCoords.push(coords[index]);
        snapCoordsAfter.push(coords[index + 1]);
      });
    };

    for (let i = 0; i < features.length; i += 1) {
      const geom = features[i].getGeometry();
      let featureCoord = geom.getCoordinates();

      if (!featureCoord && geom instanceof Circle) {
        featureCoord = geom.getCenter();
      }

      // Polygons initially return a geometry with an empty coordinate array, so we need to catch it
      if (featureCoord?.length) {
        if (geom instanceof Point || geom instanceof Circle) {
          snapCoordsBefore.push();
          snapCoords.push(featureCoord);
          snapCoordsAfter.push();
        } else {
          // Add feature vertices
          // eslint-disable-next-line no-lonely-if
          if (geom instanceof LineString) {
            parseCoordList(featureCoord);
          } else if (geom instanceof Polygon) {
            parseCoordList(featureCoord[0]);
          }

          // Add extent vertices
          // const coords = this.getRotatedExtent(geom, coordinate);
          // for (let j = 0; j < coords.length; j += 1) {
          //   snapCoordsBefore.push();
          //   snapCoords.push(coords[j]);
          //   snapCoordsNext.push();
          // }
        }
      }
    }

    const {
      showVerticalAndHorizontalLines,
      showOrthoLines,
      showSegmentLines,
      snapLinesOrder,
    } = this.properties;

    let lines = [];
    const helpLinesOrdered = [];
    const helpLines = {
      [ORTHO_LINE_KEY]: [],
      [SEGMENT_LINE_KEY]: [],
      [VH_LINE_KEY]: [],
      [CUSTOM_LINE_KEY]: [],
    };

    if (showOrthoLines) {
      helpLines[ORTHO_LINE_KEY] =
        this.getOrthoLines(coordinate, snapCoords, snapCoordsBefore) || [];
    }

    if (showSegmentLines) {
      helpLines[SEGMENT_LINE_KEY] =
        this.getSegmentLines(coordinate, snapCoords, snapCoordsBefore) || [];
    }

    if (showVerticalAndHorizontalLines) {
      helpLines[VH_LINE_KEY] =
        this.getVerticalAndHorizontalLines(coordinate, snapCoords) || [];
    }

    // Add custom lines
    if (this.drawCustomSnapLines) {
      helpLines[CUSTOM_LINE_KEY] =
        this.drawCustomSnapLines(
          coordinate,
          snapCoords,
          snapCoordsBefore,
          snapCoordsAfter,
        ) || [];
    }

    // Add help lines in a defined order.
    snapLinesOrder.forEach((lineType) => {
      helpLinesOrdered.push(...(helpLines[lineType] || []));
    });

    // Remove duplicated lines, comparing their equation using pixels.
    helpLinesOrdered.forEach((lineA) => {
      if (
        !lines.length ||
        !lines.find((lineB) => isSameLines(lineA, lineB, this.map))
      ) {
        lines.push(lineA);
      }
    });

    if (this.lineFilter) {
      lines = this.lineFilter(lines, coordinate);
    }

    // We snap on intersections of lines (distance < this.snapTolerance) or on all the help lines.
    const intersectFeatures = getIntersectedLinesAndPoint(
      coordinate,
      lines,
      this.map,
      this.snapTolerance,
    );

    if (intersectFeatures?.length) {
      intersectFeatures.forEach((feature) => {
        if (feature.getGeometry().getType() === 'Point') {
          this.snapLayer.getSource().addFeature(feature);
        } else {
          this.linesLayer.getSource().addFeature(feature);
        }
      });
    } else {
      this.snapLayer.getSource().addFeatures(lines);
    }
  }

  /**
   * Adds snap points to the snapping layer.
   * @private
   * @param {ol.Coordinate} coordinate cursor coordinate.
   * @param {ol.eaturee} feature Feature to draw the snap points for.
   */
  drawSnapPoints(coordinate, feature) {
    const featCoord = feature.getGeometry().getClosestPoint(coordinate);

    const px = this.map.getPixelFromCoordinate(featCoord);
    let snapCoords = [];

    if (this.properties.useMapUnits) {
      snapCoords = [
        [featCoord[0] - this.properties.snapPointDist, featCoord[1]],
        [featCoord[0] + this.properties.snapPointDist, featCoord[1]],
        [featCoord[0], featCoord[1] - this.properties.snapPointDist],
        [featCoord[0], featCoord[1] + this.properties.snapPointDist],
      ];
    } else {
      const snapPx = [
        [px[0] - this.properties.snapPointDist, px[1]],
        [px[0] + this.properties.snapPointDist, px[1]],
        [px[0], px[1] - this.properties.snapPointDist],
        [px[0], px[1] + this.properties.snapPointDist],
      ];

      for (let j = 0; j < snapPx.length; j += 1) {
        snapCoords.push(this.map.getCoordinateFromPixel(snapPx[j]));
      }
    }

    const snapGeom = new MultiPoint(snapCoords);
    this.snapLayer.getSource().addFeature(new Feature(snapGeom));
  }

  /**
   * @inheritdoc
   */
  activate(silent) {
    super.activate(silent);
    this.snapLayer.setMap(this.map);
    this.linesLayer.setMap(this.map);
    this.map?.addInteraction(this.pointerInteraction);
    this.map?.addInteraction(this.snapInteraction);

    document.getElementById('aux-cb')?.addEventListener('change', (evt) => {
      this.setProperties({
        showSnapLines: evt.target.checked,
        showSnapPoints: !evt.target.checked,
      });
    });

    document.getElementById('dist-cb')?.addEventListener('change', (evt) => {
      this.setProperties({
        showSnapPoints: evt.target.checked,
        showSnapLines: !evt.target.checked,
      });
    });

    document.getElementById('width-input')?.addEventListener('keyup', (evt) => {
      const snapPointDist = parseFloat(evt.target.value);
      if (!Number.isNaN(snapPointDist)) {
        this.setProperties({ snapPointDist });
      }
    });
  }

  /**
   * @inheritdoc
   */
  deactivate(silent) {
    super.deactivate(silent);
    this.snapLayer.setMap(null);
    this.linesLayer.setMap(null);
    this.map?.removeInteraction(this.pointerInteraction);
    this.map?.removeInteraction(this.snapInteraction);
  }
}

export default CadControl;