344 lines
11 KiB
JavaScript
344 lines
11 KiB
JavaScript
/* 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 {buffer as ol_extent_buffer, containsCoordinate as ol_extent_containsCoordinate} from 'ol/extent.js'
|
|
import ol_source_Vector from 'ol/source/Vector.js'
|
|
import ol_layer_Vector from 'ol/layer/Vector.js'
|
|
import ol_Collection from 'ol/Collection.js'
|
|
import ol_Feature from 'ol/Feature.js'
|
|
import ol_geom_LineString from 'ol/geom/LineString.js'
|
|
import './Modify.js'
|
|
|
|
/** Interaction to snap to guidelines
|
|
* @constructor
|
|
* @extends {ol_interaction_Interaction}
|
|
* @param {*} options
|
|
* @param {number | undefined} options.pixelTolerance distance (in px) to snap to a guideline, default 10 px
|
|
* @param {bool | undefined} options.enableInitialGuides whether to draw initial guidelines based on the maps orientation, default false.
|
|
* @param {ol_style_Style | Array<ol_style_Style> | undefined} options.style Style for the sektch features.
|
|
* @param {*} options.vectorClass a vector layer class to create the guides with ol6, use ol/layer/VectorImage using ol6
|
|
*/
|
|
var ol_interaction_SnapGuides = class olinteractionSnapGuides extends ol_interaction_Interaction {
|
|
constructor(options) {
|
|
options = options || {}
|
|
|
|
// Intersect 2 guides
|
|
function getIntersectionPoint(d1, d2) {
|
|
var d1x = d1[1][0] - d1[0][0]
|
|
var d1y = d1[1][1] - d1[0][1]
|
|
var d2x = d2[1][0] - d2[0][0]
|
|
var d2y = d2[1][1] - d2[0][1]
|
|
var det = d1x * d2y - d1y * d2x
|
|
|
|
if (det != 0) {
|
|
var k = (d1x * d1[0][1] - d1x * d2[0][1] - d1y * d1[0][0] + d1y * d2[0][0]) / det
|
|
return [d2[0][0] + k * d2x, d2[0][1] + k * d2y]
|
|
}
|
|
else
|
|
return false
|
|
}
|
|
function dist2D(p1, p2) {
|
|
var dx = p1[0] - p2[0]
|
|
var dy = p1[1] - p2[1]
|
|
return Math.sqrt(dx * dx + dy * dy)
|
|
}
|
|
|
|
// Use snap interaction
|
|
super({
|
|
handleEvent: function (e) {
|
|
if (this.getActive()) {
|
|
var features = this.overlaySource_.getFeatures()
|
|
var prev = null
|
|
var p = null
|
|
var res = e.frameState.viewState.resolution
|
|
for (var i = 0, f; f = features[i]; i++) {
|
|
var c = f.getGeometry().getClosestPoint(e.coordinate)
|
|
if (dist2D(c, e.coordinate) / res < this.snapDistance_) {
|
|
// Intersection on 2 lines
|
|
if (prev) {
|
|
var c2 = getIntersectionPoint(prev.getGeometry().getCoordinates(), f.getGeometry().getCoordinates())
|
|
if (c2) {
|
|
if (dist2D(c2, e.coordinate) / res < this.snapDistance_) {
|
|
p = c2
|
|
}
|
|
}
|
|
} else {
|
|
p = c
|
|
}
|
|
prev = f
|
|
}
|
|
}
|
|
if (p)
|
|
e.coordinate = p
|
|
}
|
|
return true
|
|
}
|
|
})
|
|
|
|
// Snap distance (in px)
|
|
this.snapDistance_ = options.pixelTolerance || 10
|
|
this.enableInitialGuides_ = options.enableInitialGuides || false
|
|
|
|
// Default style
|
|
var sketchStyle = [
|
|
new ol_style_Style({
|
|
stroke: new ol_style_Stroke({
|
|
color: '#ffcc33',
|
|
lineDash: [8, 5],
|
|
width: 1.25
|
|
})
|
|
})
|
|
]
|
|
|
|
// Custom style
|
|
if (options.style) {
|
|
sketchStyle = options.style instanceof Array ? options.style : [options.style]
|
|
}
|
|
|
|
// Create a new overlay for the sketch
|
|
this.overlaySource_ = new ol_source_Vector({
|
|
features: new ol_Collection(),
|
|
useSpatialIndex: false
|
|
})
|
|
|
|
// Use ol/layer/VectorImage to render the snap guides as an image to improve performance on rerenderers
|
|
const vectorClass = options.vectorClass || ol_layer_Vector
|
|
this.overlayLayer_ = new vectorClass({
|
|
// render the snap guides as an image to improve performance on rerenderers
|
|
renderMode: 'image',
|
|
source: this.overlaySource_,
|
|
style: function () {
|
|
return sketchStyle
|
|
},
|
|
name: 'Snap overlay',
|
|
displayInLayerSwitcher: false
|
|
})
|
|
this.overlayLayer_.setVisible(this.getActive());
|
|
}
|
|
/**
|
|
* 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)
|
|
if (map) this.projExtent_ = map.getView().getProjection().getExtent()
|
|
}
|
|
/** Activate or deactivate the interaction.
|
|
* @param {boolean} active
|
|
*/
|
|
setActive(active) {
|
|
if (this.overlayLayer_) this.overlayLayer_.setVisible(active)
|
|
super.setActive(active)
|
|
}
|
|
/** Clear previous added guidelines
|
|
* @param {Array<ol.Feature> | undefined} features a list of feature to remove, default remove all feature
|
|
*/
|
|
clearGuides(features) {
|
|
if (!features) {
|
|
this.overlaySource_.clear()
|
|
} else {
|
|
for (var i = 0, f; f = features[i]; i++) {
|
|
try {
|
|
this.overlaySource_.removeFeature(f)
|
|
} catch (e) { /* nothing to to */ }
|
|
}
|
|
}
|
|
}
|
|
/** Get guidelines
|
|
* @return {ol.Collection} guidelines features
|
|
*/
|
|
getGuides() {
|
|
return this.overlaySource_.getFeaturesCollection()
|
|
}
|
|
/** Add a new guide to snap to
|
|
* @param {Array<ol.coordinate>} v the direction vector
|
|
* @return {ol.Feature} feature guide
|
|
*/
|
|
addGuide(v, ortho) {
|
|
if (v) {
|
|
var map = this.getMap()
|
|
// Limit extent
|
|
var extent = map.getView().calculateExtent(map.getSize())
|
|
|
|
var guideLength = Math.max(
|
|
this.projExtent_[2] - this.projExtent_[0],
|
|
this.projExtent_[3] - this.projExtent_[1]
|
|
)
|
|
|
|
extent = ol_extent_buffer(extent, guideLength * 1.5)
|
|
//extent = ol_extent_boundingExtent(extent, this.projExtent_);
|
|
if (extent[0] < this.projExtent_[0])
|
|
extent[0] = this.projExtent_[0]
|
|
if (extent[1] < this.projExtent_[1])
|
|
extent[1] = this.projExtent_[1]
|
|
if (extent[2] > this.projExtent_[2])
|
|
extent[2] = this.projExtent_[2]
|
|
if (extent[3] > this.projExtent_[3])
|
|
extent[3] = this.projExtent_[3]
|
|
|
|
var dx = v[0][0] - v[1][0]
|
|
var dy = v[0][1] - v[1][1]
|
|
var d = 1 / Math.sqrt(dx * dx + dy * dy)
|
|
|
|
var generateLine = function (loopDir) {
|
|
var p, g = []
|
|
var loopCond = guideLength * loopDir * 2
|
|
for (var i = 0; loopDir > 0 ? i < loopCond : i > loopCond; i += (guideLength * loopDir) / 100) {
|
|
if (ortho)
|
|
p = [v[0][0] + dy * d * i, v[0][1] - dx * d * i]
|
|
else
|
|
p = [v[0][0] + dx * d * i, v[0][1] + dy * d * i]
|
|
if (ol_extent_containsCoordinate(extent, p))
|
|
g.push(p)
|
|
else
|
|
break
|
|
}
|
|
return new ol_Feature(new ol_geom_LineString([g[0], g[g.length - 1]]))
|
|
}
|
|
|
|
var f0 = generateLine(1)
|
|
var f1 = generateLine(-1)
|
|
this.overlaySource_.addFeature(f0)
|
|
this.overlaySource_.addFeature(f1)
|
|
return [f0, f1]
|
|
}
|
|
}
|
|
/** Add a new orthogonal guide to snap to
|
|
* @param {Array<ol.coordinate>} v the direction vector
|
|
* @return {ol.Feature} feature guide
|
|
*/
|
|
addOrthoGuide(v) {
|
|
return this.addGuide(v, true)
|
|
}
|
|
/** Listen to draw event to add orthogonal guidelines on the first and last point.
|
|
* @param {_ol_interaction_Draw_} drawi a draw interaction to listen to
|
|
* @api
|
|
*/
|
|
setDrawInteraction(drawi) {
|
|
var self = this
|
|
// Number of points currently drawing
|
|
var nb = 0
|
|
// Current guidelines
|
|
var features = []
|
|
function setGuides(e) {
|
|
var coord = e.target.getCoordinates()
|
|
var s = 2
|
|
switch (e.target.getType()) {
|
|
case 'Point':
|
|
return
|
|
case 'Polygon':
|
|
coord = coord[0].slice(0, -1)
|
|
break
|
|
default: break
|
|
}
|
|
|
|
var l = coord.length
|
|
if (l === s && self.enableInitialGuides_) {
|
|
var x = coord[0][0]
|
|
var y = coord[0][1]
|
|
coord = [[x, y], [x, y - 1]]
|
|
}
|
|
if (l != nb && (self.enableInitialGuides_ ? l >= s : l > s)) {
|
|
self.clearGuides(features)
|
|
// use try catch to remove a bug on freehand draw...
|
|
try {
|
|
var p1 = coord[l - s], p2 = coord[l - s - 1]
|
|
if (l > s && !(p1[0] === p2[0] && p1[1] === p2[1])) {
|
|
features = self.addOrthoGuide([coord[l - s], coord[l - s - 1]])
|
|
}
|
|
features = features.concat(self.addGuide([coord[0], coord[1]]))
|
|
features = features.concat(self.addOrthoGuide([coord[0], coord[1]]))
|
|
nb = l
|
|
} catch (e) { /* ok*/ }
|
|
}
|
|
}
|
|
// New drawing
|
|
drawi.on("drawstart", function (e) {
|
|
// When geom is changing add a new orthogonal direction
|
|
e.feature.getGeometry().on("change", setGuides)
|
|
})
|
|
// end drawing / deactivate => clear directions
|
|
drawi.on(["drawend", "change:active"], function (e) {
|
|
self.clearGuides(features)
|
|
if (e.feature)
|
|
e.feature.getGeometry().un("change", setGuides)
|
|
nb = 0
|
|
features = []
|
|
})
|
|
}
|
|
/** Listen to modify event to add orthogonal guidelines relative to the currently dragged point
|
|
* @param {_ol_interaction_Modify_} modifyi a modify interaction to listen to
|
|
* @api
|
|
*/
|
|
setModifyInteraction(modifyi) {
|
|
function mod(d, n) {
|
|
return ((d % n) + n) % n
|
|
}
|
|
|
|
var self = this
|
|
// Current guidelines
|
|
var features = []
|
|
|
|
function computeGuides(e) {
|
|
var modifyVertex = e.coordinate
|
|
if (!modifyVertex) {
|
|
var selectedVertex = e.target.vertexFeature_
|
|
if (!selectedVertex) return
|
|
modifyVertex = selectedVertex.getGeometry().getCoordinates()
|
|
}
|
|
var f = e.target.getModifiedFeatures()[0]
|
|
var geom = f.getGeometry()
|
|
|
|
var coord = geom.getCoordinates()
|
|
switch (geom.getType()) {
|
|
case 'Point':
|
|
return
|
|
case 'Polygon':
|
|
coord = coord[0].slice(0, -1)
|
|
break
|
|
default: break
|
|
}
|
|
|
|
var idx = coord.findIndex(function (c) {
|
|
return c[0] === modifyVertex[0] && c[1] === modifyVertex[1]
|
|
})
|
|
|
|
var l = coord.length
|
|
|
|
self.clearGuides(features)
|
|
features = self.addOrthoGuide([coord[mod(idx - 1, l)], coord[mod(idx - 2, l)]])
|
|
features = features.concat(self.addGuide([coord[mod(idx - 1, l)], coord[mod(idx - 2, l)]]))
|
|
features = features.concat(self.addGuide([coord[mod(idx + 1, l)], coord[mod(idx + 2, l)]]))
|
|
features = features.concat(self.addOrthoGuide([coord[mod(idx + 1, l)], coord[mod(idx + 2, l)]]))
|
|
}
|
|
|
|
function setGuides(e) {
|
|
// This callback is called before ol adds the vertex to the feature, so
|
|
// defer a moment for openlayers to add the new vertex
|
|
setTimeout(computeGuides, 0, e)
|
|
}
|
|
|
|
|
|
function drawEnd() {
|
|
self.clearGuides(features)
|
|
features = []
|
|
}
|
|
|
|
// New drawing
|
|
modifyi.on('modifystart', setGuides)
|
|
// end drawing, clear directions
|
|
modifyi.on('modifyend', drawEnd)
|
|
}
|
|
}
|
|
|
|
export default ol_interaction_SnapGuides
|