diff --git a/examples/draw-features.html b/examples/draw-features.html new file mode 100644 index 0000000000..3cb90e6121 --- /dev/null +++ b/examples/draw-features.html @@ -0,0 +1,59 @@ + + + + + + + + + + + Draw features example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

Draw features example

+

Example of using the Draw interaction.

+
+ + +
+ +
+

See the draw-features.js source to see how this is done.

+
+
draw, edit, vector
+
+ +
+ +
+ + + + + + diff --git a/examples/draw-features.js b/examples/draw-features.js new file mode 100644 index 0000000000..a61b246f94 --- /dev/null +++ b/examples/draw-features.js @@ -0,0 +1,74 @@ +goog.require('ol.Map'); +goog.require('ol.RendererHint'); +goog.require('ol.View2D'); +goog.require('ol.interaction'); +goog.require('ol.interaction.Draw'); +goog.require('ol.layer.Tile'); +goog.require('ol.layer.Vector'); +goog.require('ol.source.MapQuestOpenAerial'); +goog.require('ol.source.Vector'); +goog.require('ol.style.Circle'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); + +var raster = new ol.layer.Tile({ + source: new ol.source.MapQuestOpenAerial() +}); + +var styleArray = [new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.2)' + }), + stroke: new ol.style.Stroke({ + color: '#ffcc33', + width: 2 + }), + image: new ol.style.Circle({ + radius: 7, + fill: new ol.style.Fill({ + color: '#ffcc33' + }) + }) +})]; + +var vector = new ol.layer.Vector({ + source: new ol.source.Vector(), + styleFunction: function(feature, resolution) { + return styleArray; + } +}); + +var map = new ol.Map({ + layers: [raster, vector], + renderer: ol.RendererHint.CANVAS, + target: 'map', + view: new ol.View2D({ + center: [-11000000, 4600000], + zoom: 4 + }) +}); + +var typeSelect = document.getElementById('type'); + +var draw; // global so we can remove it later +function addInteraction() { + draw = new ol.interaction.Draw({ + layer: vector, + type: /** @type {ol.geom.GeometryType} */ + (typeSelect.options[typeSelect.selectedIndex].value) + }); + map.addInteraction(draw); +} + + +/** + * Let user change the geometry type. + * @param {Event} e Change event. + */ +typeSelect.onchange = function(e) { + map.removeInteraction(draw); + addInteraction(); +}; + +addInteraction(); diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index 6b033629e6..c787d828be 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' +}; diff --git a/src/ol/render/featuresoverlay.js b/src/ol/render/featuresoverlay.js index 494444b9ec..74e8fc058d 100644 --- a/src/ol/render/featuresoverlay.js +++ b/src/ol/render/featuresoverlay.js @@ -19,7 +19,7 @@ goog.require('ol.render.EventType'); */ ol.render.FeaturesOverlay = function(opt_options) { - var options = goog.isDef(opt_options) ? opt_options : options; + var options = goog.isDef(opt_options) ? opt_options : {}; /** * @private