/* 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_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_interaction_Interaction from 'ol/interaction/Interaction.js' import {ol_coordinate_dist2d, ol_coordinate_equal} from "../geom/GeomUtils.js"; import {boundingExtent as ol_extent_boundingExtent} from 'ol/extent.js' import {buffer as ol_extent_buffer} from 'ol/extent.js' import {altKeyOnly as ol_events_condition_altKeyOnly} from 'ol/events/condition.js' import {primaryAction as ol_events_condition_primaryAction} from 'ol/events/condition.js' import {always as ol_events_condition_always} from 'ol/events/condition.js' import ol_ext_element from '../util/element.js' import '../geom/LineStringSplitAt.js' /** Interaction for modifying feature geometries. Similar to the core ol/interaction/Modify. * The interaction is more suitable to use to handle feature modification: only features concerned * by the modification are passed to the events (instead of all feature with ol/interaction/Modify) * - the modifystart event is fired before the feature is modified (no points still inserted) * - the modifyend event is fired after the modification * - it fires a modifying event * @constructor * @extends {ol_interaction_Interaction} * @fires modifystart * @fires modifying * @fires modifyend * @fires select * @param {*} options * @param {ol.source.Vector} options.source a source to modify (configured with useSpatialIndex set to true) * @param {ol.source.Vector|Array} options.sources a list of source to modify (configured with useSpatialIndex set to true) * @param {ol.Collection.} options.features collection of feature to modify * @param {integer} options.pixelTolerance Pixel tolerance for considering the pointer close enough to a segment or vertex for editing. Default is 10. * @param {function|undefined} options.filter a filter that takes a feature and return true if it can be modified, default always true. * @param {ol.style.Style | Array | undefined} options.style Style for the sketch features. * @param {ol.EventsConditionType | undefined} options.condition A function that takes an ol.MapBrowserEvent and returns a boolean to indicate whether that event will be considered to add or move a vertex to the sketch. Default is ol.events.condition.primaryAction. * @param {ol.EventsConditionType | undefined} options.deleteCondition A function that takes an ol.MapBrowserEvent and returns a boolean to indicate whether that event should be handled. By default, ol.events.condition.singleClick with ol.events.condition.altKeyOnly results in a vertex deletion. * @param {ol.EventsConditionType | undefined} options.insertVertexCondition A function that takes an ol.MapBrowserEvent and returns a boolean to indicate whether a new vertex can be added to the sketch features. Default is ol.events.condition.always * @param {boolean} options.wrapX Wrap the world horizontally on the sketch overlay, default false */ var ol_interaction_ModifyFeature = class olinteractionModifyFeature extends ol_interaction_Interaction { constructor(options) { options = options || {} var dragging, modifying super({ handleEvent: function (e) { switch (e.type) { case 'pointerdown': { dragging = this.handleDownEvent(e) modifying = dragging || this._deleteCondition(e) return !dragging } case 'pointerup': { dragging = false return this.handleUpEvent(e) } case 'pointerdrag': { if (dragging) return this.handleDragEvent(e) else return true } case 'pointermove': { if (!dragging){ return this.handleMoveEvent(e) } else { return false } } case 'singleclick': case 'click': { // Prevent click when modifying return !modifying } default: return true } } }) // Snap distance (in px) this.snapDistance_ = options.pixelTolerance || 10 // Split tolerance between the calculated intersection and the geometry this.tolerance_ = 1e-10 // Cursor this.cursor_ = options.cursor // List of source to split this.sources_ = options.sources ? (options.sources instanceof Array) ? options.sources : [options.sources] : [] if (options.source) { this.sources_.push(options.source) } if (options.features) { this.sources_.push(new ol_source_Vector({ features: options.features })) } // Get all features candidate this.filterSplit_ = options.filter || function () { return true } this._condition = options.condition || ol_events_condition_primaryAction this._deleteCondition = options.deleteCondition || ol_events_condition_altKeyOnly this._insertVertexCondition = options.insertVertexCondition || ol_events_condition_always // Default style var sketchStyle = function () { return [new ol_style_Style({ image: new ol_style_Circle({ radius: 6, fill: new ol_style_Fill({ color: [0, 153, 255, 1] }), stroke: new ol_style_Stroke({ color: '#FFF', width: 1.25 }) }) }) ] } // Custom style if (options.style) { if (typeof (options.style) === 'function') { sketchStyle = options.style } else { sketchStyle = function () { return options.style } } } // Create a new overlay for the sketch this.overlayLayer_ = new ol_layer_Vector({ source: new ol_source_Vector({ useSpatialIndex: false }), name: 'Modify overlay', displayInLayerSwitcher: false, style: sketchStyle, wrapX: options.wrapX }) } /** * 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) } /** * Activate or deactivate the interaction + remove the sketch. * @param {boolean} active. * @api stable */ setActive(active) { super.setActive(active) if (this.overlayLayer_) this.overlayLayer_.getSource().clear() } /** Change the filter function * @param {function|undefined} options.filter a filter that takes a feature and return true if it can be modified, default always true. */ setFilter(filter) { if (typeof (filter) === 'function') this.filterSplit_ = filter else if (filter === undefined) this.filterSplit_ = function () { return true } } /** Get closest feature at pixel * @param {ol.Pixel} * @return {*} * @private */ getClosestFeature(e) { var f, c, d = this.snapDistance_ + 1 for (var i = 0; i < this.sources_.length; i++) { var source = this.sources_[i] f = source.getClosestFeatureToCoordinate(e.coordinate) if (f && this.filterSplit_(f)) { var ci = f.getGeometry().getClosestPoint(e.coordinate) var di = ol_coordinate_dist2d(e.coordinate, ci) / e.frameState.viewState.resolution if (di < d) { d = di c = ci } break } } if (d > this.snapDistance_) { if (this.currentFeature) this.dispatchEvent({ type: 'select', selected: [], deselected: [this.currentFeature] }) this.currentFeature = null return false } else { // Snap to node var coord = this.getNearestCoord(c, f.getGeometry()) if (coord) { coord = coord.coord var p = this.getMap().getPixelFromCoordinate(coord) if (ol_coordinate_dist2d(e.pixel, p) < this.snapDistance_) { c = coord } // if (this.currentFeature !== f) this.dispatchEvent({ type: 'select', selected: [f], deselected: [this.currentFeature] }) this.currentFeature = f return { source: source, feature: f, coord: c } } } } /** Get nearest coordinate in a list * @param {ol.coordinate} pt the point to find nearest * @param {ol.geom} coords list of coordinates * @return {*} the nearest point with a coord (projected point), dist (distance to the geom), ring (if Polygon) */ getNearestCoord(pt, geom) { var i, l, p, p0, dm switch (geom.getType()) { case 'Point': { return { coord: geom.getCoordinates(), dist: ol_coordinate_dist2d(geom.getCoordinates(), pt) } } case 'MultiPoint': { return this.getNearestCoord(pt, new ol_geom_LineString(geom.getCoordinates())) } case 'LineString': case 'LinearRing': { var d dm = Number.MAX_VALUE var coords = geom.getCoordinates() for (i = 0; i < coords.length; i++) { d = ol_coordinate_dist2d(pt, coords[i]) if (d < dm) { dm = d p0 = coords[i] } } return { coord: p0, dist: dm } } case 'MultiLineString': { var lstring = geom.getLineStrings() p0 = false, dm = Number.MAX_VALUE for (i = 0; l = lstring[i]; i++) { p = this.getNearestCoord(pt, l) if (p && p.dist < dm) { p0 = p dm = p.dist p0.ring = i } } return p0 } case 'Polygon': { var lring = geom.getLinearRings() p0 = false dm = Number.MAX_VALUE for (i = 0; l = lring[i]; i++) { p = this.getNearestCoord(pt, l) if (p && p.dist < dm) { p0 = p dm = p.dist p0.ring = i } } return p0 } case 'MultiPolygon': { var poly = geom.getPolygons() p0 = false dm = Number.MAX_VALUE for (i = 0; l = poly[i]; i++) { p = this.getNearestCoord(pt, l) if (p && p.dist < dm) { p0 = p dm = p.dist p0.poly = i } } return p0 } case 'GeometryCollection': { var g = geom.getGeometries() p0 = false dm = Number.MAX_VALUE for (i = 0; l = g[i]; i++) { p = this.getNearestCoord(pt, l) if (p && p.dist < dm) { p0 = p dm = p.dist p0.geom = i } } return p0 } default: return false } } /** Get arcs concerned by a modification * @param {ol.geom} geom the geometry concerned * @param {ol.coordinate} coord pointed coordinates */ getArcs(geom, coord) { var arcs = false var coords, i, s, l, g switch (geom.getType()) { case 'Point': { if (ol_coordinate_equal(coord, geom.getCoordinates())) { arcs = { geom: geom, type: geom.getType(), coord1: [], coord2: [], node: true } } break } case 'MultiPoint': { coords = geom.getCoordinates() for (i = 0; i < coords.length; i++) { if (ol_coordinate_equal(coord, coords[i])) { arcs = { geom: geom, type: geom.getType(), index: i, coord1: [], coord2: [], node: true } break } } break } case 'LinearRing': case 'LineString': { var p = geom.getClosestPoint(coord) if (ol_coordinate_dist2d(p, coord) < 1.5 * this.tolerance_) { var split // Split the line in two if (geom.getType() === 'LinearRing') { g = new ol_geom_LineString(geom.getCoordinates()) split = g.splitAt(coord, this.tolerance_) } else { split = geom.splitAt(coord, this.tolerance_) } // If more than 2 if (split.length > 2) { coords = split[1].getCoordinates() for (i = 2; s = split[i]; i++) { var c = s.getCoordinates() c.shift() coords = coords.concat(c) } split = [split[0], new ol_geom_LineString(coords)] } // Split in two if (split.length === 2) { var c0 = split[0].getCoordinates() var c1 = split[1].getCoordinates() var nbpt = c0.length + c1.length - 1 c0.pop() c1.shift() arcs = { geom: geom, type: geom.getType(), coord1: c0, coord2: c1, node: (geom.getCoordinates().length === nbpt), closed: false } } else if (split.length === 1) { s = split[0].getCoordinates() var start = ol_coordinate_equal(s[0], coord) var end = ol_coordinate_equal(s[s.length - 1], coord) // Move first point if (start) { s.shift() if (end) s.pop() arcs = { geom: geom, type: geom.getType(), coord1: [], coord2: s, node: true, closed: end } } else if (end) { // Move last point s.pop() arcs = { geom: geom, type: geom.getType(), coord1: s, coord2: [], node: true, closed: false } } } } break } case 'MultiLineString': { var lstring = geom.getLineStrings() for (i = 0; l = lstring[i]; i++) { arcs = this.getArcs(l, coord) if (arcs) { arcs.geom = geom arcs.type = geom.getType() arcs.lstring = i break } } break } case 'Polygon': { var lring = geom.getLinearRings() for (i = 0; l = lring[i]; i++) { arcs = this.getArcs(l, coord) if (arcs) { arcs.geom = geom arcs.type = geom.getType() arcs.index = i break } } break } case 'MultiPolygon': { var poly = geom.getPolygons() for (i = 0; l = poly[i]; i++) { arcs = this.getArcs(l, coord) if (arcs) { arcs.geom = geom arcs.type = geom.getType() arcs.poly = i break } } break } case 'GeometryCollection': { g = geom.getGeometries() for (i = 0; l = g[i]; i++) { arcs = this.getArcs(l, coord) if (arcs) { arcs.geom = geom arcs.g = i arcs.typeg = arcs.type arcs.type = geom.getType() break } } break } default: { console.error('ol/interaction/ModifyFeature ' + geom.getType() + ' not supported!') break } } return arcs } /** * @param {ol.MapBrowserEvent} evt Map browser event. * @return {boolean} `true` to start the drag sequence. */ handleDownEvent(evt) { if (!this.getActive()) return false // Something to move ? var current = this.getClosestFeature(evt) if (current && (this._condition(evt) || this._deleteCondition(evt))) { var features = [] this.arcs = [] // Get features concerned this.sources_.forEach(function (s) { var extent = ol_extent_buffer(ol_extent_boundingExtent([current.coord]), this.tolerance_) features = features.concat(features, s.getFeaturesInExtent(extent)) }.bind(this)) // Get arcs concerned this._modifiedFeatures = [] features.forEach(function (f) { var a = this.getArcs(f.getGeometry(), current.coord) if (a) { if (this._insertVertexCondition(evt) || a.node) { a.feature = f this._modifiedFeatures.push(f) this.arcs.push(a) } } }.bind(this)) if (this._modifiedFeatures.length) { if (this._deleteCondition(evt)) { return !this._removePoint(current, evt) } else { this.dispatchEvent({ type: 'modifystart', coordinate: current.coord, originalEvent: evt.originalEvent, features: this._modifiedFeatures }) this.handleDragEvent({ coordinate: current.coord, originalEvent: evt.originalEvent }) return true } } else { return true } } else { return false } } /** Get modified features * @return {Array} list of modified features */ getModifiedFeatures() { return this._modifiedFeatures || [] } /** Removes the vertex currently being pointed. */ removePoint() { this._removePoint({}, {}) } /** * @private */ _getModification(a) { var coords = a.coord1.concat(a.coord2) switch (a.type) { case 'LineString': { if (a.closed) coords.push(coords[0]) if (coords.length > 1) { if (a.geom.getCoordinates().length != coords.length) { a.coords = coords return true } } break } case 'MultiLineString': { if (a.closed) coords.push(coords[0]) if (coords.length > 1) { var c = a.geom.getCoordinates() if (c[a.lstring].length != coords.length) { c[a.lstring] = coords a.coords = c return true } } break } case 'Polygon': { if (a.closed) coords.push(coords[0]) if (coords.length > 3) { c = a.geom.getCoordinates() if (c[a.index].length != coords.length) { c[a.index] = coords a.coords = c return true } } break } case 'MultiPolygon': { if (a.closed) coords.push(coords[0]) if (coords.length > 3) { c = a.geom.getCoordinates() if (c[a.poly][a.index].length != coords.length) { c[a.poly][a.index] = coords a.coords = c return true } } break } case 'GeometryCollection': { a.type = a.typeg var geom = a.geom var geoms = geom.getGeometries() a.geom = geoms[a.g] var found = this._getModification(a) // Restore current arc geom.setGeometries(geoms) a.geom = geom a.type = 'GeometryCollection' return found } default: { //console.error('ol/interaction/ModifyFeature '+a.type+' not supported!'); break } } return false } /** Removes the vertex currently being pointed. * @private */ _removePoint(current, evt) { if (!this.arcs) return false this.overlayLayer_.getSource().clear() var found = false // Get all modifications this.arcs.forEach(function (a) { found = found || this._getModification(a) }.bind(this)) // Almost one point is removed if (found) { this.dispatchEvent({ type: 'modifystart', coordinate: current.coord, originalEvent: evt.originalEvent, features: this._modifiedFeatures }) this.arcs.forEach(function (a) { if (a.geom.getType() === 'GeometryCollection') { if (a.coords) { var geoms = a.geom.getGeometries() geoms[a.g].setCoordinates(a.coords) a.geom.setGeometries(geoms) } } else { if (a.coords) a.geom.setCoordinates(a.coords) } }.bind(this)) this.dispatchEvent({ type: 'modifyend', coordinate: current.coord, originalEvent: evt.originalEvent, features: this._modifiedFeatures }) } this.arcs = [] return found } /** * @private */ handleUpEvent(e) { if (!this.getActive()) return false if (!this.arcs || !this.arcs.length) return true this.overlayLayer_.getSource().clear() this.dispatchEvent({ type: 'modifyend', coordinate: e.coordinate, originalEvent: e.originalEvent, features: this._modifiedFeatures }) this.arcs = [] return true } /** * @private */ setArcCoordinates(a, coords) { var c switch (a.type) { case 'Point': { a.geom.setCoordinates(coords[0]) break } case 'MultiPoint': { c = a.geom.getCoordinates() c[a.index] = coords[0] a.geom.setCoordinates(c) break } case 'LineString': { a.geom.setCoordinates(coords) break } case 'MultiLineString': { c = a.geom.getCoordinates() c[a.lstring] = coords a.geom.setCoordinates(c) break } case 'Polygon': { c = a.geom.getCoordinates() c[a.index] = coords a.geom.setCoordinates(c) break } case 'MultiPolygon': { c = a.geom.getCoordinates() c[a.poly][a.index] = coords a.geom.setCoordinates(c) break } case 'GeometryCollection': { a.type = a.typeg var geom = a.geom var geoms = geom.getGeometries() a.geom = geoms[a.g] this.setArcCoordinates(a, coords) geom.setGeometries(geoms) a.geom = geom a.type = 'GeometryCollection' break } } } /** * @private */ handleDragEvent(e) { if (!this.getActive()) return false if (!this.arcs) return true // Show sketch this.overlayLayer_.getSource().clear() var p = new ol_Feature(new ol_geom_Point(e.coordinate)) this.overlayLayer_.getSource().addFeature(p) // Nothing to do if (!this.arcs.length) return true // Move arcs this.arcs.forEach(function (a) { var coords = a.coord1.concat([e.coordinate], a.coord2) if (a.closed) coords.push(e.coordinate) this.setArcCoordinates(a, coords) }.bind(this)) this.dispatchEvent({ type: 'modifying', coordinate: e.coordinate, originalEvent: e.originalEvent, features: this._modifiedFeatures }) return true } /** * @param {ol.MapBrowserEvent} evt Event. * @private */ handleMoveEvent(e) { if (!this.getActive()) return true this.overlayLayer_.getSource().clear() var current = this.getClosestFeature(e) // Draw sketch if (current) { var p = new ol_Feature(new ol_geom_Point(current.coord)) this.overlayLayer_.getSource().addFeature(p) } // Show cursor var element = e.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 } } return true } /** Get the current feature to modify * @return {ol.Feature} */ getCurrentFeature() { return this.currentFeature } } export default ol_interaction_ModifyFeature