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