/* eslint-disable no-unused-vars */
import EventType from 'ol/events/EventType'
import VectorEventType from 'ol/source/VectorEventType'
import Interaction from 'ol/interaction/Interaction'
import RBush from 'ol/structs/RBush'
import { boundingExtent, createEmpty } from 'ol/extent'
import {
closestOnCircle,
closestOnSegment,
squaredDistance,
} from 'ol/coordinate'
import { fromCircle } from 'ol/geom/Polygon'
import {
fromUserCoordinate,
getUserProjection,
toUserCoordinate,
} from 'ol/proj'
import { getUid } from 'ol/util'
import { listen, unlistenByKey } from 'ol/events'
/**
* @param {ol.source.Vector.VectorSourceEvent|ol.Collection.CollectionEvent} evt Event.
* @return {ol.Feature} Feature.
*/
function getFeatureFromEvent (evt) {
if (
(evt).feature
) {
return (evt).feature
} else if (
(evt).element
) {
return (
(evt).element
)
}
}
const tempSegment = []
class FeatureSnapper extends Interaction {
/**
* @param {Options} [optOptions] Options.
*/
constructor (optOptions) {
const options = optOptions || {}
super(
/** @type {ol.interaction.InteractionOptions} */ (options)
)
if (!options.viewer) {
console.error('[FeatureSnapper] options.viewer non présent')
return
}
this.viewer = options.viewer
/**
* Liste des sources utilisées
* @type {Array<Object>}
* @private
*/
this.snapSources = {}
/**
* Index pour accéder rapidements aux infos des features
* Extents
* snapSourcesUids
* snapGroups
*/
this.indexedFeatures = {}
/**
* @type {Number}
* @private
*/
this.pixelTolerance_ =
options.pixelTolerance !== undefined ? options.pixelTolerance : 10
/**
* Segment RTree for each layer
* @type {ol.structs.RBush<SegmentData>}
* @private
*/
this.rBush_ = new RBush()
/**
* @const
* @private
* @type {Object<string, function(Array<Array<ol.coordinate.Coordinate>>, ol.geom.Geometry): void>}
*/
this.GEOMETRY_SEGMENTERS_ = {
Point: this.segmentPointGemetry_.bind(this),
LineString: this.segmentLineStringGemetry_.bind(this),
LinearRing: this.segmentLineStringGemetry_.bind(this),
Polygon: this.segmentPolygonGemetry_.bind(this),
MultiPoint: this.segmentMultiPointGemetry_.bind(this),
MultiLineString: this.segmentMultiLineStringGemetry_.bind(this),
MultiPolygon: this.segmentMultiPolygonGemetry_.bind(this),
GeometryCollection: this.segmentGeometryCollectionGemetry_.bind(this),
Circle: this.segmentCircleGemetry_.bind(this),
}
}
/**
* Remove the interaction from its current map and attach it to the new map.
* Subclasses may set up event handlers to get notified about changes to
* the map here.
* @param {ol.PluggableMap} map Map.
*/
setMap (map) {
const currentMap = this.getMap()
if (currentMap) {
this.clear()
}
super.setMap(map)
// TODO : recommencer l'indexation ... indexSource_
}
/**
* @typdef {Object} snapOptions
* @property {Array<String>} snapGroups Noms des groupes (paramétrés via addSource) auquel s'accrocher
* @property {Number} [pixelTolerance] tolérance pixel d'accroche objet
* @property {Function} [filterFn] fonction pour exclure des features du snap
*
* @param {ol.pixel.Pixel} pixel Pixel
* @param {ol.pixel.Pixel} pixel Pixel
* @param {ol.coordinate.Coordinate} pixelCoordinate Coordinate
* @param {snapOptions} snapOptions options for snap operation
* @return {Result|null} Snap result
*/
snapTo (pixel, pixelCoordinate, snapOptions) {
const snapGroups = snapOptions?.snapGroups || []
const pixelTolerance = snapOptions?.pixelTolerance || this.pixelTolerance_
if (snapGroups.length === 0) {
return null
}
const map = this.getMap()
// récupère la zone d'intersection en fonction de la tolerance en poxel
const lowerLeft = map.getCoordinateFromPixel([
pixel[0] - pixelTolerance,
pixel[1] + pixelTolerance,
])
const upperRight = map.getCoordinateFromPixel([
pixel[0] + pixelTolerance,
pixel[1] - pixelTolerance,
])
const box = boundingExtent([lowerLeft, upperRight])
// recupère les segments dans la zone d'intersection pour les groups demandés dans snapOptions
const segments = this.rBush_.getInExtent(box).filter((segmentData) => {
if (!this.viewer.dataLayer.isVisibleFeature({ feature: segmentData.feature, evaluateStyles: false })) { return false }
if (snapOptions.filterFn && !snapOptions.filterFn(segmentData.feature)) { return false }
const featureInfos = this.indexedFeatures[getUid(segmentData.feature)]
return snapGroups.some((group) => featureInfos.groups[group] !== undefined)
})
const segmentsLength = segments.length
if (segmentsLength === 0) {
return null
}
const projection = map.getView().getProjection()
const projectedCoordinate = fromUserCoordinate(pixelCoordinate, projection)
// les fonctions de recherche par vertex ou edge vont set ces variable puis appeler getResult
let closestVertex
let minSquaredDistance = Infinity
const squaredPixelTolerance = pixelTolerance * pixelTolerance
const getResult = () => {
if (closestVertex) {
const vertexPixel = map.getPixelFromCoordinate(closestVertex)
const squaredPixelDistance = squaredDistance(pixel, vertexPixel)
if (squaredPixelDistance <= squaredPixelTolerance) {
return {
vertex: closestVertex,
vertexPixel: [
Math.round(vertexPixel[0]),
Math.round(vertexPixel[1]),
],
}
}
}
return null
}
// regroupe les features utilisable en mode vertex en fonctions des paramètres du groupe
const segmentWithVertexOption = segments.filter((segmentData) => {
const featureInfos = this.indexedFeatures[getUid(segmentData.feature)]
return snapGroups.some((group) => featureInfos.groups[group] !== undefined && featureInfos.groups[group].vertex)
})
const segmentWithVertexOptionLength = segmentWithVertexOption.length
if (segmentWithVertexOptionLength > 0) {
for (let i = 0; i < segmentWithVertexOptionLength; ++i) {
const segmentData = segmentWithVertexOption[i]
if (segmentData.feature.getGeometry().getType() !== 'Circle') {
segmentData.segment.forEach((vertex) => {
const tempVertexCoord = fromUserCoordinate(vertex, projection)
const delta = squaredDistance(projectedCoordinate, tempVertexCoord)
if (delta < minSquaredDistance) {
closestVertex = vertex
minSquaredDistance = delta
}
})
}
}
const result = getResult()
if (result) {
return result
}
}
// regroupe les features utilisable en mode edge en fonctions des paramètres du groupe
const segmentWithEdgeOption = segments.filter((segmentData) => {
const featureInfos = this.indexedFeatures[getUid(segmentData.feature)]
return snapGroups.some((group) => featureInfos.groups[group] !== undefined && featureInfos.groups[group].edge)
})
const segmentWithEdgeOptionLength = segmentWithEdgeOption.length
if (segmentWithEdgeOptionLength > 0) {
for (let i = 0; i < segmentWithEdgeOptionLength; ++i) {
let vertex = null
const segmentData = segmentWithEdgeOption[i]
if (segmentData.feature.getGeometry().getType() === 'Circle') {
let circleGeometry = segmentData.feature.getGeometry()
const userProjection = getUserProjection()
if (userProjection) {
circleGeometry = circleGeometry
.clone()
.transform(userProjection, projection)
}
vertex = toUserCoordinate(
closestOnCircle(
projectedCoordinate,
/** @type {ol.Geom.Circle} */ (circleGeometry)
),
projection
)
} else {
const [segmentStart, segmentEnd] = segmentData.segment
// points have only one coordinate
if (segmentEnd) {
tempSegment[0] = fromUserCoordinate(segmentStart, projection)
tempSegment[1] = fromUserCoordinate(segmentEnd, projection)
vertex = closestOnSegment(projectedCoordinate, tempSegment)
}
}
if (vertex) {
const delta = squaredDistance(projectedCoordinate, vertex)
if (delta < minSquaredDistance) {
closestVertex = vertex
minSquaredDistance = delta
}
}
}
const result = getResult()
if (result) {
return result
}
}
return null
}
/**
* @typdef {Object} snapGroup
* @property {String} name nom du groupe auquel appartient la source
* @property {Boolean} edge accrochage aux ligne
* @property {Boolean} vertex accrochage au extremités
*
* Ajoute une source vecteur a surveiller
* @param {Object} snapSource paramétrage d'une snapSource
* @param {Array<snapGroup>} [snapSource.groups] groupes auquels appartientla source
* @param {ol.source.Vector} [snapSource.source] Source openlayer, attention a ne pas ajouter des sources composée des même features...
*/
addSource (snapSource) {
const newSource = {
...{
groups: [],
featuresListenerKeys: [],
source: /** @type {ol/source/Vector} */ null,
},
...snapSource,
}
const sourceUid = getUid(newSource)
this.snapSources[sourceUid] = newSource
this.indexSource_(sourceUid)
return sourceUid
}
/**
* retire une source de l'indexation
* @param {ol.source.Vector} source source openlayers
*/
removeSource (source) {
Object.entries(this.snapSources).forEach(([sourceId, snapSource]) => {
if (snapSource.source === source) {
this.unIndexSource_(sourceId)
delete this.snapSources[sourceId]
}
})
}
/** indexe la source de données */
indexSource_ (sourceUid) {
const snapSource = this.snapSources[sourceUid]
const features = /** @type {Array<ol.Feature.default>} */ (
snapSource.source.getFeatures()
)
snapSource.featuresListenerKeys.push(
listen(
snapSource.source,
VectorEventType.ADDFEATURE,
(evt) => { this.handleFeatureAdd_(evt, sourceUid) },
this
),
listen(
snapSource.source,
VectorEventType.REMOVEFEATURE,
(evt) => { this.handleFeatureRemove_(evt, sourceUid) },
this
)
)
features.forEach((feature) => this.forEachFeatureAdd_(feature, sourceUid))
// console.log('indexed')
}
unIndexSource_ (sourceUid) {
const snapSource = this.snapSources[sourceUid]
const features = /** @type {Array<ol.Feature>} */ (
snapSource.source.getFeatures()
)
snapSource.featuresListenerKeys.forEach(unlistenByKey)
snapSource.featuresListenerKeys.length = 0
features.forEach(this.forEachFeatureRemove_.bind(this))
}
/** Nettoit les sources ecoutés
* @param {boolean} [clearSystemSnap=false] doit-on effacer les sources systeme?
*/
clear (clearSystemSnap = false) {
Object.entries(this.snapSources).forEach(([sourceId, snapSource]) => {
if (!snapSource.groups.some((group) => group.name === 'KMAPV') || clearSystemSnap) {
this.unIndexSource_(sourceId)
delete this.snapSources[sourceId]
}
})
}
/**
* Add a feature to the collection of features that we may snap to.
* @param {ol.Feature} feature Feature.
* @param {boolean} [optListen] Whether to listen to the feature change or not
* Defaults to `true`.
* @api
*/
addFeature (feature, sourceUid, optListen) {
const register = optListen !== undefined ? optListen : true
const featureUid = getUid(feature)
const geometry = feature.getGeometry()
if (geometry) {
const segmenter = this.GEOMETRY_SEGMENTERS_[geometry.getType()]
if (segmenter) {
const groups = {}
this.snapSources[sourceUid].groups.forEach((group) => {
groups[group.name] = {
edge: group.edge,
vertex: group.vertex,
}
})
// indexe les étendues et l'id de la source pour chaque feature (d'ou le fait qu'il ne faut pas qu'une feature soit sur deux sources indexées)
this.indexedFeatures[featureUid] = {
extent: geometry.getExtent(
createEmpty()
),
sourceUid,
groups,
}
const segments = /** @type {Array<Array<ol.coordinate.Coordinate>>} */ ([])
segmenter(segments, geometry)
if (segments.length === 1) {
this.rBush_.insert(boundingExtent(segments[0]), {
feature,
segment: segments[0],
})
} else if (segments.length > 1) {
const extents = segments.map((s) => boundingExtent(s))
const segmentsData = segments.map((segment) => ({
feature,
segment,
}))
this.rBush_.load(extents, segmentsData)
}
}
}
if (register) {
this.snapSources[sourceUid].featuresListenerKeys[featureUid] = listen(
feature,
EventType.CHANGE,
(evt) => this.handleFeatureChange_(evt, sourceUid),
this
)
}
}
/**
* @param {ol.Feature} feature Feature.
* @private
*/
forEachFeatureAdd_ (feature, sourceUid) {
this.addFeature(feature, sourceUid)
}
/**
* @param {ol.source.Vector.VectorSourceEvent|ol.Collection.CollectionEvent} evt Event.
* @private
*/
handleFeatureAdd_ (evt, sourceUid) {
const feature = getFeatureFromEvent(evt)
this.addFeature(feature, sourceUid)
}
/**
* Remove a feature from the collection of features that we may snap to.
* @param {ol.Feature} feature Feature
* @param {boolean} [optUnlisten] Whether to unlisten to the feature change
* or not. Defaults to `true`.
* @api
*/
removeFeature (feature, optUnlisten) {
const unregister = optUnlisten !== undefined ? optUnlisten : true
const featureUid = getUid(feature)
if (!this.indexedFeatures[featureUid]) { return }
const { extent, sourceUid } = this.indexedFeatures[featureUid]
if (this.indexedFeatures[featureUid]) {
const rBush = this.rBush_
const nodesToRemove = []
rBush.forEachInExtent(extent, function (node) {
if (feature === node.feature) {
nodesToRemove.push(node)
}
})
for (let i = nodesToRemove.length - 1; i >= 0; --i) {
rBush.remove(nodesToRemove[i])
}
}
if (unregister) {
unlistenByKey(this.snapSources[sourceUid].featuresListenerKeys)
delete this.snapSources[sourceUid].featuresListenerKeys[featureUid]
delete this.indexedFeatures[featureUid]
}
}
/**
* @param {ol.Feature} feature Feature.
* @private
*/
forEachFeatureRemove_ (feature) {
this.removeFeature(feature)
}
/**
* @param {ol.source.Vector.VectorSourceEvent|ol.Collection.CollectionEvent} evt Event.
* @private
*/
handleFeatureRemove_ (evt) {
const feature = getFeatureFromEvent(evt)
this.removeFeature(feature)
}
/**
* @param {ol.Feature} feature Feature
* @private
*/
updateFeature_ (feature, sourceUid) {
const featureUid = getUid(feature)
// const { sourceUid } = this.indexedFeatures[featureUid]
if (this.indexedFeatures[featureUid]) { this.removeFeature(feature, false) }
this.addFeature(feature, sourceUid, false)
}
/**
* @param {ol.events.Event} evt Event.
* @private
*/
handleFeatureChange_ (evt, sourceUid) {
const feature = /** @type {ol.Feature} */ (evt.target)
this.updateFeature_(feature, sourceUid)
}
/**
* @param {Array<Array<ol.coordinate.Coordinate>>} segments Segments
* @param {ol.geom.Circle} geometry Geometry.
* @private
*/
segmentCircleGemetry_ (segments, geometry) {
const projection = this.getMap().getView().getProjection()
let circleGeometry = geometry
const userProjection = getUserProjection()
if (userProjection) {
circleGeometry = /** @type {ol.geom.Circle} */ (
circleGeometry.clone().transform(userProjection, projection)
)
}
const polygon = fromCircle(circleGeometry)
if (userProjection) {
polygon.transform(projection, userProjection)
}
const coordinates = polygon.getCoordinates()[0]
for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) {
segments.push(coordinates.slice(i, i + 2))
}
}
/**
* @param {Array<Array<ol.coordinate.Coordinate>>} segments Segments
* @param {ol.geom.GeometryCollection} geometry Geometry.
* @private
*/
segmentGeometryCollectionGemetry_ (segments, geometry) {
const geometries = geometry.getGeometriesArray()
for (let i = 0; i < geometries.length; ++i) {
const segmenter = this.GEOMETRY_SEGMENTERS_[geometries[i].getType()]
if (segmenter) {
segmenter(segments, geometries[i])
}
}
}
/**
* @param {Array<Array<ol.coordinate.Coordinate>>} segments Segments
* @param {ol.geom.LineString} geometry Geometry.
* @private
*/
segmentLineStringGemetry_ (segments, geometry) {
const coordinates = geometry.getCoordinates()
for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) {
segments.push(coordinates.slice(i, i + 2))
}
}
/**
* @param {Array<Array<ol.coordinateCoordinate>>} segments Segments
* @param {ol.geom.MultiLineString} geometry Geometry.
* @private
*/
segmentMultiLineStringGemetry_ (segments, geometry) {
const lines = geometry.getCoordinates()
for (let j = 0, jj = lines.length; j < jj; ++j) {
const coordinates = lines[j]
for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) {
segments.push(coordinates.slice(i, i + 2))
}
}
}
/**
* @param {Array<Array<ol.coordinateCoordinate>>} segments Segments
* @param {ol.geom.MultiPoint} geometry Geometry.
* @private
*/
segmentMultiPointGemetry_ (segments, geometry) {
geometry.getCoordinates().forEach((point) => {
segments.push([point])
})
}
/**
* @param {Array<Array<ol.coordinate.Coordinate>>} segments Segments
* @param {ol.geom.MultiPolygon} geometry Geometry.
* @private
*/
segmentMultiPolygonGemetry_ (segments, geometry) {
const polygons = geometry.getCoordinates()
for (let k = 0, kk = polygons.length; k < kk; ++k) {
const rings = polygons[k]
for (let j = 0, jj = rings.length; j < jj; ++j) {
const coordinates = rings[j]
for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) {
segments.push(coordinates.slice(i, i + 2))
}
}
}
}
/**
* @param {Array<Array<ol.coordinateCoordinate>>} segments Segments
* @param {ol.geom.Point} geometry Geometry.
* @private
*/
segmentPointGemetry_ (segments, geometry) {
segments.push([geometry.getCoordinates()])
}
/**
* @param {Array<Array<olcoordinateCoordinate>>} segments Segments
* @param {ol.geom.Polygon} geometry Geometry.
* @privateimport { Interaction } from 'ol/interaction/Interaction';
*/
segmentPolygonGemetry_ (segments, geometry) {
const rings = geometry.getCoordinates()
for (let j = 0, jj = rings.length; j < jj; ++j) {
const coordinates = rings[j]
for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) {
segments.push(coordinates.slice(i, i + 2))
}
}
}
}
export default FeatureSnapper