mapview_interactions_snap.mjs

/**
 * ### mapview.interactions.snap()
 * This module sets up the snap interaction for the mapview.
 * @module /mapview/interactions/snap
 */

/**
 * Sets up the snap interaction for the mapview.
 * @function snap
 * @param {Object} mapview - The mapview object.
 * @param {ol.Map} mapview.Map - The OpenLayers map object.
 * @param {Object} mapview.interaction - The interaction object.
 * @param {boolean|Object} mapview.interaction.snap - The snap interaction configuration.
 * @param {string} [mapview.interaction.snap.layer] - The key of the layer to snap to.
 * @param {ol.layer.Layer} mapview.interaction.layer - The interaction layer.
 * @param {Object} mapview.layers - The map layers object.
 */
export default function (mapview) {
  // The current draw/modify interaction doesn't snap.
  if (!mapview.interaction.snap) return;

  if (mapview.interaction.snap === true) {
    // Assign the interaction layer as snap layer if snap is true.
    mapview.interaction.snap = {
      layer: mapview.interaction.layer.key
    }
  }

  if (typeof (mapview.interaction.snap) === 'string') {
    // Assign the interaction layer as snap layer if snap is a string.
    mapview.interaction.snap = {
      layer: mapview.interaction.snap
    }
  }
  // Assign mapview and interaction to be _this.
  mapview.interaction.snap = {
    /**
     * Removes the snap interaction and related resources.
     */
    remove: () => {
      if (mapview.interaction.layer.featureSource) {
        // Detach tile load event,
        mapview.interaction.layer.featureSource.un('tileloadend', tileloadend)

        // Remove featureSource layer from mapview.Map,
        mapview.Map.removeLayer(mapview.interaction.snap.vectorTileLayer)

        // And clear featureSource.
        mapview.interaction.layer.featureSource.clear()
      }

      mapview.Map.removeInteraction(mapview.interaction.snap.interaction)
    },

    ...mapview.interaction.snap
  }

  // Check if snap layer exists in mapview.layers.
  if (!mapview.layers[mapview.interaction.snap.layer]) {
    console.warn(`Unable to snap to layer:${mapview.interaction.snap.layer} as it is not found in mapview.layers.`)
    return;
  }

  // Assign snap layer from key if defined.
  mapview.interaction.snap.layer &&= mapview.layers[mapview.interaction.snap.layer] || mapview.interaction.layer

  // Assign interaction as snap layer if undefined.
  mapview.interaction.snap.layer ??= mapview.interaction.layer

  mapview.interaction.snap.layer.show()

  /**
   * Callback function for the `tileloadend event of the feature source.
   * Adds features from the loaded tile to the snap source.
   * @param {ol.TileLoadEvent} e - The tile load event.
   */
  function tileloadend(e) {
    const features = e.tile.getFeatures()

    // Try adding features to prevent a crash adding the same feature twice.
    try {
      mapview.interaction.snap.source.addFeatures(features)
    } catch { }
  }

  if (mapview.interaction.snap.layer.featureSource) {
    // Create new Vector source for snap features.
    mapview.interaction.snap.source = new ol.source.Vector()

    // Assign loadend event to MVT layer featureSource.
    mapview.interaction.snap.layer.featureSource.on('tileloadend', tileloadend)

    mapview.interaction.snap.vectorTileLayer = new ol.layer.VectorTile({
      source: mapview.interaction.snap.layer.featureSource,
      opacity: 0
    })

    // Add invisible tile layer for the featureSource to trigger tile loads.
    mapview.Map.addLayer(mapview.interaction.snap.vectorTileLayer)
  } else {
    // Assign vector source as snap source for vector layer.
    mapview.interaction.snap.source = mapview.interaction.snap.layer.L.getSource()
  }

  // Create snap interaction with snap.source.
  mapview.interaction.snap.interaction = new ol.interaction.Snap({
    source: mapview.interaction.snap.source
  })

  // Add snap.interaction to mapview.Map
  mapview.Map.addInteraction(mapview.interaction.snap.interaction)
}