From 34260399e7434f59a069e5f4a213ad17eba3700b Mon Sep 17 00:00:00 2001 From: oterral Date: Fri, 22 Nov 2013 17:47:13 +0100 Subject: [PATCH] Add draw features interaction --- src/objectliterals.jsdoc | 11 + src/ol/interaction/drawinteraction.exports | 1 + src/ol/interaction/drawinteraction.js | 483 +++++++++++++++++++++ 3 files changed, 495 insertions(+) create mode 100644 src/ol/interaction/drawinteraction.exports create mode 100644 src/ol/interaction/drawinteraction.js diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index 8d6611ca29..85ad5c0bb4 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -353,6 +353,17 @@ * @todo stability experimental */ +/** + * @typedef {Object} olx.interaction.DrawOptions + * @property {ol.layer.Vector|undefined} layer Destination layer for the features. + * @property {number|undefined} snapTolerance Pixel distance for snapping to the + * drawing finish (default is 12). + * @property {ol.geom.GeometryType} type Drawing type ('Point', 'LineString', + * 'Polygon', 'MultiPoint', 'MultiLineString', or 'MultiPolygon'). + * @property {ol.feature.StyleFunction|undefined} styleFunction Style function. + * @todo stability experimental + */ + /** * @typedef {Object} olx.interaction.KeyboardPanOptions * @property {ol.events.ConditionType|undefined} condition A conditional diff --git a/src/ol/interaction/drawinteraction.exports b/src/ol/interaction/drawinteraction.exports new file mode 100644 index 0000000000..890ae0f515 --- /dev/null +++ b/src/ol/interaction/drawinteraction.exports @@ -0,0 +1 @@ +@exportSymbol ol.interaction.Draw diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js new file mode 100644 index 0000000000..24866d4fc2 --- /dev/null +++ b/src/ol/interaction/drawinteraction.js @@ -0,0 +1,483 @@ +goog.provide('ol.interaction.Draw'); + +goog.require('goog.asserts'); +goog.require('ol.Collection'); +goog.require('ol.Coordinate'); +goog.require('ol.Feature'); +goog.require('ol.Map'); +goog.require('ol.MapBrowserEvent'); +goog.require('ol.MapBrowserEvent.EventType'); +goog.require('ol.geom.LineString'); +goog.require('ol.geom.Point'); +goog.require('ol.geom.Polygon'); +goog.require('ol.interaction.Interaction'); +goog.require('ol.render.FeaturesOverlay'); +goog.require('ol.style.Circle'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); + + + +/** + * Interaction that allows drawing geometries + * @constructor + * @extends {ol.interaction.Interaction} + * @param {olx.interaction.DrawOptions=} opt_options Options. + */ +ol.interaction.Draw = function(opt_options) { + + goog.base(this); + + /** + * Target layer for drawn features. + * @type {ol.layer.Vector} + * @private + */ + this.layer_ = goog.isDef(opt_options.layer) ? opt_options.layer : null; + + /** + * Pixel distance for snapping. + * @type {number} + * @private + */ + this.snapTolerance_ = goog.isDef(opt_options.snapTolerance) ? + opt_options.snapTolerance : 12; + + /** + * Geometry type. + * @type {ol.geom.GeometryType} + * @private + */ + this.type_ = opt_options.type; + + /** + * Drawing mode (derived from geometry type. + * @type {ol.interaction.DrawMode} + * @private + */ + this.mode_ = ol.interaction.Draw.getMode_(this.type_); + + /** + * Finish coordinate for the feature (first point for polygons, last point for + * linestrings). + * @type {ol.Coordinate} + * @private + */ + this.finishCoordinate_ = null; + + /** + * Sketch feature. + * @type {ol.Feature} + * @private + */ + this.sketchFeature_ = null; + + /** + * Sketch point. + * @type {ol.Feature} + * @private + */ + this.sketchPoint_ = null; + + /** + * Sketch line. Used when drawing polygon. + * @type {ol.Feature} + * @private + */ + this.sketchLine_ = null; + + /** + * Sketch polygon. Used when drawing polygon. + * @type {ol.geom.RawPolygon} + * @private + */ + this.sketchRawPolygon_ = null; + + /** + * Squared tolerance for handling click events. If the squared distance + * between a down and click event is greater than this tolerance, click events + * will not be handled. + * @type {number} + * @private + */ + this.squaredClickTolerance_ = 4; + + /** + * Draw overlay where are sketch features are drawn. + * @type {ol.render.FeaturesOverlay} + * @private + */ + this.overlay_ = new ol.render.FeaturesOverlay(); + this.overlay_.setStyleFunction(goog.isDef(opt_options.styleFunction) ? + opt_options.styleFunction : ol.interaction.Draw.defaultStyleFunction + ); +}; +goog.inherits(ol.interaction.Draw, ol.interaction.Interaction); + + +/** + * @param {ol.Feature} feature Feature. + * @param {number} resolution Resolution. + * @return {Array.} Styles. + */ +ol.interaction.Draw.defaultStyleFunction = (function() { + /** @type {Object.>} */ + var styles = {}; + styles[ol.geom.GeometryType.POLYGON] = [ + new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.5)' + }) + }) + ]; + styles[ol.geom.GeometryType.MULTI_POLYGON] = + styles[ol.geom.GeometryType.POLYGON]; + + styles[ol.geom.GeometryType.LINE_STRING] = [ + new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: 'white', + width: 5 + }) + }), + new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: '#0099ff', + width: 3 + }) + }) + ]; + styles[ol.geom.GeometryType.MULTI_LINE_STRING] = + styles[ol.geom.GeometryType.LINE_STRING]; + + styles[ol.geom.GeometryType.POINT] = [ + new ol.style.Style({ + image: new ol.style.Circle({ + radius: 7, + fill: new ol.style.Fill({ + color: '#0099ff' + }), + stroke: new ol.style.Stroke({ + color: 'rgba(255, 255, 255, 0.75)', + width: 1.5 + }) + }), + zIndex: 100000 + }) + ]; + styles[ol.geom.GeometryType.MULTI_POINT] = + styles[ol.geom.GeometryType.POINT]; + + return function(feature, resolution) { + return styles[feature.getGeometry().getType()]; + }; +})(); + + +/** + * @inheritDoc + */ +ol.interaction.Draw.prototype.setMap = function(map) { + if (goog.isNull(map)) { + // removing from a map, clean up + this.abortDrawing_(); + } + this.overlay_.setMap(map); + goog.base(this, 'setMap', map); +}; + + +/** + * @inheritDoc + */ +ol.interaction.Draw.prototype.handleMapBrowserEvent = function(event) { + var map = event.map; + if (!map.isDef()) { + return true; + } + var pass = true; + if (event.type === ol.MapBrowserEvent.EventType.CLICK) { + pass = this.handleClick_(event); + } else if (event.type === ol.MapBrowserEvent.EventType.MOUSEMOVE) { + pass = this.handleMove_(event); + } else if (event.type === ol.MapBrowserEvent.EventType.DBLCLICK) { + pass = false; + } + return pass; +}; + + +/** + * Handle click events. + * @param {ol.MapBrowserEvent} event A click event. + * @return {boolean} Pass the event to other interactions. + * @private + */ +ol.interaction.Draw.prototype.handleClick_ = function(event) { + var downPx = event.map.getEventPixel(event.target.getDown()); + var clickPx = event.getPixel(); + var dx = downPx[0] - clickPx[0]; + var dy = downPx[1] - clickPx[1]; + var squaredDistance = dx * dx + dy * dy; + var pass = true; + if (squaredDistance <= this.squaredClickTolerance_) { + if (goog.isNull(this.finishCoordinate_)) { + this.startDrawing_(event); + } else if (this.mode_ === ol.interaction.DrawMode.POINT || + this.atFinish_(event)) { + this.finishDrawing_(event); + } else { + this.addToDrawing_(event); + } + pass = false; + } + return pass; +}; + + +/** + * Handle mousemove events. + * @param {ol.MapBrowserEvent} event A mousemove event. + * @return {boolean} Pass the event to other interactions. + * @private + */ +ol.interaction.Draw.prototype.handleMove_ = function(event) { + if (this.mode_ === ol.interaction.DrawMode.POINT && + goog.isNull(this.finishCoordinate_)) { + this.startDrawing_(event); + } else if (!goog.isNull(this.finishCoordinate_)) { + this.modifyDrawing_(event); + } + return true; +}; + + +/** + * Determine if an event is within the snapping tolerance of the start coord. + * @param {ol.MapBrowserEvent} event Event. + * @return {boolean} The event is within the snapping tolerance of the start. + * @private + */ +ol.interaction.Draw.prototype.atFinish_ = function(event) { + var at = false; + if (this.sketchFeature_) { + var geometry = this.sketchFeature_.getGeometry(); + var potentiallyDone = false; + if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) { + goog.asserts.assertInstanceof(geometry, ol.geom.LineString); + potentiallyDone = geometry.getCoordinates().length > 2; + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + goog.asserts.assertInstanceof(geometry, ol.geom.Polygon); + potentiallyDone = geometry.getCoordinates()[0].length > 3; + } + if (potentiallyDone) { + var map = event.map; + var finishPixel = map.getPixelFromCoordinate(this.finishCoordinate_); + var pixel = event.getPixel(); + var dx = pixel[0] - finishPixel[0]; + var dy = pixel[1] - finishPixel[1]; + at = Math.sqrt(dx * dx + dy * dy) <= this.snapTolerance_; + } + } + return at; +}; + + +/** + * Start the drawing. + * @param {ol.MapBrowserEvent} event Event. + * @private + */ +ol.interaction.Draw.prototype.startDrawing_ = function(event) { + var start = event.getCoordinate(); + this.finishCoordinate_ = start; + var geometry; + if (this.mode_ === ol.interaction.DrawMode.POINT) { + geometry = new ol.geom.Point(start.slice()); + } else { + this.sketchPoint_ = new ol.Feature(new ol.geom.Point(start.slice())); + + if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) { + geometry = new ol.geom.LineString([start.slice(), start.slice()]); + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + this.sketchLine_ = new ol.Feature(new ol.geom.LineString([start.slice(), + start.slice()])); + this.sketchRawPolygon_ = [[start.slice(), start.slice()]]; + geometry = new ol.geom.Polygon(this.sketchRawPolygon_); + } + } + goog.asserts.assert(goog.isDef(geometry)); + this.sketchFeature_ = new ol.Feature(geometry); + this.updateSketchFeatures_(); +}; + + +/** + * Modify the drawing. + * @param {ol.MapBrowserEvent} event Event. + * @private + */ +ol.interaction.Draw.prototype.modifyDrawing_ = function(event) { + var coordinate = event.getCoordinate(); + var geometry = this.sketchFeature_.getGeometry(); + var coordinates, last; + if (this.mode_ === ol.interaction.DrawMode.POINT) { + goog.asserts.assertInstanceof(geometry, ol.geom.Point); + last = geometry.getCoordinates(); + last[0] = coordinate[0]; + last[1] = coordinate[1]; + geometry.setCoordinates(last); + } else { + if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) { + goog.asserts.assertInstanceof(geometry, ol.geom.LineString); + coordinates = geometry.getCoordinates(); + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + goog.asserts.assertInstanceof(geometry, ol.geom.Polygon); + coordinates = this.sketchRawPolygon_[0]; + } + if (this.atFinish_(event)) { + // snap to finish + coordinate = this.finishCoordinate_.slice(); + } + var sketchPointGeom = this.sketchPoint_.getGeometry(); + goog.asserts.assertInstanceof(sketchPointGeom, ol.geom.Point); + sketchPointGeom.setCoordinates(coordinate); + last = coordinates[coordinates.length - 1]; + last[0] = coordinate[0]; + last[1] = coordinate[1]; + if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) { + goog.asserts.assertInstanceof(geometry, ol.geom.LineString); + geometry.setCoordinates(coordinates); + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + var sketchLineGeom = this.sketchLine_.getGeometry(); + goog.asserts.assertInstanceof(sketchLineGeom, ol.geom.LineString); + sketchLineGeom.setCoordinates(coordinates); + goog.asserts.assertInstanceof(geometry, ol.geom.Polygon); + geometry.setCoordinates(this.sketchRawPolygon_); + } + } + this.updateSketchFeatures_(); +}; + + +/** + * Add a new coordinate to the drawing. + * @param {ol.MapBrowserEvent} event Event. + * @private + */ +ol.interaction.Draw.prototype.addToDrawing_ = function(event) { + var coordinate = event.getCoordinate(); + var geometry = this.sketchFeature_.getGeometry(); + var coordinates, last; + if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) { + this.finishCoordinate_ = coordinate.slice(); + goog.asserts.assertInstanceof(geometry, ol.geom.LineString); + coordinates = geometry.getCoordinates(); + coordinates.push(coordinate.slice()); + geometry.setCoordinates(coordinates); + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + this.sketchRawPolygon_[0].push(coordinate.slice()); + goog.asserts.assertInstanceof(geometry, ol.geom.Polygon); + geometry.setCoordinates(this.sketchRawPolygon_); + } + this.updateSketchFeatures_(); +}; + + +/** + * Stop drawing and add the sketch feature to the target layer. + * @param {ol.MapBrowserEvent} event Event. + * @private + */ +ol.interaction.Draw.prototype.finishDrawing_ = function(event) { + var sketchFeature = this.abortDrawing_(); + goog.asserts.assert(!goog.isNull(sketchFeature)); + var coordinates; + var geometry = sketchFeature.getGeometry(); + if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) { + goog.asserts.assertInstanceof(geometry, ol.geom.LineString); + coordinates = geometry.getCoordinates(); + // remove the redundant last point + coordinates.pop(); + geometry.setCoordinates(coordinates); + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + goog.asserts.assertInstanceof(geometry, ol.geom.Polygon); + coordinates = geometry.getCoordinates(); + // force clockwise order for exterior ring + sketchFeature.setGeometry(new ol.geom.Polygon(coordinates)); + } + if (this.layer_) { + this.layer_.getSource().addFeature(sketchFeature); + } +}; + + +/** + * Stop drawing without adding the sketch feature to the target layer. + * @return {ol.Feature} The sketch feature (or null if none). + * @private + */ +ol.interaction.Draw.prototype.abortDrawing_ = function() { + this.finishCoordinate_ = null; + var sketchFeature = this.sketchFeature_; + if (!goog.isNull(sketchFeature)) { + this.sketchFeature_ = null; + this.sketchPoint_ = null; + this.sketchLine_ = null; + this.overlay_.getFeatures().clear(); + } + return sketchFeature; +}; + + +/** + * Redraw the skecth features. + * @private + */ +ol.interaction.Draw.prototype.updateSketchFeatures_ = function() { + var sketchFeatures = [this.sketchFeature_]; + if (this.sketchLine_) { + sketchFeatures.push(this.sketchLine_); + } + if (this.sketchPoint_) { + sketchFeatures.push(this.sketchPoint_); + } + this.overlay_.setFeatures(new ol.Collection(sketchFeatures)); +}; + + +/** + * Get the drawing mode. The mode for mult-part geometries is the same as for + * their single-part cousins. + * @param {ol.geom.GeometryType} type Geometry type. + * @return {ol.interaction.DrawMode} Drawing mode. + * @private + */ +ol.interaction.Draw.getMode_ = function(type) { + var mode; + if (type === ol.geom.GeometryType.POINT || + type === ol.geom.GeometryType.MULTI_POINT) { + mode = ol.interaction.DrawMode.POINT; + } else if (type === ol.geom.GeometryType.LINE_STRING || + type === ol.geom.GeometryType.MULTI_LINE_STRING) { + mode = ol.interaction.DrawMode.LINE_STRING; + } else if (type === ol.geom.GeometryType.POLYGON || + type === ol.geom.GeometryType.MULTI_POLYGON) { + mode = ol.interaction.DrawMode.POLYGON; + } + goog.asserts.assert(goog.isDef(mode)); + return mode; +}; + + +/** + * Draw mode. This collapses multi-part geometry types with their single-part + * cousins. + * @enum {string} + */ +ol.interaction.DrawMode = { + POINT: 'Point', + LINE_STRING: 'LineString', + POLYGON: 'Polygon' +};