From 2e544cb6778f035a4f83a7db699543b2a20ecdab Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Mon, 25 Nov 2019 11:58:48 +0000 Subject: [PATCH] Circles and custom geometry in user coordinates Pass the view projection to the geometry function so circles and other custom geometry can be given the expected shape/size in the view while being defined in user coordinates. Add tests to draw circles in a user projection and along both axes Draw regular polygon in a user projection Draw box in a user projection --- src/ol/interaction/Draw.js | 68 ++++++++---- test/spec/ol/interaction/draw.test.js | 143 +++++++++++++++++++++++++- 2 files changed, 188 insertions(+), 23 deletions(-) diff --git a/src/ol/interaction/Draw.js b/src/ol/interaction/Draw.js index c4e2824b44..d6332fe8d8 100644 --- a/src/ol/interaction/Draw.js +++ b/src/ol/interaction/Draw.js @@ -24,6 +24,7 @@ import InteractionProperty from './Property.js'; import VectorLayer from '../layer/Vector.js'; import VectorSource from '../source/Vector.js'; import {createEditingStyle} from '../style/Style.js'; +import {fromUserCoordinate, getUserProjection} from '../proj.js'; /** @@ -104,11 +105,12 @@ import {createEditingStyle} from '../style/Style.js'; /** - * Function that takes an array of coordinates and an optional existing geometry as - * arguments, and returns a geometry. The optional existing geometry is the - * geometry that is returned when the function is called without a second - * argument. - * @typedef {function(!SketchCoordType, import("../geom/SimpleGeometry.js").default=): + * Function that takes an array of coordinates and an optional existing geometry + * and a projection as arguments, and returns a geometry. The optional existing + * geometry is the geometry that is returned when the function is called without + * a second argument. + * @typedef {function(!SketchCoordType, import("../geom/SimpleGeometry.js").default=, + * import("../proj/Projection.js").default): * import("../geom/SimpleGeometry.js").default} GeometryFunction */ @@ -296,14 +298,20 @@ class Draw extends PointerInteraction { /** * @param {!LineCoordType} coordinates The coordinates. * @param {import("../geom/SimpleGeometry.js").default=} opt_geometry Optional geometry. + * @param {import("../proj/Projection.js").default} projection The view projection. * @return {import("../geom/SimpleGeometry.js").default} A geometry. */ - geometryFunction = function(coordinates, opt_geometry) { + geometryFunction = function(coordinates, opt_geometry, projection) { const circle = opt_geometry ? /** @type {Circle} */ (opt_geometry) : new Circle([NaN, NaN]); + const center = fromUserCoordinate(coordinates[0], projection); const squaredLength = squaredCoordinateDistance( - coordinates[0], coordinates[1]); - circle.setCenterAndRadius(coordinates[0], Math.sqrt(squaredLength)); + center, fromUserCoordinate(coordinates[1], projection)); + circle.setCenterAndRadius(center, Math.sqrt(squaredLength)); + const userProjection = getUserProjection(); + if (userProjection) { + circle.transform(projection, userProjection); + } return circle; }; } else { @@ -319,9 +327,10 @@ class Draw extends PointerInteraction { /** * @param {!LineCoordType} coordinates The coordinates. * @param {import("../geom/SimpleGeometry.js").default=} opt_geometry Optional geometry. + * @param {import("../proj/Projection.js").default} projection The view projection. * @return {import("../geom/SimpleGeometry.js").default} A geometry. */ - geometryFunction = function(coordinates, opt_geometry) { + geometryFunction = function(coordinates, opt_geometry, projection) { let geometry = opt_geometry; if (geometry) { if (mode === Mode.POLYGON) { @@ -675,6 +684,7 @@ class Draw extends PointerInteraction { */ startDrawing_(event) { const start = event.coordinate; + const projection = event.map.getView().getProjection(); this.finishCoordinate_ = start; if (this.mode_ === Mode.POINT) { this.sketchCoords_ = start.slice(); @@ -688,7 +698,7 @@ class Draw extends PointerInteraction { this.sketchLine_ = new Feature( new LineString(this.sketchLineCoords_)); } - const geometry = this.geometryFunction_(this.sketchCoords_); + const geometry = this.geometryFunction_(this.sketchCoords_, undefined, projection); this.sketchFeature_ = new Feature(); if (this.geometryName_) { this.sketchFeature_.setGeometryName(this.geometryName_); @@ -706,6 +716,7 @@ class Draw extends PointerInteraction { modifyDrawing_(event) { let coordinate = event.coordinate; const geometry = this.sketchFeature_.getGeometry(); + const projection = event.map.getView().getProjection(); let coordinates, last; if (this.mode_ === Mode.POINT) { last = this.sketchCoords_; @@ -722,7 +733,7 @@ class Draw extends PointerInteraction { } last[0] = coordinate[0]; last[1] = coordinate[1]; - this.geometryFunction_(/** @type {!LineCoordType} */ (this.sketchCoords_), geometry); + this.geometryFunction_(/** @type {!LineCoordType} */ (this.sketchCoords_), geometry, projection); if (this.sketchPoint_) { const sketchPointGeom = this.sketchPoint_.getGeometry(); sketchPointGeom.setCoordinates(coordinate); @@ -759,6 +770,7 @@ class Draw extends PointerInteraction { addToDrawing_(event) { const coordinate = event.coordinate; const geometry = this.sketchFeature_.getGeometry(); + const projection = event.map.getView().getProjection(); let done; let coordinates; if (this.mode_ === Mode.LINE_STRING) { @@ -772,7 +784,7 @@ class Draw extends PointerInteraction { } } coordinates.push(coordinate.slice()); - this.geometryFunction_(coordinates, geometry); + this.geometryFunction_(coordinates, geometry, projection); } else if (this.mode_ === Mode.POLYGON) { coordinates = /** @type {PolyCoordType} */ (this.sketchCoords_)[0]; if (coordinates.length >= this.maxPoints_) { @@ -786,7 +798,7 @@ class Draw extends PointerInteraction { if (done) { this.finishCoordinate_ = coordinates[0]; } - this.geometryFunction_(this.sketchCoords_, geometry); + this.geometryFunction_(this.sketchCoords_, geometry, projection); } this.updateSketchFeatures_(); if (done) { @@ -803,13 +815,14 @@ class Draw extends PointerInteraction { return; } 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); - this.geometryFunction_(coordinates, geometry); + this.geometryFunction_(coordinates, geometry, projection); if (coordinates.length >= 2) { this.finishCoordinate_ = coordinates[coordinates.length - 2].slice(); } @@ -818,7 +831,7 @@ class Draw extends PointerInteraction { coordinates.splice(-2, 1); sketchLineGeom = this.sketchLine_.getGeometry(); sketchLineGeom.setCoordinates(coordinates); - this.geometryFunction_(this.sketchCoords_, geometry); + this.geometryFunction_(this.sketchCoords_, geometry, projection); } if (coordinates.length === 0) { @@ -841,14 +854,15 @@ class Draw extends PointerInteraction { } let coordinates = this.sketchCoords_; const geometry = sketchFeature.getGeometry(); + const projection = this.getMap().getView().getProjection(); if (this.mode_ === Mode.LINE_STRING) { // remove the redundant last point coordinates.pop(); - this.geometryFunction_(coordinates, geometry); + this.geometryFunction_(coordinates, geometry, projection); } else if (this.mode_ === Mode.POLYGON) { // remove the redundant last point in ring /** @type {PolyCoordType} */ (coordinates)[0].pop(); - this.geometryFunction_(coordinates, geometry); + this.geometryFunction_(coordinates, geometry, projection); coordinates = geometry.getCoordinates(); } @@ -966,9 +980,9 @@ function getDefaultStyleFunction() { * @api */ export function createRegularPolygon(opt_sides, opt_angle) { - return function(coordinates, opt_geometry) { - const center = /** @type {LineCoordType} */ (coordinates)[0]; - const end = /** @type {LineCoordType} */ (coordinates)[1]; + 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) : @@ -980,6 +994,10 @@ export function createRegularPolygon(opt_sides, opt_angle) { angle = Math.atan(y / x) - (x < 0 ? Math.PI : 0); } makeRegular(geometry, center, radius, angle); + const userProjection = getUserProjection(); + if (userProjection) { + geometry.transform(projection, userProjection); + } return geometry; }; } @@ -994,8 +1012,10 @@ export function createRegularPolygon(opt_sides, opt_angle) { */ export function createBox() { return ( - function(coordinates, opt_geometry) { - const extent = boundingExtent(/** @type {LineCoordType} */ (coordinates)); + function(coordinates, opt_geometry, projection) { + const extent = boundingExtent(/** @type {LineCoordType} */ (coordinates).map(function(coordinate) { + return fromUserCoordinate(coordinate, projection); + })); const boxCoordinates = [[ getBottomLeft(extent), getBottomRight(extent), @@ -1009,6 +1029,10 @@ export function createBox() { } else { geometry = new Polygon(boxCoordinates); } + const userProjection = getUserProjection(); + if (userProjection) { + geometry.transform(projection, userProjection); + } return geometry; } ); diff --git a/test/spec/ol/interaction/draw.test.js b/test/spec/ol/interaction/draw.test.js index 5961ddb700..7a12b66887 100644 --- a/test/spec/ol/interaction/draw.test.js +++ b/test/spec/ol/interaction/draw.test.js @@ -17,6 +17,9 @@ import Interaction from '../../../../src/ol/interaction/Interaction.js'; import VectorLayer from '../../../../src/ol/layer/Vector.js'; import Event from '../../../../src/ol/events/Event.js'; import VectorSource from '../../../../src/ol/source/Vector.js'; +import {clearUserProjection, setUserProjection, transform} from '../../../../src/ol/proj.js'; +import {register} from '../../../../src/ol/proj/proj4.js'; +import proj4 from 'proj4'; describe('ol.interaction.Draw', function() { @@ -53,6 +56,7 @@ describe('ol.interaction.Draw', function() { afterEach(function() { map.dispose(); document.body.removeChild(target); + clearUserProjection(); }); /** @@ -909,7 +913,7 @@ describe('ol.interaction.Draw', function() { map.addInteraction(draw); }); - it('draws circle with clicks, finishing on second point', function() { + it('draws circle with clicks, finishing on second point along x axis', function() { // first point simulateEvent('pointermove', 10, 20); simulateEvent('pointerdown', 10, 20); @@ -928,6 +932,73 @@ describe('ol.interaction.Draw', function() { expect(geometry.getRadius()).to.eql(20); }); + it('draws circle with clicks, finishing on second point along y axis', function() { + // first point + simulateEvent('pointermove', 10, 20); + simulateEvent('pointerdown', 10, 20); + simulateEvent('pointerup', 10, 20); + + // finish on second point + simulateEvent('pointermove', 10, 40); + simulateEvent('pointerdown', 10, 40); + simulateEvent('pointerup', 10, 40); + + const features = source.getFeatures(); + expect(features).to.have.length(1); + const geometry = features[0].getGeometry(); + expect(geometry).to.be.a(Circle); + expect(geometry.getCenter()).to.eql([10, -20]); + expect(geometry.getRadius()).to.eql(20); + }); + + it('draws circle with clicks in a user projection, finishing on second point along x axis', function() { + const userProjection = 'EPSG:3857'; + setUserProjection(userProjection); + + // first point + simulateEvent('pointermove', 10, 20); + simulateEvent('pointerdown', 10, 20); + simulateEvent('pointerup', 10, 20); + + // finish on second point + simulateEvent('pointermove', 30, 20); + simulateEvent('pointerdown', 30, 20); + simulateEvent('pointerup', 30, 20); + + const features = source.getFeatures(); + expect(features).to.have.length(1); + const geometry = features[0].getGeometry(); + expect(geometry).to.be.a(Circle); + const viewProjection = map.getView().getProjection(); + expect(geometry.getCenter()).to.eql(transform([10, -20], viewProjection, userProjection)); + const radius = geometry.clone().transform(userProjection, viewProjection).getRadius(); + expect(radius).to.roughlyEqual(20, 1e-9); + }); + + it('draws circle with clicks in a user projection, finishing on second point along y axis', function() { + const userProjection = 'EPSG:3857'; + setUserProjection(userProjection); + + // first point + simulateEvent('pointermove', 10, 20); + simulateEvent('pointerdown', 10, 20); + simulateEvent('pointerup', 10, 20); + + // finish on second point + simulateEvent('pointermove', 10, 40); + simulateEvent('pointerdown', 10, 40); + simulateEvent('pointerup', 10, 40); + + const features = source.getFeatures(); + expect(features).to.have.length(1); + const geometry = features[0].getGeometry(); + expect(geometry).to.be.a(Circle); + const viewProjection = map.getView().getProjection(); + expect(geometry.getCenter()).to.eql(transform([10, -20], viewProjection, userProjection)); + const radius = geometry.clone().transform(userProjection, viewProjection).getRadius(); + expect(radius).to.roughlyEqual(20, 1e-9); + }); + it('supports freehand drawing for circles', function() { draw.freehand_ = true; draw.freehandCondition_ = always; @@ -1153,6 +1224,38 @@ describe('ol.interaction.Draw', function() { expect(coordinates[0][0][1]).to.roughlyEqual(20, 1e-9); }); + it('creates a regular polygon in Circle mode in a user projection', function() { + const userProjection = 'EPSG:3857'; + setUserProjection(userProjection); + + const draw = new Draw({ + source: source, + type: 'Circle', + geometryFunction: createRegularPolygon(4, Math.PI / 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); + const viewProjection = map.getView().getProjection(); + const coordinate = transform([20, 20], viewProjection, userProjection); + expect(coordinates[0][0][0]).to.roughlyEqual(coordinate[0], 1e-9); + expect(coordinates[0][0][1]).to.roughlyEqual(coordinate[1], 1e-9); + }); + it('sketch start point always matches the mouse point', function() { const draw = new Draw({ source: source, @@ -1227,6 +1330,44 @@ describe('ol.interaction.Draw', function() { expect(geometry.getArea()).to.equal(400); expect(geometry.getExtent()).to.eql([0, -20, 20, 0]); }); + + it('creates a box-shaped polygon in Circle mode in a user projection', function() { + proj4.defs('ESRI:54009', '+proj=moll +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'); + register(proj4); + const userProjection = 'ESRI:54009'; + setUserProjection(userProjection); + + const draw = new Draw({ + source: source, + type: 'Circle', + geometryFunction: createBox() + }); + 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]).to.have.length(5); + const viewProjection = map.getView().getProjection(); + const area = geometry.clone().transform(userProjection, viewProjection).getArea(); + expect(area).to.roughlyEqual(400, 1e-9); + const extent = geometry.clone().transform(userProjection, viewProjection).getExtent(); + expect(extent[0]).to.roughlyEqual(0, 1e-9); + expect(extent[1]).to.roughlyEqual(-20, 1e-9); + expect(extent[2]).to.roughlyEqual(20, 1e-9); + expect(extent[3]).to.roughlyEqual(0, 1e-9); + }); }); describe('extend an existing feature', function() {