/* Copyright (c) 2016 Jean-Marc VIGLINO,
released under the CeCILL-B license (French BSD license)
(http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
import ol_interaction_Interaction from 'ol/interaction/Interaction.js'
import ol_style_Style from 'ol/style/Style.js'
import ol_style_Stroke from 'ol/style/Stroke.js'
import ol_source_Vector from 'ol/source/Vector.js'
import ol_style_Fill from 'ol/style/Fill.js'
import ol_style_Circle from 'ol/style/Circle.js'
import ol_layer_Vector from 'ol/layer/Vector.js'
import ol_geom_Point from 'ol/geom/Point.js'
import ol_Feature from 'ol/Feature.js'
import ol_geom_LineString from 'ol/geom/LineString.js'
import {ol_coordinate_dist2d} from "../geom/GeomUtils.js";
import '../geom/LineStringSplitAt.js'
import ol_ext_element from '../util/element.js'
/** Interaction split interaction for splitting feature geometry
* @constructor
* @extends {ol_interaction_Interaction}
* @fires beforesplit, aftersplit, pointermove
* @param {*}
* @param {ol.source.Vector|Array
} [options.sources] a list of source to split (configured with useSpatialIndex set to true), if none use map visible layers.
* @param {ol.Collection.} options.features collection of feature to split (instead of a list of sources)
* @param {integer} options.snapDistance distance (in px) to snap to an object, default 25px
* @param {string|undefined} options.cursor cursor name to display when hovering an objet
* @param {function|undefined} options.filter a filter that takes a feature and return true if it can be clipped, default always split.
* @param ol_style_Style | Array | false | undefined} options.featureStyle Style for the selected features, choose false if you don't want feature selection. By default the default edit style is used.
* @param {ol_style_Style | Array | undefined} options.sketchStyle Style for the sektch features.
* @param {function|undefined} options.tolerance Distance between the calculated intersection and a vertex on the source geometry below which the existing vertex will be used for the split. Default is 1e-10.
*/
var ol_interaction_Split = class olinteractionSplit extends ol_interaction_Interaction {
constructor(options) {
if (!options)
options = {}
super({
handleEvent: function (e) {
switch (e.type) {
case "singleclick":
return this.handleDownEvent(e)
case "pointermove":
return this.handleMoveEvent(e)
default:
return true
}
//return true;
}
})
// Snap distance (in px)
this.snapDistance_ = options.snapDistance || 25
// Split tolerance between the calculated intersection and the geometry
this.tolerance_ = options.tolerance || 1e-10
// Cursor
this.cursor_ = options.cursor
// List of source to split
this.setSources(options.sources)
if (options.features) {
if (!this.sources_) this.sources_ = [];
this.sources_.push(new ol_source_Vector({ features: options.features }))
}
// Get all features candidate
this.filterSplit_ = options.filter || function () { return true }
// Default style
var white = [255, 255, 255, 1]
var blue = [0, 153, 255, 1]
var width = 3
var fill = new ol_style_Fill({ color: 'rgba(255,255,255,0.4)' })
var stroke = new ol_style_Stroke({
color: '#3399CC',
width: 1.25
})
var sketchStyle = [
new ol_style_Style({
image: new ol_style_Circle({
fill: fill,
stroke: stroke,
radius: 5
}),
fill: fill,
stroke: stroke
})
]
var featureStyle = [
new ol_style_Style({
stroke: new ol_style_Stroke({
color: white,
width: width + 2
})
}),
new ol_style_Style({
image: new ol_style_Circle({
radius: 2 * width,
fill: new ol_style_Fill({
color: blue
}),
stroke: new ol_style_Stroke({
color: white,
width: width / 2
})
}),
stroke: new ol_style_Stroke({
color: blue,
width: width
})
}),
]
// Custom style
if (options.sketchStyle)
sketchStyle = options.sketchStyle instanceof Array ? options.sketchStyle : [options.sketchStyle]
if (options.featureStyle)
featureStyle = options.featureStyle instanceof Array ? options.featureStyle : [options.featureStyle]
// Create a new overlay for the sketch
this.overlayLayer_ = new ol_layer_Vector({
source: new ol_source_Vector({
useSpatialIndex: false
}),
name: 'Split overlay',
displayInLayerSwitcher: false,
style: function (f) {
if (f._sketch_)
return sketchStyle
else
return featureStyle
}
})
}
/**
* Remove the interaction from its current map, if any, and attach it to a new
* map, if any. Pass `null` to just remove the interaction from the current map.
* @param {ol.Map} map Map.
* @api stable
*/
setMap(map) {
if (this.getMap()) {
this.getMap().removeLayer(this.overlayLayer_)
}
super.setMap(map)
this.overlayLayer_.setMap(map)
}
/** Get sources to split features in
* @return {Array}
*/
getSources() {
if (!this.sources_ && this.getMap()) {
var sources = []
var getSources = function (layers) {
layers.forEach(function (layer) {
if (layer.getVisible()) {
if (layer.getSource && layer.getSource() instanceof ol_source_Vector) {
sources.unshift(layer.getSource())
} else if (layer.getLayers) {
getSources(layer.getLayers())
}
}
})
}
getSources(this.getMap().getLayers())
return sources
}
return this.sources_ || []
}
/** Set sources to split features in
* @param {ol.source.Vector|Array|boolean} [sources] if not defined get all map vector sources
*/
setSources(sources) {
this.sources_ = sources ? (sources instanceof Array ? sources || false : [sources]) : false
}
/** Get closest feature at pixel
* @param {ol.Pixel}
* @return {ol.feature}
* @private
*/
getClosestFeature(e) {
var source, f, c, g, d = this.snapDistance_ + 1
// Look for closest point in the sources
this.getSources().forEach(function (si) {
var fi = si.getClosestFeatureToCoordinate(e.coordinate)
if (fi && fi.getGeometry().splitAt) {
var ci = fi.getGeometry().getClosestPoint(e.coordinate)
var gi = new ol_geom_LineString([e.coordinate, ci])
var di = gi.getLength() / e.frameState.viewState.resolution
if (di < d) {
source = si
d = di
f = fi
g = gi
c = ci
}
}
})
// Snap ?
if (d > this.snapDistance_) {
return false
} else {
// Snap to node
var coord = this.getNearestCoord(c, f.getGeometry().getCoordinates())
var p = this.getMap().getPixelFromCoordinate(coord)
if (ol_coordinate_dist2d(e.pixel, p) < this.snapDistance_) {
c = coord
}
//
return { source: source, feature: f, coord: c, link: g }
}
}
/** Get nearest coordinate in a list
* @param {ol.coordinate} pt the point to find nearest
* @param {Array} coords list of coordinates
* @return {ol.coordinate} the nearest coordinate in the list
*/
getNearestCoord(pt, coords) {
var d, dm = Number.MAX_VALUE, p0
for (var i = 0; i < coords.length; i++) {
d = ol_coordinate_dist2d(pt, coords[i])
if (d < dm) {
dm = d
p0 = coords[i]
}
}
return p0
}
/**
* @param {ol.MapBrowserEvent} evt Map browser event.
* @return {boolean} `true` to start the drag sequence.
*/
handleDownEvent(evt) {
// Something to split ?
var current = this.getClosestFeature(evt)
if (current) {
var self = this
self.overlayLayer_.getSource().clear()
var split = current.feature.getGeometry().splitAt(current.coord, this.tolerance_)
var i
if (split.length > 1) {
var tosplit = []
for (i = 0; i < split.length; i++) {
var f = current.feature.clone()
f.setGeometry(split[i])
tosplit.push(f)
}
self.dispatchEvent({ type: 'beforesplit', original: current.feature, features: tosplit })
current.source.dispatchEvent({ type: 'beforesplit', original: current.feature, features: tosplit })
current.source.removeFeature(current.feature)
for (i = 0; i < tosplit.length; i++) {
current.source.addFeature(tosplit[i])
}
self.dispatchEvent({ type: 'aftersplit', original: current.feature, features: tosplit })
current.source.dispatchEvent({ type: 'aftersplit', original: current.feature, features: tosplit })
}
}
return false
}
/**
* @param {ol.MapBrowserEvent} evt Event.
*/
handleMoveEvent(e) {
var map = e.map
this.overlayLayer_.getSource().clear()
var current = this.getClosestFeature(e)
if (current && this.filterSplit_(current.feature)) {
var p, l
// Draw sketch
this.overlayLayer_.getSource().addFeature(current.feature)
p = new ol_Feature(new ol_geom_Point(current.coord))
p._sketch_ = true
this.overlayLayer_.getSource().addFeature(p)
//
l = new ol_Feature(current.link)
l._sketch_ = true
this.overlayLayer_.getSource().addFeature(l)
// move event
this.dispatchEvent({
type: 'pointermove',
coordinate: e.coordinate,
frameState: e.frameState,
originalEvent: e.originalEvent,
map: e.map,
pixel: e.pixel,
feature: current.feature,
linkGeometry: current.link
})
} else {
this.dispatchEvent(e)
}
var element = map.getTargetElement()
if (this.cursor_) {
if (current) {
if (element.style.cursor != this.cursor_) {
this.previousCursor_ = element.style.cursor
ol_ext_element.setCursor(element, this.cursor_)
}
} else if (this.previousCursor_ !== undefined) {
ol_ext_element.setCursor(element, this.previousCursor_)
this.previousCursor_ = undefined
}
}
}
}
export default ol_interaction_Split