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.
+
+
+
+
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