diff --git a/examples/draw-shapes.html b/examples/draw-shapes.html index 9bdf4edc0b..7ac16e5795 100644 --- a/examples/draw-shapes.html +++ b/examples/draw-shapes.html @@ -9,18 +9,19 @@ docs: > achieved by using `type: 'Circle'` type with a `geometryFunction` that creates a 4-sided regular polygon instead of a circle. Box drawing uses `type: 'Circle'` with a `geometryFunction` that creates a box-shaped polygon instead of a - circle. Star drawing uses a custom geometry function that coverts a circle - into a start using the center and radius provided by the draw interaction. + circle. Star drawing uses a custom geometry function that converts a circle + into a star using the center and radius provided by the draw interaction. tags: "draw, edit, freehand, vector" ---
- - +
diff --git a/examples/draw-shapes.js b/examples/draw-shapes.js index f02fba2ac8..5d7d013265 100644 --- a/examples/draw-shapes.js +++ b/examples/draw-shapes.js @@ -43,28 +43,30 @@ function addInteraction() { } else if (value === 'Star') { value = 'Circle'; geometryFunction = function (coordinates, geometry) { - const center = coordinates[0]; - const last = coordinates[1]; - const dx = center[0] - last[0]; - const dy = center[1] - last[1]; - const radius = Math.sqrt(dx * dx + dy * dy); - const rotation = Math.atan2(dy, dx); - const newCoordinates = []; - const numPoints = 12; - for (let i = 0; i < numPoints; ++i) { - const angle = rotation + (i * 2 * Math.PI) / numPoints; - const fraction = i % 2 === 0 ? 1 : 0.5; - const offsetX = radius * fraction * Math.cos(angle); - const offsetY = radius * fraction * Math.sin(angle); - newCoordinates.push([center[0] + offsetX, center[1] + offsetY]); + if (coordinates.length) { + const center = coordinates[0]; + const last = coordinates[coordinates.length - 1]; + const dx = center[0] - last[0]; + const dy = center[1] - last[1]; + const radius = Math.sqrt(dx * dx + dy * dy); + const rotation = Math.atan2(dy, dx); + const newCoordinates = []; + const numPoints = 12; + for (let i = 0; i < numPoints; ++i) { + const angle = rotation + (i * 2 * Math.PI) / numPoints; + const fraction = i % 2 === 0 ? 1 : 0.5; + const offsetX = radius * fraction * Math.cos(angle); + const offsetY = radius * fraction * Math.sin(angle); + newCoordinates.push([center[0] + offsetX, center[1] + offsetY]); + } + newCoordinates.push(newCoordinates[0].slice()); + if (!geometry) { + geometry = new Polygon([newCoordinates]); + } else { + geometry.setCoordinates([newCoordinates]); + } + return geometry; } - newCoordinates.push(newCoordinates[0].slice()); - if (!geometry) { - geometry = new Polygon([newCoordinates]); - } else { - geometry.setCoordinates([newCoordinates]); - } - return geometry; }; } draw = new Draw({ @@ -84,4 +86,8 @@ typeSelect.onchange = function () { addInteraction(); }; +document.getElementById('undo').addEventListener('click', function () { + draw.removeLastPoint(); +}); + addInteraction(); diff --git a/src/ol/interaction/Draw.js b/src/ol/interaction/Draw.js index 6d679711fb..37e196a681 100644 --- a/src/ol/interaction/Draw.js +++ b/src/ol/interaction/Draw.js @@ -123,7 +123,6 @@ const Mode = { POINT: 'Point', LINE_STRING: 'LineString', POLYGON: 'Polygon', - CIRCLE: 'Circle', }; /** @@ -290,7 +289,12 @@ class Draw extends PointerInteraction { * @type {number} * @private */ - this.maxPoints_ = options.maxPoints ? options.maxPoints : Infinity; + this.maxPoints_ = + this.type_ === GeometryType.CIRCLE + ? 2 + : options.maxPoints + ? options.maxPoints + : Infinity; /** * A function to decide if a potential finish coordinate is permissible @@ -311,20 +315,25 @@ class Draw extends PointerInteraction { * @return {import("../geom/SimpleGeometry.js").default} A geometry. */ geometryFunction = function (coordinates, geometry, projection) { - const circle = geometry - ? /** @type {Circle} */ (geometry) - : new Circle([NaN, NaN]); - const center = fromUserCoordinate(coordinates[0], projection); - const squaredLength = squaredCoordinateDistance( - center, - fromUserCoordinate(coordinates[1], projection) - ); - circle.setCenterAndRadius(center, Math.sqrt(squaredLength)); - const userProjection = getUserProjection(); - if (userProjection) { - circle.transform(projection, userProjection); + if (coordinates.length) { + const circle = geometry + ? /** @type {Circle} */ (geometry) + : new Circle([NaN, NaN]); + const center = fromUserCoordinate(coordinates[0], projection); + const squaredLength = squaredCoordinateDistance( + center, + fromUserCoordinate( + coordinates[coordinates.length - 1], + projection + ) + ); + circle.setCenterAndRadius(center, Math.sqrt(squaredLength)); + const userProjection = getUserProjection(); + if (userProjection) { + circle.transform(projection, userProjection); + } + return circle; } - return circle; }; } else { let Constructor; @@ -551,7 +560,7 @@ class Draw extends PointerInteraction { event.preventDefault(); } } else if ( - event.originalEvent.pointerType == 'mouse' || + event.originalEvent.pointerType === 'mouse' || (event.type === MapBrowserEventType.POINTERDRAG && this.downTimeout_ === undefined) ) { @@ -618,15 +627,13 @@ class Draw extends PointerInteraction { this.handlePointerMove_(event); - const circleMode = this.mode_ === Mode.CIRCLE; - if (this.shouldHandle_) { if (!this.finishCoordinate_) { this.startDrawing_(event); if (this.mode_ === Mode.POINT) { this.finishDrawing(); } - } else if (this.freehand_ || circleMode) { + } else if (this.freehand_) { this.finishDrawing(); } else if (this.atFinish_(event)) { if (this.finishCondition_(event)) { @@ -735,6 +742,31 @@ class Draw extends PointerInteraction { } } + /** + * @param {import("../geom/Polygon.js").default} geometry Polygon geometry. + * @private + */ + createOrUpdateCustomSketchLine_(geometry) { + if (!this.sketchLine_) { + this.sketchLine_ = new Feature(); + } + const ring = geometry.getLinearRing(0); + let sketchLineGeom = this.sketchLine_.getGeometry(); + if (!sketchLineGeom) { + sketchLineGeom = new LineString( + ring.getFlatCoordinates(), + ring.getLayout() + ); + this.sketchLine_.setGeometry(sketchLineGeom); + } else { + sketchLineGeom.setFlatCoordinates( + ring.getLayout(), + ring.getFlatCoordinates() + ); + sketchLineGeom.changed(); + } + } + /** * Start the drawing. * @param {import("../MapBrowserEvent.js").default} event Event. @@ -805,32 +837,13 @@ class Draw extends PointerInteraction { const sketchPointGeom = this.sketchPoint_.getGeometry(); sketchPointGeom.setCoordinates(coordinate); } - /** @type {LineString} */ - let sketchLineGeom; if ( - geometry.getType() == GeometryType.POLYGON && + geometry.getType() === GeometryType.POLYGON && this.mode_ !== Mode.POLYGON ) { - if (!this.sketchLine_) { - this.sketchLine_ = new Feature(); - } - const ring = geometry.getLinearRing(0); - sketchLineGeom = this.sketchLine_.getGeometry(); - if (!sketchLineGeom) { - sketchLineGeom = new LineString( - ring.getFlatCoordinates(), - ring.getLayout() - ); - this.sketchLine_.setGeometry(sketchLineGeom); - } else { - sketchLineGeom.setFlatCoordinates( - ring.getLayout(), - ring.getFlatCoordinates() - ); - sketchLineGeom.changed(); - } + this.createOrUpdateCustomSketchLine_(/** @type {Polygon} */ (geometry)); } else if (this.sketchLineCoords_) { - sketchLineGeom = this.sketchLine_.getGeometry(); + const sketchLineGeom = this.sketchLine_.getGeometry(); sketchLineGeom.setCoordinates(this.sketchLineCoords_); } this.updateSketchFeatures_(); @@ -890,8 +903,6 @@ class Draw extends PointerInteraction { const geometry = this.sketchFeature_.getGeometry(); const projection = this.getMap().getView().getProjection(); let coordinates; - /** @type {LineString} */ - let sketchLineGeom; if (this.mode_ === Mode.LINE_STRING) { coordinates = /** @type {LineCoordType} */ (this.sketchCoords_); coordinates.splice(-2, 1); @@ -905,11 +916,14 @@ class Draw extends PointerInteraction { } } this.geometryFunction_(coordinates, geometry, projection); + if (geometry.getType() === GeometryType.POLYGON && this.sketchLine_) { + this.createOrUpdateCustomSketchLine_(/** @type {Polygon} */ (geometry)); + } } else if (this.mode_ === Mode.POLYGON) { coordinates = /** @type {PolyCoordType} */ (this.sketchCoords_)[0]; coordinates.splice(-2, 1); - sketchLineGeom = this.sketchLine_.getGeometry(); - if (this.pointerType_ !== 'mouse') { + const sketchLineGeom = this.sketchLine_.getGeometry(); + if (coordinates.length >= 2 && this.pointerType_ !== 'mouse') { const finishCoordinate = coordinates[coordinates.length - 2].slice(); coordinates.pop(); coordinates.push(finishCoordinate); @@ -940,7 +954,7 @@ class Draw extends PointerInteraction { let coordinates = this.sketchCoords_; const geometry = sketchFeature.getGeometry(); const projection = this.getMap().getView().getProjection(); - if (this.mode_ === Mode.LINE_STRING) { + if (this.mode_ === Mode.LINE_STRING && this.type_ !== GeometryType.CIRCLE) { // remove the redundant last point coordinates.pop(); this.geometryFunction_(coordinates, geometry, projection); @@ -1109,43 +1123,47 @@ function getDefaultStyleFunction() { /** * Create a `geometryFunction` for `type: 'Circle'` that will create a regular - * polygon with a user specified number of sides and start angle instead of an + * polygon with a user specified number of sides and start angle instead of a * `import("../geom/Circle.js").Circle` geometry. - * @param {number=} opt_sides Number of sides of the regular polygon. Default is - * 32. - * @param {number=} opt_angle Angle of the first point in radians. 0 means East. + * @param {number=} opt_sides Number of sides of the regular polygon. + * Default is 32. + * @param {number=} opt_angle Angle of the first point in counter-clockwise + * radians. 0 means East. * Default is the angle defined by the heading from the center of the * regular polygon to the current pointer position. - * @return {GeometryFunction} Function that draws a - * polygon. + * @return {GeometryFunction} Function that draws a polygon. * @api */ export function createRegularPolygon(opt_sides, opt_angle) { return function (coordinates, opt_geometry, projection) { - const center = fromUserCoordinate( - /** @type {LineCoordType} */ (coordinates)[0], - projection - ); - const end = fromUserCoordinate( - /** @type {LineCoordType} */ (coordinates)[1], - projection - ); - const radius = Math.sqrt(squaredCoordinateDistance(center, end)); - const geometry = opt_geometry - ? /** @type {Polygon} */ (opt_geometry) - : fromCircle(new Circle(center), opt_sides); - let angle = opt_angle; - if (!opt_angle) { - const x = end[0] - center[0]; - const y = end[1] - center[1]; - angle = Math.atan(y / x) - (x < 0 ? Math.PI : 0); + if (coordinates.length) { + const center = fromUserCoordinate( + /** @type {LineCoordType} */ (coordinates)[0], + projection + ); + const end = fromUserCoordinate( + /** @type {LineCoordType} */ (coordinates)[coordinates.length - 1], + projection + ); + const radius = Math.sqrt(squaredCoordinateDistance(center, end)); + const geometry = opt_geometry + ? /** @type {Polygon} */ (opt_geometry) + : fromCircle(new Circle(center), opt_sides); + + let angle = opt_angle; + if (!opt_angle && opt_angle !== 0) { + const x = end[0] - center[0]; + const y = end[1] - center[1]; + angle = Math.atan2(y, x); + } + makeRegular(geometry, center, radius, angle); + + const userProjection = getUserProjection(); + if (userProjection) { + geometry.transform(projection, userProjection); + } + return geometry; } - makeRegular(geometry, center, radius, angle); - const userProjection = getUserProjection(); - if (userProjection) { - geometry.transform(projection, userProjection); - } - return geometry; }; } @@ -1158,31 +1176,36 @@ export function createRegularPolygon(opt_sides, opt_angle) { */ export function createBox() { return function (coordinates, opt_geometry, projection) { - const extent = boundingExtent( - /** @type {LineCoordType} */ (coordinates).map(function (coordinate) { - return fromUserCoordinate(coordinate, projection); - }) - ); - const boxCoordinates = [ - [ - getBottomLeft(extent), - getBottomRight(extent), - getTopRight(extent), - getTopLeft(extent), - getBottomLeft(extent), - ], - ]; - let geometry = opt_geometry; - if (geometry) { - geometry.setCoordinates(boxCoordinates); - } else { - geometry = new Polygon(boxCoordinates); + if (coordinates.length) { + const extent = boundingExtent( + /** @type {LineCoordType} */ ([ + coordinates[0], + coordinates[coordinates.length - 1], + ]).map(function (coordinate) { + return fromUserCoordinate(coordinate, projection); + }) + ); + const boxCoordinates = [ + [ + getBottomLeft(extent), + getBottomRight(extent), + getTopRight(extent), + getTopLeft(extent), + getBottomLeft(extent), + ], + ]; + let geometry = opt_geometry; + if (geometry) { + geometry.setCoordinates(boxCoordinates); + } else { + geometry = new Polygon(boxCoordinates); + } + const userProjection = getUserProjection(); + if (userProjection) { + geometry.transform(projection, userProjection); + } + return geometry; } - const userProjection = getUserProjection(); - if (userProjection) { - geometry.transform(projection, userProjection); - } - return geometry; }; } @@ -1198,7 +1221,8 @@ function getMode(type) { mode = Mode.POINT; } else if ( type === GeometryType.LINE_STRING || - type === GeometryType.MULTI_LINE_STRING + type === GeometryType.MULTI_LINE_STRING || + type === GeometryType.CIRCLE ) { mode = Mode.LINE_STRING; } else if ( @@ -1206,8 +1230,6 @@ function getMode(type) { type === GeometryType.MULTI_POLYGON ) { mode = Mode.POLYGON; - } else if (type === GeometryType.CIRCLE) { - mode = Mode.CIRCLE; } return /** @type {!Mode} */ (mode); } diff --git a/test/spec/ol/interaction/draw.test.js b/test/spec/ol/interaction/draw.test.js index aa114aef9a..6b77cdfb8f 100644 --- a/test/spec/ol/interaction/draw.test.js +++ b/test/spec/ol/interaction/draw.test.js @@ -1346,6 +1346,33 @@ describe('ol.interaction.Draw', function () { describe('createRegularPolygon', function () { it('creates a regular polygon in Circle mode', function () { + const draw = new Draw({ + source: source, + type: 'Circle', + geometryFunction: createRegularPolygon(4), + }); + map.addInteraction(draw); + + // first point + simulateEvent('pointermove', 0, 0); + simulateEvent('pointerdown', 0, 0); + simulateEvent('pointerup', 0, 0); + + // finish on second point + simulateEvent('pointermove', 20, 20); + simulateEvent('pointerdown', 20, 20); + simulateEvent('pointerup', 20, 20); + + const features = source.getFeatures(); + const geometry = features[0].getGeometry(); + expect(geometry).to.be.a(Polygon); + const coordinates = geometry.getCoordinates(); + expect(coordinates[0].length).to.eql(5); + expect(coordinates[0][0][0]).to.roughlyEqual(20, 1e-9); + expect(coordinates[0][0][1]).to.roughlyEqual(-20, 1e-9); + }); + + it('creates a regular polygon at specified angle', function () { const draw = new Draw({ source: source, type: 'Circle', @@ -1372,6 +1399,33 @@ describe('ol.interaction.Draw', function () { expect(coordinates[0][0][1]).to.roughlyEqual(20, 1e-9); }); + it('creates a regular polygon at specified 0 angle', function () { + const draw = new Draw({ + source: source, + type: 'Circle', + geometryFunction: createRegularPolygon(4, 0), + }); + map.addInteraction(draw); + + // first point + simulateEvent('pointermove', 0, 0); + simulateEvent('pointerdown', 0, 0); + simulateEvent('pointerup', 0, 0); + + // finish on second point + simulateEvent('pointermove', 20, 20); + simulateEvent('pointerdown', 20, 20); + simulateEvent('pointerup', 20, 20); + + const features = source.getFeatures(); + const geometry = features[0].getGeometry(); + expect(geometry).to.be.a(Polygon); + const coordinates = geometry.getCoordinates(); + expect(coordinates[0].length).to.eql(5); + expect(coordinates[0][0][0]).to.roughlyEqual(28.2842712474619, 1e-9); + expect(coordinates[0][0][1]).to.roughlyEqual(0, 1e-9); + }); + it('creates a regular polygon in Circle mode in a user projection', function () { const userProjection = 'EPSG:3857'; setUserProjection(userProjection);