diff --git a/examples/draw-features.html b/examples/draw-features.html new file mode 100644 index 0000000000..5459365499 --- /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..637035f756 --- /dev/null +++ b/examples/draw-features.js @@ -0,0 +1,122 @@ +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 + }), + stroke: new ol.style.Stroke({ + color: 'white', + opacity: 0.75 + }), + size: 14 + }), + new ol.style.Fill({ + color: '#ffffff', + opacity: 0.5 + }), + new ol.style.Stroke({ + color: 'white', + width: 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: '#0099ff', + opacity: 1 + }), + stroke: new ol.style.Stroke({ + color: 'white', + opacity: 0.75 + }), + size: 14, + zIndex: 1 + }) + ] + }) + ], + symbolizers: [ + new ol.style.Shape({ + fill: new ol.style.Fill({ + color: '#ffcc33', + opacity: 1 + }), + size: 14 + }), + new ol.style.Fill({ + color: 'white', + opacity: 0.2 + }), + new ol.style.Stroke({ + color: '#ffcc33', + width: 2 + }) + ] + }) +}); + +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 0c6c3b03fc..e39f3b5156 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -341,6 +341,16 @@ * @todo stability experimental */ +/** + * @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.geom.GeometryType} type Drawing type ('point', 'linestring', + * 'polygon', 'multipoint', 'multilinestring', or 'multipolygon'). + * @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..274fa87977 --- /dev/null +++ b/src/ol/interaction/drawinteraction.js @@ -0,0 +1,405 @@ +goog.provide('ol.interaction.Draw'); + +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.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'); +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_ = goog.isDef(options.snapTolerance) ? + options.snapTolerance : 12; + + /** + * Geometry type. + * @type {ol.geom.GeometryType} + * @private + */ + this.type_ = 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; + + /** + * 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); + + +/** + * @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.abortDrawing_(); + this.sketchLayer_ = 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); + } 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.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; +}; + + +/** + * Start the drawing. + * @param {ol.MapBrowserEvent} event Event. + * @private + */ +ol.interaction.Draw.prototype.startDrawing_ = function(event) { + var start = event.getCoordinate(); + this.finishCoordinate_ = start; + 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 { + var sketchPoint = new ol.Feature({ + geom: new ol.geom.Point(start.slice()) + }); + sketchPoint.setRenderIntent(ol.layer.VectorLayerRenderIntent.TEMPORARY); + 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)); + sketchFeature.setGeometry(geometry); + this.sketchFeature_ = sketchFeature; + + this.sketchLayer_.addFeatures(features); +}; + + +/** + * 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(); + } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) { + geometry = geometry.getRings()[0]; + coordinates = geometry.getCoordinates(); + } + if (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); + } +}; + + +/** + * 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) { + this.finishCoordinate_ = coordinate.slice(); + 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); + } +}; + + +/** + * 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)); + sketchFeature.setRenderIntent(ol.layer.VectorLayerRenderIntent.DEFAULT); + var geometry = sketchFeature.getGeometry(); + var coordinates = geometry.getCoordinates(); + if (this.mode_ === ol.interaction.DrawMode.LINESTRING) { + // 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([coordinates])); + } else if (this.type_ === ol.geom.GeometryType.MULTILINESTRING) { + sketchFeature.setGeometry(new ol.geom.MultiLineString([coordinates])); + } else if (this.type_ === ol.geom.GeometryType.MULTIPOLYGON) { + sketchFeature.setGeometry(new ol.geom.MultiPolygon([coordinates])); + } + this.layer_.addFeatures([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)) { + 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; +}; + + +/** + * 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 = { + POINT: 'point', + LINESTRING: 'linestring', + POLYGON: 'polygon' +}; 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' }; 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 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');