From b3487ad30ea2853e41db600af755b6e8b39b4816 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 29 Oct 2013 17:24:22 -0600 Subject: [PATCH 01/15] Draw interaction --- examples/draw-features.html | 50 ++++ examples/draw-features.js | 92 +++++++ src/objectliterals.jsdoc | 8 + src/ol/interaction/drawinteraction.exports | 1 + src/ol/interaction/drawinteraction.js | 286 +++++++++++++++++++++ 5 files changed, 437 insertions(+) create mode 100644 examples/draw-features.html create mode 100644 examples/draw-features.js create mode 100644 src/ol/interaction/drawinteraction.exports create mode 100644 src/ol/interaction/drawinteraction.js diff --git a/examples/draw-features.html b/examples/draw-features.html new file mode 100644 index 0000000000..11a34094d4 --- /dev/null +++ b/examples/draw-features.html @@ -0,0 +1,50 @@ + + + + + + + + + + + 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..3eae7e65e4 --- /dev/null +++ b/examples/draw-features.js @@ -0,0 +1,92 @@ +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.Fill'); +goog.require('ol.style.Rule'); +goog.require('ol.style.Shape'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); + +var raster = new ol.layer.Tile({ + source: new ol.source.MapQuestOpenAerial() +}); + +var vector = new ol.layer.Vector({ + source: new ol.source.Vector({parser: null}), + style: new ol.style.Style({ + rules: [ + new ol.style.Rule({ + filter: 'renderIntent("selected")', + symbolizers: [ + new ol.style.Shape({ + fill: new ol.style.Fill({ + color: '#0099ff', + opacity: 1 + }), + size: 16 + }), + new ol.style.Fill({ + color: '#ffffff', + opacity: 0.5 + }), + new ol.style.Stroke({ + color: '#0099ff', + width: 3 + }) + ] + }), + new ol.style.Rule({ + filter: 'renderIntent("temporary")', + symbolizers: [ + new ol.style.Shape({ + fill: new ol.style.Fill({ + color: '#0033ff', + opacity: 1 + }), + size: 16, + zIndex: 1 + }) + ] + }) + ], + symbolizers: [ + new ol.style.Shape({ + fill: new ol.style.Fill({ + color: '#0033ff', + opacity: 1 + }), + size: 16 + }), + new ol.style.Fill({ + color: '#ffffff', + opacity: 0.2 + }), + new ol.style.Stroke({ + color: '#ffcc33', + width: 1.5 + }) + ] + }) +}); + +var draw = new ol.interaction.Draw({ + layer: vector, + mode: /** @type {ol.interaction.DrawMode} */ ('polygon') +}); + +var map = new ol.Map({ + interactions: ol.interaction.defaults().extend([draw]), + layers: [raster, vector], + renderer: ol.RendererHint.CANVAS, + target: 'map', + view: new ol.View2D({ + center: [-11000000, 4600000], + zoom: 4 + }) +}); diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index 0c6c3b03fc..be482df52e 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -341,6 +341,14 @@ * @todo stability experimental */ +/** + * @typedef {Object} ol.interaction.DrawOptions + * @property {ol.layer.Vector} layer Destination layer for the features. + * @property {ol.interaction.DrawMode} mode Drawing mode ('point', 'linestring', + * or 'polygon'). + * @todo stability experimental + */ + /** * @typedef {Object} ol.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..c085ef69e2 --- /dev/null +++ b/src/ol/interaction/drawinteraction.exports @@ -0,0 +1 @@ +@exportClass ol.interaction.Draw ol.interaction.DrawOptions diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js new file mode 100644 index 0000000000..8436d70769 --- /dev/null +++ b/src/ol/interaction/drawinteraction.js @@ -0,0 +1,286 @@ +goog.provide('ol.interaction.Draw'); +goog.provide('ol.interaction.DrawMode'); + +goog.require('goog.asserts'); + +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.layer.Vector'); +goog.require('ol.layer.VectorLayerRenderIntent'); +goog.require('ol.source.Vector'); + + + +/** + * Interaction that allows drawing geometries. + * @param {ol.interaction.DrawOptions} options Options. + * @constructor + * @extends {ol.interaction.Interaction} + */ +ol.interaction.Draw = function(options) { + goog.base(this); + + /** + * Target layer for drawn features. + * @type {ol.layer.Vector} + * @private + */ + this.layer_ = options.layer; + + /** + * Temporary sketch layer. + * @type {ol.layer.Vector} + * @private + */ + this.sketchLayer_ = null; + + /** + * Pixel distance for snapping. + * @type {number} + * @private + */ + this.snapTolerance_ = 15; + + /** + * Draw mode. + * @type {ol.interaction.DrawMode} + * @private + */ + this.mode_ = options.mode; + + /** + * Start coordinate for the feature. + * @type {ol.Coordinate} + * @private + */ + this.startCoordinate_ = null; + + /** + * Sketch feature. + * @type {ol.Feature} + * @private + */ + this.sketchFeature_ = null; + +}; +goog.inherits(ol.interaction.Draw, ol.interaction.Interaction); + + +/** + * @inheritDoc + */ +ol.interaction.Draw.prototype.setMap = function(map) { + var oldMap = this.getMap(); + if (!goog.isNull(oldMap)) { + oldMap.removeLayer(this.sketchLayer_); + } + + if (!goog.isNull(map)) { + if (goog.isNull(this.sketchLayer_)) { + var layer = new ol.layer.Vector({ + source: new ol.source.Vector({parser: null}), + style: this.layer_.getStyle() + }); + layer.setTemporary(true); + this.sketchLayer_ = layer; + } + map.addLayer(this.sketchLayer_); + } else { + // removing from a map, clean up + this.sketchLayer_ = null; + this.startCoordinate_ = null; + this.sketchFeature_ = null; + } + + 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); + } + 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) { + if (goog.isNull(this.startCoordinate_)) { + this.startDrawing_(event); + } else if (this.mode_ === ol.interaction.DrawMode.POINT || + this.atStart_(event)) { + this.finishDrawing_(event); + } else { + this.addToDrawing_(event); + } + return false; +}; + + +/** + * 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.startCoordinate_)) { + this.startDrawing_(event); + } else if (!goog.isNull(this.startCoordinate_)) { + 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.atStart_ = function(event) { + var map = event.map; + var startPixel = map.getPixelFromCoordinate(this.startCoordinate_); + var pixel = event.getPixel(); + var dx = pixel[0] - startPixel[0]; + var dy = pixel[1] - startPixel[1]; + return Math.sqrt(dx * dx + dy * dy) <= this.snapTolerance_; +}; + + +/** + * Start the drawing. + * @param {ol.MapBrowserEvent} event Event. + * @private + */ +ol.interaction.Draw.prototype.startDrawing_ = function(event) { + var start = event.getCoordinate(); + this.startCoordinate_ = start; + var feature = new ol.Feature(); + feature.setRenderIntent(ol.layer.VectorLayerRenderIntent.SELECTED); + var geometry; + if (this.mode_ === ol.interaction.DrawMode.POINT) { + geometry = new ol.geom.Point(start.slice()); + } else if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { + geometry = new ol.geom.LineString([start.slice(), start.slice()]); + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + geometry = new ol.geom.Polygon([[start.slice(), start.slice()]]); + } + goog.asserts.assert(goog.isDef(geometry)); + feature.setGeometry(geometry); + this.sketchFeature_ = feature; + this.sketchLayer_.addFeatures([feature]); +}; + + +/** + * 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) { + last = geometry.getCoordinates(); + last[0] = coordinate[0]; + last[1] = coordinate[1]; + geometry.setCoordinates(last); + } else if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { + coordinates = geometry.getCoordinates(); + last = coordinates[coordinates.length - 1]; + last[0] = coordinate[0]; + last[1] = coordinate[1]; + geometry.setCoordinates(coordinates); + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + var ring = geometry.getRings()[0]; + coordinates = ring.getCoordinates(); + last = coordinates[coordinates.length - 1]; + last[0] = coordinate[0]; + last[1] = coordinate[1]; + ring.setCoordinates(coordinates); + } +}; + + +/** + * 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.LINESTRING) { + coordinates = geometry.getCoordinates(); + coordinates.push(coordinate.slice()); + geometry.setCoordinates(coordinates); + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + var ring = geometry.getRings()[0]; + coordinates = ring.getCoordinates(); + coordinates.push(coordinate.slice()); + ring.setCoordinates(coordinates); + } +}; + + +/** + * Finish the drawing. + * @param {ol.MapBrowserEvent} event Event. + * @private + */ +ol.interaction.Draw.prototype.finishDrawing_ = function(event) { + this.startCoordinate_ = null; + var feature = this.sketchFeature_; + this.sketchLayer_.removeFeatures([feature]); + feature.setRenderIntent(ol.layer.VectorLayerRenderIntent.DEFAULT); + this.layer_.addFeatures([feature]); +}; + + +/** + * Set the drawing mode. + * @param {ol.interaction.DrawMode} mode Draw mode ('point', 'linestring', or + * 'polygon'). + */ +ol.interaction.Draw.prototype.setMode = function(mode) { + this.mode_ = mode; +}; + + +/** + * Draw mode. + * @enum {string} + */ +ol.interaction.DrawMode = { + POINT: 'point', + LINESTRING: 'linestring', + POLYGON: 'polygon' +}; From d821f227babab4ea595fcd5e1607168638cbc5a6 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Wed, 30 Oct 2013 15:28:52 -0600 Subject: [PATCH 02/15] Add point at head of drawing --- examples/draw-features.js | 13 +++++ src/ol/interaction/drawinteraction.js | 77 ++++++++++++++++++--------- 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/examples/draw-features.js b/examples/draw-features.js index 3eae7e65e4..4bb1617f92 100644 --- a/examples/draw-features.js +++ b/examples/draw-features.js @@ -53,6 +53,19 @@ var vector = new ol.layer.Vector({ zIndex: 1 }) ] + }), + new ol.style.Rule({ + filter: 'renderIntent("future")', + symbolizers: [ + new ol.style.Shape({ + fill: new ol.style.Fill({ + color: '#0099ff', + opacity: 1 + }), + size: 16, + zIndex: 1 + }) + ] }) ], symbolizers: [ diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index 8436d70769..bc626db88b 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -69,6 +69,13 @@ ol.interaction.Draw = function(options) { */ this.sketchFeature_ = null; + /** + * Sketch point. + * @type {ol.Feature} + * @private + */ + this.sketchPoint_ = null; + }; goog.inherits(ol.interaction.Draw, ol.interaction.Interaction); @@ -181,20 +188,31 @@ ol.interaction.Draw.prototype.atStart_ = function(event) { ol.interaction.Draw.prototype.startDrawing_ = function(event) { var start = event.getCoordinate(); this.startCoordinate_ = start; - var feature = new ol.Feature(); - feature.setRenderIntent(ol.layer.VectorLayerRenderIntent.SELECTED); + var sketchFeature = new ol.Feature(); + sketchFeature.setRenderIntent(ol.layer.VectorLayerRenderIntent.SELECTED); + var features = [sketchFeature]; var geometry; if (this.mode_ === ol.interaction.DrawMode.POINT) { geometry = new ol.geom.Point(start.slice()); - } else if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { - geometry = new ol.geom.LineString([start.slice(), start.slice()]); - } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { - geometry = new ol.geom.Polygon([[start.slice(), start.slice()]]); + } else { + var sketchPoint = new ol.Feature({ + geom: new ol.geom.Point(start.slice()) + }); + sketchPoint.setRenderIntent(ol.layer.VectorLayerRenderIntent.FUTURE); + this.sketchPoint_ = sketchPoint; + features.push(sketchPoint); + + if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { + geometry = new ol.geom.LineString([start.slice(), start.slice()]); + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + geometry = new ol.geom.Polygon([[start.slice(), start.slice()]]); + } } goog.asserts.assert(goog.isDef(geometry)); - feature.setGeometry(geometry); - this.sketchFeature_ = feature; - this.sketchLayer_.addFeatures([feature]); + sketchFeature.setGeometry(geometry); + this.sketchFeature_ = sketchFeature; + + this.sketchLayer_.addFeatures(features); }; @@ -212,19 +230,22 @@ ol.interaction.Draw.prototype.modifyDrawing_ = function(event) { last[0] = coordinate[0]; last[1] = coordinate[1]; geometry.setCoordinates(last); - } else if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { - coordinates = geometry.getCoordinates(); - last = coordinates[coordinates.length - 1]; - last[0] = coordinate[0]; - last[1] = coordinate[1]; - geometry.setCoordinates(coordinates); - } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { - var ring = geometry.getRings()[0]; - coordinates = ring.getCoordinates(); - last = coordinates[coordinates.length - 1]; - last[0] = coordinate[0]; - last[1] = coordinate[1]; - ring.setCoordinates(coordinates); + } else { + this.sketchPoint_.getGeometry().setCoordinates(coordinate); + if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { + coordinates = geometry.getCoordinates(); + last = coordinates[coordinates.length - 1]; + last[0] = coordinate[0]; + last[1] = coordinate[1]; + geometry.setCoordinates(coordinates); + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + var ring = geometry.getRings()[0]; + coordinates = ring.getCoordinates(); + last = coordinates[coordinates.length - 1]; + last[0] = coordinate[0]; + last[1] = coordinate[1]; + ring.setCoordinates(coordinates); + } } }; @@ -258,10 +279,14 @@ ol.interaction.Draw.prototype.addToDrawing_ = function(event) { */ ol.interaction.Draw.prototype.finishDrawing_ = function(event) { this.startCoordinate_ = null; - var feature = this.sketchFeature_; - this.sketchLayer_.removeFeatures([feature]); - feature.setRenderIntent(ol.layer.VectorLayerRenderIntent.DEFAULT); - this.layer_.addFeatures([feature]); + var sketchFeature = this.sketchFeature_; + var features = [sketchFeature]; + if (this.mode_ !== ol.interaction.DrawMode.POINT) { + features.push(this.sketchPoint_); + } + this.sketchLayer_.removeFeatures(features); + sketchFeature.setRenderIntent(ol.layer.VectorLayerRenderIntent.DEFAULT); + this.layer_.addFeatures([sketchFeature]); }; From ea6500ecd0e0378470b10a6c14bcd507a340f778 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Wed, 30 Oct 2013 15:41:20 -0600 Subject: [PATCH 03/15] Finish lines by clicking last point --- src/ol/interaction/drawinteraction.js | 28 ++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index bc626db88b..cf033fb53f 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -56,11 +56,12 @@ ol.interaction.Draw = function(options) { this.mode_ = options.mode; /** - * Start coordinate for the feature. + * Finish coordinate for the feature (first point for polygons, last point for + * linestrings). * @type {ol.Coordinate} * @private */ - this.startCoordinate_ = null; + this.finishCoordinate_ = null; /** * Sketch feature. @@ -102,7 +103,7 @@ ol.interaction.Draw.prototype.setMap = function(map) { } else { // removing from a map, clean up this.sketchLayer_ = null; - this.startCoordinate_ = null; + this.finishCoordinate_ = null; this.sketchFeature_ = null; } @@ -135,10 +136,10 @@ ol.interaction.Draw.prototype.handleMapBrowserEvent = function(event) { * @private */ ol.interaction.Draw.prototype.handleClick_ = function(event) { - if (goog.isNull(this.startCoordinate_)) { + if (goog.isNull(this.finishCoordinate_)) { this.startDrawing_(event); } else if (this.mode_ === ol.interaction.DrawMode.POINT || - this.atStart_(event)) { + this.atFinish_(event)) { this.finishDrawing_(event); } else { this.addToDrawing_(event); @@ -155,9 +156,9 @@ ol.interaction.Draw.prototype.handleClick_ = function(event) { */ ol.interaction.Draw.prototype.handleMove_ = function(event) { if (this.mode_ === ol.interaction.DrawMode.POINT && - goog.isNull(this.startCoordinate_)) { + goog.isNull(this.finishCoordinate_)) { this.startDrawing_(event); - } else if (!goog.isNull(this.startCoordinate_)) { + } else if (!goog.isNull(this.finishCoordinate_)) { this.modifyDrawing_(event); } return true; @@ -170,12 +171,12 @@ ol.interaction.Draw.prototype.handleMove_ = function(event) { * @return {boolean} The event is within the snapping tolerance of the start. * @private */ -ol.interaction.Draw.prototype.atStart_ = function(event) { +ol.interaction.Draw.prototype.atFinish_ = function(event) { var map = event.map; - var startPixel = map.getPixelFromCoordinate(this.startCoordinate_); + var finishPixel = map.getPixelFromCoordinate(this.finishCoordinate_); var pixel = event.getPixel(); - var dx = pixel[0] - startPixel[0]; - var dy = pixel[1] - startPixel[1]; + var dx = pixel[0] - finishPixel[0]; + var dy = pixel[1] - finishPixel[1]; return Math.sqrt(dx * dx + dy * dy) <= this.snapTolerance_; }; @@ -187,7 +188,7 @@ ol.interaction.Draw.prototype.atStart_ = function(event) { */ ol.interaction.Draw.prototype.startDrawing_ = function(event) { var start = event.getCoordinate(); - this.startCoordinate_ = start; + this.finishCoordinate_ = start; var sketchFeature = new ol.Feature(); sketchFeature.setRenderIntent(ol.layer.VectorLayerRenderIntent.SELECTED); var features = [sketchFeature]; @@ -260,6 +261,7 @@ ol.interaction.Draw.prototype.addToDrawing_ = function(event) { var geometry = this.sketchFeature_.getGeometry(); var coordinates, last; if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { + this.finishCoordinate_ = coordinate.slice(); coordinates = geometry.getCoordinates(); coordinates.push(coordinate.slice()); geometry.setCoordinates(coordinates); @@ -278,7 +280,7 @@ ol.interaction.Draw.prototype.addToDrawing_ = function(event) { * @private */ ol.interaction.Draw.prototype.finishDrawing_ = function(event) { - this.startCoordinate_ = null; + this.finishCoordinate_ = null; var sketchFeature = this.sketchFeature_; var features = [sketchFeature]; if (this.mode_ !== ol.interaction.DrawMode.POINT) { From b5cc35ee043ad57c8b9590f38ac67a47b7c5741e Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Wed, 30 Oct 2013 16:01:26 -0600 Subject: [PATCH 04/15] Snap to finish with configurable tolerance --- src/objectliterals.jsdoc | 2 ++ src/ol/interaction/drawinteraction.js | 28 +++++++++++++++------------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index be482df52e..55dcb25881 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -344,6 +344,8 @@ /** * @typedef {Object} ol.interaction.DrawOptions * @property {ol.layer.Vector} layer Destination layer for the features. + * @property {number|undefined} snapTolerance Pixel distance for snapping to the + * drawing finish (default is 12). * @property {ol.interaction.DrawMode} mode Drawing mode ('point', 'linestring', * or 'polygon'). * @todo stability experimental diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index cf033fb53f..3ea59ea0a5 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -46,7 +46,8 @@ ol.interaction.Draw = function(options) { * @type {number} * @private */ - this.snapTolerance_ = 15; + this.snapTolerance_ = goog.isDef(options.snapTolerance) ? + options.snapTolerance : 12; /** * Draw mode. @@ -232,21 +233,24 @@ ol.interaction.Draw.prototype.modifyDrawing_ = function(event) { last[1] = coordinate[1]; geometry.setCoordinates(last); } else { - this.sketchPoint_.getGeometry().setCoordinates(coordinate); + var potentiallyDone = false; if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { coordinates = geometry.getCoordinates(); - last = coordinates[coordinates.length - 1]; - last[0] = coordinate[0]; - last[1] = coordinate[1]; - geometry.setCoordinates(coordinates); + potentiallyDone = coordinates.length > 2; } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { - var ring = geometry.getRings()[0]; - coordinates = ring.getCoordinates(); - last = coordinates[coordinates.length - 1]; - last[0] = coordinate[0]; - last[1] = coordinate[1]; - ring.setCoordinates(coordinates); + geometry = geometry.getRings()[0]; + coordinates = geometry.getCoordinates(); + potentiallyDone = coordinates.length > 3; } + if (potentiallyDone && this.atFinish_(event)) { + // snap to finish + coordinate = this.finishCoordinate_.slice(); + } + this.sketchPoint_.getGeometry().setCoordinates(coordinate); + last = coordinates[coordinates.length - 1]; + last[0] = coordinate[0]; + last[1] = coordinate[1]; + geometry.setCoordinates(coordinates); } }; From 45ba000df5a7bf7a86e7fe34e3f7574f8f66a75f Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Wed, 30 Oct 2013 16:07:39 -0600 Subject: [PATCH 05/15] Pop off last point from linestring --- src/ol/interaction/drawinteraction.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index 3ea59ea0a5..5209b33d44 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -292,6 +292,13 @@ ol.interaction.Draw.prototype.finishDrawing_ = function(event) { } this.sketchLayer_.removeFeatures(features); sketchFeature.setRenderIntent(ol.layer.VectorLayerRenderIntent.DEFAULT); + var geometry = sketchFeature.getGeometry(); + if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { + var geometry = sketchFeature.getGeometry(); + var coordinates = geometry.getCoordinates(); + coordinates.pop(); + geometry.setCoordinates(coordinates); + } this.layer_.addFeatures([sketchFeature]); }; From 7c197252ed3ce83dcfd2c166cbc1b50a39fb1674 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Wed, 30 Oct 2013 16:17:18 -0600 Subject: [PATCH 06/15] Ensure drawing is not finished prematurely --- src/ol/interaction/drawinteraction.js | 30 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index 5209b33d44..b215d73103 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -173,12 +173,25 @@ ol.interaction.Draw.prototype.handleMove_ = function(event) { * @private */ ol.interaction.Draw.prototype.atFinish_ = function(event) { - 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]; - return Math.sqrt(dx * dx + dy * dy) <= this.snapTolerance_; + var at = false; + if (this.sketchFeature_) { + var geometry = this.sketchFeature_.getGeometry(); + var potentiallyDone = false; + if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { + potentiallyDone = geometry.getCoordinates().length > 2; + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + potentiallyDone = geometry.getRings()[0].getCoordinates().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; }; @@ -233,16 +246,13 @@ ol.interaction.Draw.prototype.modifyDrawing_ = function(event) { last[1] = coordinate[1]; geometry.setCoordinates(last); } else { - var potentiallyDone = false; if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { coordinates = geometry.getCoordinates(); - potentiallyDone = coordinates.length > 2; } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { geometry = geometry.getRings()[0]; coordinates = geometry.getCoordinates(); - potentiallyDone = coordinates.length > 3; } - if (potentiallyDone && this.atFinish_(event)) { + if (this.atFinish_(event)) { // snap to finish coordinate = this.finishCoordinate_.slice(); } From 048dec1644a6d4ae8af98ce096164632cf175564 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 31 Oct 2013 09:51:10 -0600 Subject: [PATCH 07/15] Updated sketch style --- examples/draw-features.js | 27 ++++++++++--------------- src/ol/interaction/drawinteraction.js | 2 +- src/ol/layer/vectorlayerrenderintent.js | 3 ++- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/examples/draw-features.js b/examples/draw-features.js index 4bb1617f92..7e7b2fc7e9 100644 --- a/examples/draw-features.js +++ b/examples/draw-features.js @@ -35,6 +35,10 @@ var vector = new ol.layer.Vector({ color: '#ffffff', opacity: 0.5 }), + new ol.style.Stroke({ + color: 'white', + width: 5 + }), new ol.style.Stroke({ color: '#0099ff', width: 3 @@ -43,26 +47,17 @@ var vector = new ol.layer.Vector({ }), new ol.style.Rule({ filter: 'renderIntent("temporary")', - symbolizers: [ - new ol.style.Shape({ - fill: new ol.style.Fill({ - color: '#0033ff', - opacity: 1 - }), - size: 16, - zIndex: 1 - }) - ] - }), - new ol.style.Rule({ - filter: 'renderIntent("future")', symbolizers: [ new ol.style.Shape({ fill: new ol.style.Fill({ color: '#0099ff', opacity: 1 }), - size: 16, + stroke: new ol.style.Stroke({ + color: 'white', + opacity: 0.75 + }), + size: 14, zIndex: 1 }) ] @@ -74,10 +69,10 @@ var vector = new ol.layer.Vector({ color: '#0033ff', opacity: 1 }), - size: 16 + size: 14 }), new ol.style.Fill({ - color: '#ffffff', + color: 'white', opacity: 0.2 }), new ol.style.Stroke({ diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index b215d73103..13bd1d9c34 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -213,7 +213,7 @@ ol.interaction.Draw.prototype.startDrawing_ = function(event) { var sketchPoint = new ol.Feature({ geom: new ol.geom.Point(start.slice()) }); - sketchPoint.setRenderIntent(ol.layer.VectorLayerRenderIntent.FUTURE); + sketchPoint.setRenderIntent(ol.layer.VectorLayerRenderIntent.TEMPORARY); this.sketchPoint_ = sketchPoint; features.push(sketchPoint); diff --git a/src/ol/layer/vectorlayerrenderintent.js b/src/ol/layer/vectorlayerrenderintent.js index 757fef5aba..f877dc1732 100644 --- a/src/ol/layer/vectorlayerrenderintent.js +++ b/src/ol/layer/vectorlayerrenderintent.js @@ -7,5 +7,6 @@ goog.provide('ol.layer.VectorLayerRenderIntent'); ol.layer.VectorLayerRenderIntent = { DEFAULT: 'default', HIDDEN: 'hidden', - SELECTED: 'selected' + SELECTED: 'selected', + TEMPORARY: 'temporary' }; From 4e98e86b141150e0fa51a902cfceff5e9bac1abc Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 31 Oct 2013 09:51:29 -0600 Subject: [PATCH 08/15] Stop dblclick propagation while editing --- src/ol/interaction/drawinteraction.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index 13bd1d9c34..28125aed3a 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -125,6 +125,8 @@ ol.interaction.Draw.prototype.handleMapBrowserEvent = function(event) { 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; }; From e3faa7677057a251ab2a27c7417587c6cf6c1654 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 1 Nov 2013 11:21:52 -0600 Subject: [PATCH 09/15] Add method to abort drawing --- src/ol/interaction/drawinteraction.js | 40 +++++++++++++++++++-------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index 28125aed3a..99681c95e2 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -103,9 +103,8 @@ ol.interaction.Draw.prototype.setMap = function(map) { map.addLayer(this.sketchLayer_); } else { // removing from a map, clean up + this.abortDrawing_(); this.sketchLayer_ = null; - this.finishCoordinate_ = null; - this.sketchFeature_ = null; } goog.base(this, 'setMap', map); @@ -291,20 +290,14 @@ ol.interaction.Draw.prototype.addToDrawing_ = function(event) { /** - * Finish the drawing. + * Stop drawing and add the sketch feature to the target layer. * @param {ol.MapBrowserEvent} event Event. * @private */ ol.interaction.Draw.prototype.finishDrawing_ = function(event) { - this.finishCoordinate_ = null; - var sketchFeature = this.sketchFeature_; - var features = [sketchFeature]; - if (this.mode_ !== ol.interaction.DrawMode.POINT) { - features.push(this.sketchPoint_); - } - this.sketchLayer_.removeFeatures(features); + var sketchFeature = this.abortDrawing_(); + goog.asserts.assert(!goog.isNull(sketchFeature)); sketchFeature.setRenderIntent(ol.layer.VectorLayerRenderIntent.DEFAULT); - var geometry = sketchFeature.getGeometry(); if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { var geometry = sketchFeature.getGeometry(); var coordinates = geometry.getCoordinates(); @@ -315,12 +308,37 @@ ol.interaction.Draw.prototype.finishDrawing_ = function(event) { }; +/** + * 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)) { + var features = [sketchFeature]; + this.sketchFeature_ = null; + if (this.mode_ !== ol.interaction.DrawMode.POINT) { + features.push(this.sketchPoint_); + this.sketchPoint_ = null; + } + this.sketchLayer_.removeFeatures(features); + } + return sketchFeature; +}; + + /** * Set the drawing mode. + * TODO: Decide if we want interactions to be purely event driven - if so, this + * method would be removed, and users would remove this interaction and create a + * new one with the desired mode instead. * @param {ol.interaction.DrawMode} mode Draw mode ('point', 'linestring', or * 'polygon'). */ ol.interaction.Draw.prototype.setMode = function(mode) { + this.abortDrawing_(); this.mode_ = mode; }; From 2f074335932675a38f1d4cd0045b99faf2147cb4 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 1 Nov 2013 11:23:31 -0600 Subject: [PATCH 10/15] Allow user to set drawing mode --- examples/draw-features.html | 9 +++++++++ examples/draw-features.js | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/examples/draw-features.html b/examples/draw-features.html index 11a34094d4..2ad504106d 100644 --- a/examples/draw-features.html +++ b/examples/draw-features.html @@ -33,6 +33,15 @@

Draw features example

Example of using the Draw interaction.

+
+ + +
+

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

diff --git a/examples/draw-features.js b/examples/draw-features.js index 7e7b2fc7e9..5d78eedc8c 100644 --- a/examples/draw-features.js +++ b/examples/draw-features.js @@ -83,11 +83,24 @@ var vector = new ol.layer.Vector({ }) }); +var modeSelect = document.getElementById('mode'); + var draw = new ol.interaction.Draw({ layer: vector, - mode: /** @type {ol.interaction.DrawMode} */ ('polygon') + mode: /** @type {ol.interaction.DrawMode} */ + (modeSelect.options[modeSelect.selectedIndex].value) }); + +/** + * Let user change the draw mode. + * @param {Event} e Change event. + */ +modeSelect.onchange = function(e) { + draw.setMode(/** @type {ol.interaction.DrawMode} */ + (modeSelect.options[modeSelect.selectedIndex].value)); +}; + var map = new ol.Map({ interactions: ol.interaction.defaults().extend([draw]), layers: [raster, vector], From 62b44f3c7352365b9487928e406b8977fb4b0906 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Mon, 11 Nov 2013 14:35:47 -0700 Subject: [PATCH 11/15] Add click tolerance to allow dragging while drawing --- src/ol/interaction/drawinteraction.js | 34 ++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index 99681c95e2..28070b188a 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -78,6 +78,15 @@ ol.interaction.Draw = function(options) { */ this.sketchPoint_ = 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; + }; goog.inherits(ol.interaction.Draw, ol.interaction.Interaction); @@ -138,15 +147,24 @@ ol.interaction.Draw.prototype.handleMapBrowserEvent = function(event) { * @private */ ol.interaction.Draw.prototype.handleClick_ = function(event) { - 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); + 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 false; + return pass; }; From dbcfdbc76fdabd0fd894367c2afd62e74550d056 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Mon, 11 Nov 2013 14:44:57 -0700 Subject: [PATCH 12/15] Updated style for draw example --- examples/draw-features.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/draw-features.js b/examples/draw-features.js index 5d78eedc8c..f9285538d5 100644 --- a/examples/draw-features.js +++ b/examples/draw-features.js @@ -29,7 +29,11 @@ var vector = new ol.layer.Vector({ color: '#0099ff', opacity: 1 }), - size: 16 + stroke: new ol.style.Stroke({ + color: 'white', + opacity: 0.75 + }), + size: 14 }), new ol.style.Fill({ color: '#ffffff', @@ -66,7 +70,7 @@ var vector = new ol.layer.Vector({ symbolizers: [ new ol.style.Shape({ fill: new ol.style.Fill({ - color: '#0033ff', + color: '#ffcc33', opacity: 1 }), size: 14 @@ -77,7 +81,7 @@ var vector = new ol.layer.Vector({ }), new ol.style.Stroke({ color: '#ffcc33', - width: 1.5 + width: 2 }) ] }) From 8194ba9f0ae6044fd4e6d7fd34f48f65046be642 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Mon, 11 Nov 2013 16:11:52 -0700 Subject: [PATCH 13/15] Remove setMode method from interaction in favor of add/removeInteraction This makes the example a bit more awkward, but eventually adding and removing interactions will be the job of an editing control. --- examples/draw-features.js | 43 +++++++++++++++------------ src/ol/interaction/drawinteraction.js | 14 --------- src/ol/map.exports | 2 ++ 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/examples/draw-features.js b/examples/draw-features.js index f9285538d5..e33ff3a3bd 100644 --- a/examples/draw-features.js +++ b/examples/draw-features.js @@ -87,26 +87,7 @@ var vector = new ol.layer.Vector({ }) }); -var modeSelect = document.getElementById('mode'); - -var draw = new ol.interaction.Draw({ - layer: vector, - mode: /** @type {ol.interaction.DrawMode} */ - (modeSelect.options[modeSelect.selectedIndex].value) -}); - - -/** - * Let user change the draw mode. - * @param {Event} e Change event. - */ -modeSelect.onchange = function(e) { - draw.setMode(/** @type {ol.interaction.DrawMode} */ - (modeSelect.options[modeSelect.selectedIndex].value)); -}; - var map = new ol.Map({ - interactions: ol.interaction.defaults().extend([draw]), layers: [raster, vector], renderer: ol.RendererHint.CANVAS, target: 'map', @@ -115,3 +96,27 @@ var map = new ol.Map({ zoom: 4 }) }); + +var modeSelect = document.getElementById('mode'); + +var draw; // global so we can remove it later +function addInteraction() { + draw = new ol.interaction.Draw({ + layer: vector, + mode: /** @type {ol.interaction.DrawMode} */ + (modeSelect.options[modeSelect.selectedIndex].value) + }); + map.addInteraction(draw); +} + + +/** + * Let user change the draw mode. + * @param {Event} e Change event. + */ +modeSelect.onchange = function(e) { + map.removeInteraction(draw); + addInteraction(); +}; + +addInteraction(); diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index 28070b188a..03807276a5 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -347,20 +347,6 @@ ol.interaction.Draw.prototype.abortDrawing_ = function() { }; -/** - * Set the drawing mode. - * TODO: Decide if we want interactions to be purely event driven - if so, this - * method would be removed, and users would remove this interaction and create a - * new one with the desired mode instead. - * @param {ol.interaction.DrawMode} mode Draw mode ('point', 'linestring', or - * 'polygon'). - */ -ol.interaction.Draw.prototype.setMode = function(mode) { - this.abortDrawing_(); - this.mode_ = mode; -}; - - /** * Draw mode. * @enum {string} diff --git a/src/ol/map.exports b/src/ol/map.exports index d6dd963a75..68ff166ae7 100644 --- a/src/ol/map.exports +++ b/src/ol/map.exports @@ -1,5 +1,6 @@ @exportClass ol.Map ol.MapOptions @exportProperty ol.Map.prototype.addControl +@exportProperty ol.Map.prototype.addInteraction @exportProperty ol.Map.prototype.addLayer @exportProperty ol.Map.prototype.addOverlay @exportProperty ol.Map.prototype.beforeRender @@ -14,6 +15,7 @@ @exportProperty ol.Map.prototype.getRenderer @exportProperty ol.Map.prototype.getViewport @exportProperty ol.Map.prototype.removeControl +@exportProperty ol.Map.prototype.removeInteraction @exportProperty ol.Map.prototype.removeLayer @exportProperty ol.Map.prototype.removeOverlay @exportProperty ol.Map.prototype.updateSize From 5a898884ecc60a91f63f16b144ce33f054d67526 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 12 Nov 2013 11:30:32 -0700 Subject: [PATCH 14/15] Use geometry type enum for configuring draw interaction This allows us to cast single-part geometries to multi-part types before adding features to the target layer. This doesn't yet allow for drawing multi-part geometries with multiple parts. That can be handled separately. --- examples/draw-features.html | 2 +- examples/draw-features.js | 10 ++--- src/objectliterals.jsdoc | 4 +- src/ol/interaction/drawinteraction.js | 57 +++++++++++++++++++++++++-- 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/examples/draw-features.html b/examples/draw-features.html index 2ad504106d..5459365499 100644 --- a/examples/draw-features.html +++ b/examples/draw-features.html @@ -35,7 +35,7 @@

Example of using the Draw interaction.

- diff --git a/examples/draw-features.js b/examples/draw-features.js index e33ff3a3bd..637035f756 100644 --- a/examples/draw-features.js +++ b/examples/draw-features.js @@ -97,24 +97,24 @@ var map = new ol.Map({ }) }); -var modeSelect = document.getElementById('mode'); +var typeSelect = document.getElementById('type'); var draw; // global so we can remove it later function addInteraction() { draw = new ol.interaction.Draw({ layer: vector, - mode: /** @type {ol.interaction.DrawMode} */ - (modeSelect.options[modeSelect.selectedIndex].value) + type: /** @type {ol.geom.GeometryType} */ + (typeSelect.options[typeSelect.selectedIndex].value) }); map.addInteraction(draw); } /** - * Let user change the draw mode. + * Let user change the geometry type. * @param {Event} e Change event. */ -modeSelect.onchange = function(e) { +typeSelect.onchange = function(e) { map.removeInteraction(draw); addInteraction(); }; diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index 55dcb25881..e39f3b5156 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -346,8 +346,8 @@ * @property {ol.layer.Vector} layer Destination layer for the features. * @property {number|undefined} snapTolerance Pixel distance for snapping to the * drawing finish (default is 12). - * @property {ol.interaction.DrawMode} mode Drawing mode ('point', 'linestring', - * or 'polygon'). + * @property {ol.geom.GeometryType} type Drawing type ('point', 'linestring', + * 'polygon', 'multipoint', 'multilinestring', or 'multipolygon'). * @todo stability experimental */ diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index 03807276a5..f828a06eac 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -1,5 +1,4 @@ goog.provide('ol.interaction.Draw'); -goog.provide('ol.interaction.DrawMode'); goog.require('goog.asserts'); @@ -8,7 +7,11 @@ goog.require('ol.Feature'); goog.require('ol.Map'); goog.require('ol.MapBrowserEvent'); goog.require('ol.MapBrowserEvent.EventType'); +goog.require('ol.geom.GeometryType'); goog.require('ol.geom.LineString'); +goog.require('ol.geom.MultiLineString'); +goog.require('ol.geom.MultiPoint'); +goog.require('ol.geom.MultiPolygon'); goog.require('ol.geom.Point'); goog.require('ol.geom.Polygon'); goog.require('ol.interaction.Interaction'); @@ -50,11 +53,18 @@ ol.interaction.Draw = function(options) { options.snapTolerance : 12; /** - * Draw mode. + * Geometry type. + * @type {ol.geom.GeometryType} + * @private + */ + this.type_ = options.type; + + /** + * Drawing mode (derived from geometry type. * @type {ol.interaction.DrawMode} * @private */ - this.mode_ = options.mode; + this.mode_ = ol.interaction.Draw.getMode_(this.type_); /** * Finish coordinate for the feature (first point for polygons, last point for @@ -322,6 +332,20 @@ ol.interaction.Draw.prototype.finishDrawing_ = function(event) { coordinates.pop(); geometry.setCoordinates(coordinates); } + // cast multi-part geometries + if (this.type_ === ol.geom.GeometryType.MULTIPOINT) { + sketchFeature.setGeometry( + new ol.geom.MultiPoint( + [sketchFeature.getGeometry().getCoordinates()])); + } else if (this.type_ === ol.geom.GeometryType.MULTILINESTRING) { + sketchFeature.setGeometry( + new ol.geom.MultiLineString( + [sketchFeature.getGeometry().getCoordinates()])); + } else if (this.type_ === ol.geom.GeometryType.MULTIPOLYGON) { + sketchFeature.setGeometry( + new ol.geom.MultiPolygon( + [sketchFeature.getGeometry().getCoordinates()])); + } this.layer_.addFeatures([sketchFeature]); }; @@ -348,7 +372,32 @@ ol.interaction.Draw.prototype.abortDrawing_ = function() { /** - * Draw mode. + * 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.MULTIPOINT) { + mode = ol.interaction.DrawMode.POINT; + } else if (type === ol.geom.GeometryType.LINESTRING || + type === ol.geom.GeometryType.MULTILINESTRING) { + mode = ol.interaction.DrawMode.LINESTRING; + } else if (type === ol.geom.GeometryType.POLYGON || + type === ol.geom.GeometryType.MULTIPOLYGON) { + 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 = { From 05b99228610cc7feaf6c8305ffca2ff9bdc0d29f Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 12 Nov 2013 15:49:03 -0700 Subject: [PATCH 15/15] Tests for draw interaction The tests revealed that polygons can be drawn with counter-clockwise exterior ring order. In the polygon constructor, we enforce clockwise winding for exterior rings. So after drawing, we pass polygon coordinates back to the constructor before adding feature to the target layer. --- src/ol/interaction/drawinteraction.js | 20 +- .../ol/interaction/drawinteraction.test.js | 344 ++++++++++++++++++ 2 files changed, 353 insertions(+), 11 deletions(-) create mode 100644 test/spec/ol/interaction/drawinteraction.test.js diff --git a/src/ol/interaction/drawinteraction.js b/src/ol/interaction/drawinteraction.js index f828a06eac..274fa87977 100644 --- a/src/ol/interaction/drawinteraction.js +++ b/src/ol/interaction/drawinteraction.js @@ -326,25 +326,23 @@ ol.interaction.Draw.prototype.finishDrawing_ = function(event) { var sketchFeature = this.abortDrawing_(); goog.asserts.assert(!goog.isNull(sketchFeature)); sketchFeature.setRenderIntent(ol.layer.VectorLayerRenderIntent.DEFAULT); + var geometry = sketchFeature.getGeometry(); + var coordinates = geometry.getCoordinates(); if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { - var geometry = sketchFeature.getGeometry(); - var coordinates = geometry.getCoordinates(); + // remove the redundant last point coordinates.pop(); geometry.setCoordinates(coordinates); + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + // force clockwise order for exterior ring + sketchFeature.setGeometry(new ol.geom.Polygon(coordinates)); } // cast multi-part geometries if (this.type_ === ol.geom.GeometryType.MULTIPOINT) { - sketchFeature.setGeometry( - new ol.geom.MultiPoint( - [sketchFeature.getGeometry().getCoordinates()])); + sketchFeature.setGeometry(new ol.geom.MultiPoint([coordinates])); } else if (this.type_ === ol.geom.GeometryType.MULTILINESTRING) { - sketchFeature.setGeometry( - new ol.geom.MultiLineString( - [sketchFeature.getGeometry().getCoordinates()])); + sketchFeature.setGeometry(new ol.geom.MultiLineString([coordinates])); } else if (this.type_ === ol.geom.GeometryType.MULTIPOLYGON) { - sketchFeature.setGeometry( - new ol.geom.MultiPolygon( - [sketchFeature.getGeometry().getCoordinates()])); + sketchFeature.setGeometry(new ol.geom.MultiPolygon([coordinates])); } this.layer_.addFeatures([sketchFeature]); }; diff --git a/test/spec/ol/interaction/drawinteraction.test.js b/test/spec/ol/interaction/drawinteraction.test.js new file mode 100644 index 0000000000..f4c27f6b42 --- /dev/null +++ b/test/spec/ol/interaction/drawinteraction.test.js @@ -0,0 +1,344 @@ +goog.provide('ol.test.interaction.Draw'); + +describe('ol.interaction.Draw', function() { + var target, map, vector; + + var width = 360; + var height = 180; + + beforeEach(function() { + target = document.createElement('div'); + var style = target.style; + style.position = 'absolute'; + style.left = '-1000px'; + style.top = '-1000px'; + style.width = width + 'px'; + style.height = height + 'px'; + document.body.appendChild(target); + vector = new ol.layer.Vector({source: new ol.source.Vector({})}); + map = new ol.Map({ + target: target, + renderer: ol.RendererHint.CANVAS, + layers: [vector], + view: new ol.View2D({ + projection: 'EPSG:4326', + center: [0, 0], + resolution: 1 + }) + }); + }); + + afterEach(function() { + goog.dispose(map); + document.body.removeChild(target); + }); + + /** + * Simulates a browser event on the map viewport. The client x/y location + * will be adjusted as if the map were centered at 0,0. + * @param {string} type Event type. + * @param {number} x Horizontal offset from map center. + * @param {number} y Vertical offset from map center. + */ + function simulateEvent(type, x, y) { + var viewport = map.getViewport(); + // calculated in case body has top < 0 (test runner with small window) + var position = goog.style.getClientPosition(viewport); + var event = new goog.events.BrowserEvent({ + type: type, + clientX: position.x + x + width / 2, + clientY: position.y + y + height / 2 + }); + goog.events.fireListeners(viewport, type, false, event); + } + + describe('constructor', function() { + + it('creates a new interaction', function() { + var draw = new ol.interaction.Draw({ + layer: vector, + type: ol.geom.GeometryType.POINT + }); + expect(draw).to.be.a(ol.interaction.Draw); + expect(draw).to.be.a(ol.interaction.Interaction); + }); + + }); + + describe('drawing points', function() { + + beforeEach(function() { + map.addInteraction(new ol.interaction.Draw({ + layer: vector, + type: ol.geom.GeometryType.POINT + })); + }); + + it('draws a point on click', function() { + simulateEvent('mousemove', 10, 20); + simulateEvent('mousedown', 10, 20); + simulateEvent('mouseup', 10, 20); + simulateEvent('click', 10, 20); + var features = vector.getFeatures(); + expect(features).to.have.length(1); + var geometry = features[0].getGeometry(); + expect(geometry).to.be.a(ol.geom.Point); + expect(geometry.getCoordinates()).to.eql([10, -20]); + }); + + it('does not draw a point with a significant drag', function() { + simulateEvent('mousemove', 10, 20); + simulateEvent('mousedown', 10, 20); + simulateEvent('mousemove', 15, 20); + simulateEvent('mouseup', 15, 20); + simulateEvent('click', 15, 20); + var features = vector.getFeatures(); + expect(features).to.have.length(0); + }); + + }); + + describe('drawing multipoints', function() { + + beforeEach(function() { + map.addInteraction(new ol.interaction.Draw({ + layer: vector, + type: ol.geom.GeometryType.MULTIPOINT + })); + }); + + it('draws multipoint on click', function() { + simulateEvent('mousemove', 30, 15); + simulateEvent('mousedown', 30, 15); + simulateEvent('mouseup', 30, 15); + simulateEvent('click', 30, 15); + var features = vector.getFeatures(); + expect(features).to.have.length(1); + var geometry = features[0].getGeometry(); + expect(geometry).to.be.a(ol.geom.MultiPoint); + expect(geometry.getCoordinates()).to.eql([[30, -15]]); + }); + + }); + + describe('drawing linestrings', function() { + + beforeEach(function() { + map.addInteraction(new ol.interaction.Draw({ + layer: vector, + type: ol.geom.GeometryType.LINESTRING + })); + }); + + it('draws linestring with clicks, finishing on last point', function() { + // first point + simulateEvent('mousemove', 10, 20); + simulateEvent('mousedown', 10, 20); + simulateEvent('mouseup', 10, 20); + simulateEvent('click', 10, 20); + + // second point + simulateEvent('mousemove', 30, 20); + simulateEvent('mousedown', 30, 20); + simulateEvent('mouseup', 30, 20); + simulateEvent('click', 30, 20); + + // finish on second point + simulateEvent('mousedown', 30, 20); + simulateEvent('mouseup', 30, 20); + simulateEvent('click', 30, 20); + + var features = vector.getFeatures(); + expect(features).to.have.length(1); + var geometry = features[0].getGeometry(); + expect(geometry).to.be.a(ol.geom.LineString); + expect(geometry.getCoordinates()).to.eql([[10, -20], [30, -20]]); + }); + + it('does not add a point with a significant drag', function() { + // first point + simulateEvent('mousemove', 10, 20); + simulateEvent('mousedown', 10, 20); + simulateEvent('mouseup', 10, 20); + simulateEvent('click', 10, 20); + + // drag map + simulateEvent('mousemove', 15, 20); + simulateEvent('mousedown', 15, 20); + simulateEvent('mousemove', 20, 20); + simulateEvent('mouseup', 20, 20); + simulateEvent('click', 20, 20); + + + // second point + simulateEvent('mousemove', 30, 20); + simulateEvent('mousedown', 30, 20); + simulateEvent('mouseup', 30, 20); + simulateEvent('click', 30, 20); + + // finish on second point + simulateEvent('mousedown', 30, 20); + simulateEvent('mouseup', 30, 20); + simulateEvent('click', 30, 20); + + var features = vector.getFeatures(); + expect(features).to.have.length(1); + var geometry = features[0].getGeometry(); + expect(geometry).to.be.a(ol.geom.LineString); + expect(geometry.getCoordinates()).to.eql([[10, -20], [30, -20]]); + }); + + }); + + describe('drawing multi-linestrings', function() { + + beforeEach(function() { + map.addInteraction(new ol.interaction.Draw({ + layer: vector, + type: ol.geom.GeometryType.MULTILINESTRING + })); + }); + + it('draws multi with clicks, finishing on last point', function() { + // first point + simulateEvent('mousemove', 10, 20); + simulateEvent('mousedown', 10, 20); + simulateEvent('mouseup', 10, 20); + simulateEvent('click', 10, 20); + + // second point + simulateEvent('mousemove', 30, 20); + simulateEvent('mousedown', 30, 20); + simulateEvent('mouseup', 30, 20); + simulateEvent('click', 30, 20); + + // finish on second point + simulateEvent('mousedown', 30, 20); + simulateEvent('mouseup', 30, 20); + simulateEvent('click', 30, 20); + + var features = vector.getFeatures(); + expect(features).to.have.length(1); + var geometry = features[0].getGeometry(); + expect(geometry).to.be.a(ol.geom.MultiLineString); + expect(geometry.getCoordinates()).to.eql([[[10, -20], [30, -20]]]); + }); + + }); + + describe('drawing polygons', function() { + + beforeEach(function() { + map.addInteraction(new ol.interaction.Draw({ + layer: vector, + type: ol.geom.GeometryType.POLYGON + })); + }); + + it('draws polygon with clicks, finishing on first point', function() { + // first point + simulateEvent('mousemove', 10, 20); + simulateEvent('mousedown', 10, 20); + simulateEvent('mouseup', 10, 20); + simulateEvent('click', 10, 20); + + // second point + simulateEvent('mousemove', 30, 20); + simulateEvent('mousedown', 30, 20); + simulateEvent('mouseup', 30, 20); + simulateEvent('click', 30, 20); + + // third point + simulateEvent('mousemove', 30, 10); + simulateEvent('mousedown', 30, 10); + simulateEvent('mouseup', 30, 10); + simulateEvent('click', 30, 10); + + // finish on first point + simulateEvent('mousemove', 10, 20); + simulateEvent('mousedown', 10, 20); + simulateEvent('mouseup', 10, 20); + simulateEvent('click', 10, 20); + + var features = vector.getFeatures(); + expect(features).to.have.length(1); + var geometry = features[0].getGeometry(); + expect(geometry).to.be.a(ol.geom.Polygon); + + // note that order is forced clockwise (despite drawing counter-clockwise) + expect(geometry.getCoordinates()).to.eql([ + [[10, -20], [30, -10], [30, -20], [10, -20]] + ]); + }); + + }); + + describe('drawing multi-polygons', function() { + + beforeEach(function() { + map.addInteraction(new ol.interaction.Draw({ + layer: vector, + type: ol.geom.GeometryType.MULTIPOLYGON + })); + }); + + it('draws multi with clicks, finishing on first point', function() { + // first point + simulateEvent('mousemove', 10, 20); + simulateEvent('mousedown', 10, 20); + simulateEvent('mouseup', 10, 20); + simulateEvent('click', 10, 20); + + // second point + simulateEvent('mousemove', 30, 20); + simulateEvent('mousedown', 30, 20); + simulateEvent('mouseup', 30, 20); + simulateEvent('click', 30, 20); + + // third point + simulateEvent('mousemove', 30, 10); + simulateEvent('mousedown', 30, 10); + simulateEvent('mouseup', 30, 10); + simulateEvent('click', 30, 10); + + // finish on first point + simulateEvent('mousemove', 10, 20); + simulateEvent('mousedown', 10, 20); + simulateEvent('mouseup', 10, 20); + simulateEvent('click', 10, 20); + + var features = vector.getFeatures(); + expect(features).to.have.length(1); + var geometry = features[0].getGeometry(); + expect(geometry).to.be.a(ol.geom.MultiPolygon); + var coordinates = geometry.getCoordinates(); + expect(coordinates).to.have.length(1); + + // note that order is forced clockwise (despite drawing counter-clockwise) + expect(coordinates[0]).to.eql([ + [[10, -20], [30, -10], [30, -20], [10, -20]] + ]); + }); + + }); + +}); + +goog.require('goog.dispose'); +goog.require('goog.events'); +goog.require('goog.events.BrowserEvent'); +goog.require('goog.style'); +goog.require('ol.Map'); +goog.require('ol.RendererHint'); +goog.require('ol.View2D'); +goog.require('ol.geom.GeometryType'); +goog.require('ol.geom.LineString'); +goog.require('ol.geom.MultiLineString'); +goog.require('ol.geom.MultiPoint'); +goog.require('ol.geom.MultiPolygon'); +goog.require('ol.geom.Point'); +goog.require('ol.geom.Polygon'); +goog.require('ol.interaction.Draw'); +goog.require('ol.interaction.Interaction'); +goog.require('ol.layer.Vector'); +goog.require('ol.source.Vector');