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