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