Adding a RegularPolygon handler for drawing squares, triangles, circles, etc. Demo in the regular-polygon.html example. Also adding a createRegularPolygon class method to the Polygon geometry class. Thanks to crschmidt for all the tests and help getting this in (closes #828).

git-svn-id: http://svn.openlayers.org/trunk/openlayers@4205 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf
This commit is contained in:
Tim Schaub
2007-09-10 20:24:27 +00:00
parent 623e6e7112
commit 35dae2bdb0
7 changed files with 724 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>OpenLayers Regular Polygon Example</title>
<style type="text/css">
html, body {
margin: 0;
padding: 1em;
font: 0.9em Verdana, Arial, sans serif;
}
input, select, textarea {
font: 1em Verdana, Arial, sans serif;
}
#map {
width: 512px;
height: 350px;
border: 1px solid gray;
}
p {
width: 512px;
}
#config {
margin-top: 1em;
width: 512px;
position: relative;
height: 8em;
}
#controls {
padding-left: 2em;
margin-left: 0;
width: 12em;
}
#controls li {
padding-top: 0.5em;
list-style: none;
}
#options {
font-size: 1em;
top: 0;
margin-left: 15em;
position: absolute;
}
</style>
<script src="../lib/OpenLayers.js"></script>
<script type="text/javascript">
<!--
var map, polygonControl;
OpenLayers.Util.onImageLoadErrorColor = "transparent";
function init(){
map = new OpenLayers.Map('map');
var wmsLayer = new OpenLayers.Layer.WMS( "OpenLayers WMS",
"http://labs.metacarta.com/wms/vmap0?", {layers: 'basic'});
var polygonLayer = new OpenLayers.Layer.Vector("Polygon Layer");
map.addLayers([wmsLayer, polygonLayer]);
map.addControl(new OpenLayers.Control.LayerSwitcher());
map.addControl(new OpenLayers.Control.MousePosition());
polyOptions = {sides: 4};
polygonControl = new OpenLayers.Control.DrawFeature(polygonLayer,
OpenLayers.Handler.RegularPolygon,
{handlerOptions: polyOptions});
map.addControl(polygonControl);
map.setCenter(new OpenLayers.LonLat(0, 0), 3);
document.getElementById('noneToggle').checked = true;
}
function setOptions(options) {
polygonControl.handler.setOptions(options);
}
function setSize(fraction) {
var radius = fraction * map.getExtent().getHeight();
polygonControl.handler.setOptions({radius: radius,
angle: 0});
}
// -->
</script>
</head>
<body onload="init()">
<h2>OpenLayers Regular Polygon Example</h2>
<div id="map"></div>
<div id="config">
<ul id="controls"><b>Map Controls</b>
<li>
<input type="radio" name="type"
value="none" id="noneToggle"
onclick="polygonControl.deactivate()"
checked="checked" />
<label for="noneToggle">navigate</label>
</li>
<li>
<input type="radio" name="type"
value="polygon" id="polygonToggle"
onclick="polygonControl.activate()" />
<label for="polygonToggle">draw polygon</label>
</li>
</ul>
<table id="options">
<tbody>
<tr>
<th>Draw Option</th>
<th>Value</th>
</tr>
<tr>
<td>
shape
</td>
<td>
<select name="sides"
onchange="setOptions({sides: parseInt(this.value)})">
<option value="3">triangle</option>
<option value="4" selected="selected">square</option>
<option value="5">pentagon</option>
<option value="6">hexagon</option>
<option value="40">circle</option>
</select>
</td>
</tr>
<tr>
<td>
snap angle
</td>
<td>
<select name="angle"
onchange="setOptions({snapAngle: parseFloat(this.value)})">
<option value="" selected="selected">no snap</option>
<option value="15">15&deg;</option>
<option value="45">45&deg;</option>
<option value="90">90&deg;</option>
</select>
</td>
</tr>
<tr>
<td>
size
</td>
<td>
<select name="size"
onchange="setSize(parseFloat(this.value))">
<option value="" selected="selected">variable</option>
<option value="0.1">small</option>
<option value="0.2">medium</option>
<option value="0.4">large</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<p>
Regular polygons can be drawn by pointing a DrawFeature control to the
RegularPolygon handler class. The options above demonstrate how the
handler can be configured. Note if you are in angle snapping mode (if
the snap angle is non-null) and you hold down the <b>Shift</b> key, you
will toggle to non-snapping mode.
</p>
</body>
</html>

View File

@@ -119,6 +119,7 @@
"OpenLayers/Handler/Polygon.js",
"OpenLayers/Handler/Feature.js",
"OpenLayers/Handler/Drag.js",
"OpenLayers/Handler/RegularPolygon.js",
"OpenLayers/Handler/Box.js",
"OpenLayers/Handler/MouseWheel.js",
"OpenLayers/Handler/Keyboard.js",

View File

@@ -59,3 +59,31 @@ OpenLayers.Geometry.Polygon = OpenLayers.Class(
CLASS_NAME: "OpenLayers.Geometry.Polygon"
});
/**
* APIMethod: createRegularPolygon
* Create a regular polygon around a radius. Useful for creating circles
* and the like.
*
* Parameters:
* origin - {<OpenLayers.Geometry.Point>} center of polygon.
* radius - {Float} distance to vertex, in map units.
* sides - {Integer} Number of sides. 20 approximates a circle.
* rotation - {Float} original angle of rotation, in degrees.
*/
OpenLayers.Geometry.Polygon.createRegularPolygon = function(origin, radius, sides, rotation) {
var angle = Math.PI * ((1/sides) - (1/2));
if(rotation) {
angle += (rotation / 180) * Math.PI;
}
var rotateAngle, x, y;
var points = [];
for(var i=0; i<sides; ++i) {
rotatedAngle = angle + (i * 2 * Math.PI / sides);
x = origin.x + (radius * Math.cos(rotatedAngle));
y = origin.y + (radius * Math.sin(rotatedAngle));
points.push(new OpenLayers.Geometry.Point(x, y));
}
var ring = new OpenLayers.Geometry.LinearRing(points);
return new OpenLayers.Geometry.Polygon([ring]);
}

View File

@@ -0,0 +1,356 @@
/* Copyright (c) 2006-2007 MetaCarta, Inc., published under a modified BSD license.
* See http://svn.openlayers.org/trunk/openlayers/repository-license.txt
* for the full text of the license. */
/**
* @requires OpenLayers/Handler/Drag.js
*
* Class: OpenLayers.Handler.RegularPolygon
* Handler to draw a regular polygon on the map. Polygon is displayed on mouse
* down, moves or is modified on mouse move, and is finished on mouse up.
* The handler triggers callbacks for 'done' and 'cancel'. Create a new
* instance with the <OpenLayers.Handler.RegularPolygon> constructor.
*
* Inherits from:
* - <OpenLayers.Handler>
*/
OpenLayers.Handler.RegularPolygon = OpenLayers.Class(OpenLayers.Handler.Drag, {
/**
* APIProperty: sides
* {Integer} Number of sides for the regular polygon. Needs to be greater
* than 2. Defaults to 4.
*/
sides: 4,
/**
* APIProperty: radius
* {Float} Optional radius in map units of the regular polygon. If this is
* set to some non-zero value, a polygon with a fixed radius will be
* drawn and dragged with mose movements. If this property is not
* set, dragging changes the radius of the polygon. Set to null by
* default.
*/
radius: null,
/**
* APIProperty: snapAngle
* {Float} If set to a non-zero value, the handler will snap the polygon
* rotation to multiples of the snapAngle. Value is an angle measured
* in degrees counterclockwise from the positive x-axis.
*/
snapAngle: null,
/**
* APIProperty: snapToggle
* {String} If set, snapToggle is checked on mouse events and will set
* the snap mode to the opposite of what it currently is. To disallow
* toggling between snap and non-snap mode, set freehandToggle to
* null. Acceptable toggle values are 'shiftKey', 'ctrlKey', and
* 'altKey'. Snap mode is only possible if this.snapAngle is set to a
* non-zero value.
*/
snapToggle: 'shiftKey',
/**
* APIProperty: persist
* {Boolean} Leave the feature rendered until clear is called. Default
* is false. If set to true, the feature remains rendered until
* clear is called, typically by deactivating the handler or starting
* another drawing.
*/
persist: false,
/**
* Property: angle
* {Float} The angle from the origin (mouse down) to the current mouse
* position, in radians. This is measured counterclockwise from the
* positive x-axis.
*/
angle: null,
/**
* Property: fixedRadius
* {Boolean} The polygon has a fixed radius. True if a radius is set before
* drawing begins. False otherwise.
*/
fixedRadius: false,
/**
* Property: feature
* {<OpenLayers.Feature.Vector>} The currently drawn polygon feature
*/
feature: null,
/**
* Property: layer
* {<OpenLayers.Layer.Vector>} The temporary drawing layer
*/
layer: null,
/**
* Property: origin
* {<OpenLayers.Geometry.Point>} Location of the first mouse down
*/
origin: null,
/**
* Constructor: OpenLayers.Handler.RegularPolygon
* Create a new regular polygon handler.
*
* Parameters:
* control - {<OpenLayers.Control>} The control that owns this handler
* callbacks - {Array} An object with a 'done' property whos value is a
* function to be called when the polygon drawing is finished.
* The callback should expect to recieve a single argument,
* the polygon geometry. If the callbacks object contains a
* 'cancel' property, this function will be called when the
* handler is deactivated while drawing. The cancel should
* expect to receive a geometry.
* options - {Object} An object with properties to be set on the handler.
* If the options.sides property is not specified, the number of sides
* will default to 4.
*/
initialize: function(control, callbacks, options) {
this.style = OpenLayers.Util.extend(OpenLayers.Feature.Vector.style['default'], {});
OpenLayers.Handler.prototype.initialize.apply(this,
[control, callbacks, options]);
this.options = (options) ? options : new Object();
},
/**
* APIMethod: setOptions
*
* Parameters:
* newOptions - {Object}
*/
setOptions: function (newOptions) {
OpenLayers.Util.extend(this.options, newOptions);
OpenLayers.Util.extend(this, newOptions);
},
/**
* APIMethod: activate
* Turn on the handler.
*
* Return:
* {Boolean} The handler was successfully activated
*/
activate: function() {
var activated = false;
if(OpenLayers.Handler.prototype.activate.apply(this, arguments)) {
// create temporary vector layer for rendering geometry sketch
var options = {displayInLayerSwitcher: false};
this.layer = new OpenLayers.Layer.Vector(this.CLASS_NAME, options);
this.map.addLayer(this.layer);
activated = true;
}
return activated;
},
/**
* APIMethod: deactivate
* Turn off the handler.
*
* Return:
* {Boolean} The handler was successfully deactivated
*/
deactivate: function() {
var deactivated = false;
if(OpenLayers.Handler.Drag.prototype.deactivate.apply(this, arguments)) {
// call the cancel callback if mid-drawing
if(this.dragging) {
this.cancel();
}
this.map.removeLayer(this.layer, false);
this.layer.destroy();
if (this.feature) {
this.feature.destroy();
}
deactivated = true;
}
return deactivated;
},
/**
* Method: downFeature
* Start drawing a new feature
*
* Parameters:
* evt - {Event} The drag start event
*/
down: function(evt) {
this.fixedRadius = !!(this.radius);
var maploc = this.map.getLonLatFromPixel(evt.xy);
this.origin = new OpenLayers.Geometry.Point(maploc.lon, maploc.lat);
// create the new polygon
if(!this.fixedRadius) {
// smallest radius should not be less one pixel in map units
// VML doesn't behave well with smaller
this.radius = this.map.getResolution();
}
if(this.persist) {
this.clear();
}
this.feature = new OpenLayers.Feature.Vector();
this.createGeometry();
this.layer.addFeatures([this.feature]);
this.layer.drawFeature(this.feature, this.style);
},
/**
* Method: move
* Respond to drag move events
*
* Parameters:
* evt - {Evt} The move event
*/
move: function(evt) {
var maploc = this.map.getLonLatFromPixel(evt.xy);
var point = new OpenLayers.Geometry.Point(maploc.lon, maploc.lat);
if(this.fixedRadius) {
this.origin = point;
} else {
this.calculateAngle(point, evt);
this.radius = Math.max(this.map.getResolution() / 2,
point.distanceTo(this.origin));
}
this.modifyGeometry();
this.layer.drawFeature(this.feature, this.style);
},
/**
* Method: up
* Finish drawing the feature
*
* Parameters:
* evt - {Event} The mouse up event
*/
up: function(evt) {
this.finalize();
},
/**
* Method: out
* Finish drawing the feature.
*
* Parameters:
* evt - {Event} The mouse out event
*/
out: function(evt) {
this.finalize();
},
/**
* Method: createGeometry
* Create the new polygon geometry. This is called at the start of the
* drag and at any point during the drag if the number of sides
* changes.
*/
createGeometry: function() {
this.angle = Math.PI * ((1/this.sides) - (1/2));
if(this.snapAngle) {
this.angle += this.snapAngle * (Math.PI / 180);
}
this.feature.geometry = OpenLayers.Geometry.Polygon.createRegularPolygon(
this.origin, this.radius, this.sides, this.snapAngle
);
},
/**
* Method: modifyGeometry
* Modify the polygon geometry in place.
*/
modifyGeometry: function() {
var angle, dx, dy, point;
var ring = this.feature.geometry.components[0];
// if the number of sides ever changes, create a new geometry
if(ring.components.length != (this.sides + 1)) {
this.createGeometry();
}
for(var i=0; i<this.sides; ++i) {
point = ring.components[i];
angle = this.angle + (i * 2 * Math.PI / this.sides);
point.x = this.origin.x + (this.radius * Math.cos(angle));
point.y = this.origin.y + (this.radius * Math.sin(angle));
point.clearBounds();
}
},
/**
* Method: calculateAngle
* Calculate the angle based on settings.
*
* Parameters:
* point - {OpenLayers.Geometry.Point}
* evt - {Event}
*/
calculateAngle: function(point, evt) {
var alpha = Math.atan2(point.y - this.origin.y,
point.x - this.origin.x);
if(this.snapAngle && (this.snapToggle && !evt[this.snapToggle])) {
var snapAngleRad = (Math.PI / 180) * this.snapAngle;
this.angle = Math.round(alpha / snapAngleRad) * snapAngleRad;
} else {
this.angle = alpha;
}
},
/**
* APIMethod: cancel
* Finish the geometry and call the "cancel" callback.
*/
cancel: function() {
// the polygon geometry gets cloned in the callback method
this.callback("cancel", null);
this.finalize();
},
/**
* Method: finalize
* Finish the geometry and call the "done" callback.
*/
finalize: function() {
this.origin = null;
this.radius = this.options.radius;
},
/**
* APIMethod: clear
* Clear any rendered features on the temporary layer. This is called
* when the handler is deactivated, canceled, or done (unless persist
* is true).
*/
clear: function() {
this.layer.renderer.clear();
this.layer.destroyFeatures();
},
/**
* Method: callback
* Trigger the control's named callback with the given arguments
*
* Parameters:
* name - {String} The key for the callback that is one of the properties
* of the handler's callbacks object.
* args - {Array} An array of arguments with which to call the callback
* (defined by the control).
*/
callback: function (name, args) {
// override the callback method to always send the polygon geometry
if (this.callbacks[name]) {
this.callbacks[name].apply(this.control,
[this.feature.geometry.clone()]);
}
// since sketch features are added to the temporary layer
// they must be cleared here if done or cancel
if(!this.persist && (name == "done" || name == "cancel")) {
this.clear();
}
},
/** @final @type String */
CLASS_NAME: "OpenLayers.Handler.RegularPolygon"
});

View File

@@ -173,6 +173,47 @@
}
function test_Polygon_createRegular(t) {
t.plan(22);
var sides = 40;
var poly = OpenLayers.Geometry.Polygon.createRegularPolygon(new OpenLayers.Geometry.Point(5,0), 6, sides);
var polyBounds = poly.getBounds();
t.eq(polyBounds.toBBOX(), "-0.981504,-5.981504,10.981504,5.981504", sides + " sided figure generates correct bbox.");
t.eq(poly.components.length, 1, "Poly has one linear ring");
t.eq(poly.components[0].components.length, sides + 1, "ring has 41 components");
t.eq(poly.components[0].components[0].id, poly.components[0].components[sides].id, "ring starts and ends with same geom");
t.eq(Math.round(poly.getArea()), Math.round(Math.PI * 36), "area of "+sides+" sided poly rounds to same area as a circle.");
var sides = 3;
var poly = OpenLayers.Geometry.Polygon.createRegularPolygon(new OpenLayers.Geometry.Point(5,0), 6, sides);
var polyBounds = poly.getBounds();
t.eq(polyBounds.toBBOX(), "-0.196152,-3,10.196152,6", sides + " sided figure generates correct bbox.");
t.eq(poly.components.length, 1, "Poly has one linear ring");
t.eq(poly.components[0].components.length, sides + 1, "ring has correct count of components");
t.eq(poly.components[0].components[0].id, poly.components[0].components[sides].id, "ring starts and ends with same geom");
t.eq(Math.round(poly.getArea()), 47, "area of 3 sided poly is correct");
var sides = 3;
var poly3 = OpenLayers.Geometry.Polygon.createRegularPolygon(new OpenLayers.Geometry.Point(10,0), 15, sides);
var polyBounds = poly3.getBounds();
t.eq(polyBounds.toBBOX(), "-2.990381,-7.5,22.990381,15", sides + " sided figure generates correct bbox.");
t.eq(Math.round(polyBounds.getCenterLonLat().lon), 10, "longitude of center of bounds is same as origin");
t.eq(poly3.components.length, 1, "Poly has one linear ring");
t.eq(poly3.components[0].components.length, sides + 1, "ring has correct count of components");
t.eq(poly3.components[0].components[0].id, poly3.components[0].components[sides].id, "ring starts and ends with same geom");
t.ok(poly3.getArea() > poly.getArea(), "area with radius 15 > poly with radius 6");
var sides = 4;
var poly4 = OpenLayers.Geometry.Polygon.createRegularPolygon(new OpenLayers.Geometry.Point(10,0), 15, sides);
var polyBounds = poly4.getBounds();
t.eq(polyBounds.toBBOX(), "-0.606602,-10.606602,20.606602,10.606602", sides + " sided figure generates correct bbox.");
t.eq(Math.round(polyBounds.getCenterLonLat().lon), 10, "longitude of center of bounds is same as origin");
t.eq(poly4.components.length, 1, "Poly has one linear ring");
t.eq(poly4.components[0].components.length, sides + 1, "ring has correct count of components");
t.eq(poly4.components[0].components[0].id, poly4.components[0].components[sides].id, "ring starts and ends with same geom");
t.ok(poly4.getArea() > poly3.getArea(), "square with radius 15 > triangle with radius 15");
}
function test_Polygon_equals(t) {
t.plan(3);

View File

@@ -0,0 +1,135 @@
<html>
<head>
<script src="../../lib/OpenLayers.js"></script>
<script type="text/javascript">
function test_Handler_RegularPolygon_constructor(t) {
t.plan(3);
var control = new OpenLayers.Control();
control.id = Math.random();
var callbacks = {foo: "bar"};
var options = {bar: "foo"};
var oldInit = OpenLayers.Handler.prototype.initialize;
OpenLayers.Handler.prototype.initialize = function(con, call, opt) {
t.eq(con.id, control.id,
"constructor calls parent with the correct control");
t.eq(call, callbacks,
"constructor calls parent with the correct callbacks");
t.eq(opt, options,
"regular polygon constructor calls parent with the correct options");
}
var handler = new OpenLayers.Handler.RegularPolygon(control, callbacks, options);
OpenLayers.Handler.prototype.initialize = oldInit;
}
function test_Handler_RegularPolygon_activation(t) {
t.plan(3);
var map = new OpenLayers.Map('map');
var control = new OpenLayers.Control();
map.addControl(control);
var handler = new OpenLayers.Handler.RegularPolygon(control);
handler.active = true;
var activated = handler.activate();
t.ok(!activated,
"activate returns false if the handler was already active");
handler.active = false;
activated = handler.activate();
t.ok(activated,
"activate returns true if the handler was not already active");
activated = handler.deactivate();
t.ok(activated,
"deactivate returns true if the handler was active already");
}
function test_Handler_RegularPolygon_four_corners(t) {
t.plan(7);
var map = new OpenLayers.Map('map');
map.addLayer(new OpenLayers.Layer.WMS("", "", {}));
map.zoomToMaxExtent();
var control = new OpenLayers.Control();
map.addControl(control);
var handler = new OpenLayers.Handler.RegularPolygon(control, {});
var activated = handler.activate();
var evt = {xy: new OpenLayers.Pixel(150, 75), which: 1};
handler.down(evt);
var evt = {xy: new OpenLayers.Pixel(175, 75), which: 1};
handler.move(evt);
t.eq(handler.feature.geometry.getBounds().toBBOX(),
"-35.15625,-35.15625,35.15625,35.15625",
"correct bounds after move");
t.eq(handler.feature.geometry.components[0].components.length, 5,
"geometry has 5 components");
t.eq(handler.feature.geometry.CLASS_NAME,
"OpenLayers.Geometry.Polygon",
"geometry is a polygon");
t.eq(handler.radius, 25*1.40625, "feature radius as set on handler");
var evt = {xy: new OpenLayers.Pixel(175, 80), which: 1};
handler.move(evt);
t.eq(handler.feature.geometry.getBounds().toBBOX(),
"-35.15625,-35.15625,35.15625,35.15625",
"correct bounds after move with a fixed radius");
handler.cancel();
handler.setOptions({radius:2 / Math.sqrt(2)});
var evt = {xy: new OpenLayers.Pixel(150, 75), which: 1};
handler.down(evt);
t.eq(handler.feature.geometry.getBounds().toBBOX(),
"-1,-1,1,1",
"bounds with manual radius setting");
var evt = {xy: new OpenLayers.Pixel(175, 90), which: 1};
handler.move(evt);
t.eq(handler.feature.geometry.getBounds().toBBOX(),
"34.15625,-22.09375,36.15625,-20.09375",
"bounds with manual radius setting and mousemove");
}
function test_Handler_RegularPolygon_circle(t) {
t.plan(7);
var map = new OpenLayers.Map('map');
map.addLayer(new OpenLayers.Layer.WMS("", "", {}));
map.zoomToMaxExtent();
var control = new OpenLayers.Control();
map.addControl(control);
var handler = new OpenLayers.Handler.RegularPolygon(control, {}, {'sides':40});
var activated = handler.activate();
var evt = {xy: new OpenLayers.Pixel(150, 75), which: 1};
handler.down(evt);
var evt = {xy: new OpenLayers.Pixel(175, 75), which: 1};
handler.move(evt);
t.eq(handler.feature.geometry.getBounds().toBBOX(),
"-35.15625,-35.15625,35.15625,35.15625",
"correct bounds after move");
t.eq(handler.feature.geometry.components[0].components.length, 41,
"geometry has correct numbre of components");
t.eq(handler.feature.geometry.CLASS_NAME,
"OpenLayers.Geometry.Polygon",
"geometry is a polygon");
t.eq(handler.radius, 25*1.40625, "feature radius as set on handler");
var evt = {xy: new OpenLayers.Pixel(175, 80), which: 1};
handler.move(evt);
t.eq(handler.feature.geometry.getBounds().toBBOX(),
"-35.823348,-35.823348,35.823348,35.823348",
"correct bounds after move with fixed radius");
handler.cancel();
handler.setOptions({radius:1});
var evt = {xy: new OpenLayers.Pixel(150, 75), which: 1};
handler.down(evt);
t.eq(handler.feature.geometry.getBounds().toBBOX(),
"-0.996917,-0.996917,0.996917,0.996917",
"bounds with manual radius setting");
var evt = {xy: new OpenLayers.Pixel(175, 80), which: 1};
handler.move(evt);
t.eq(handler.feature.geometry.getBounds().toBBOX(),
"34.159333,-8.028167,36.153167,-6.034333",
"bounds with manual radius setting and mousemove");
}
</script>
</head>
<body>
<div id="map" style="width: 300px; height: 150px;"/>
</body>
</html>

View File

@@ -80,5 +80,6 @@
<li>Handler/test_Point.html</li>
<li>Handler/test_Path.html</li>
<li>Handler/test_Polygon.html</li>
<li>Handler/test_RegularPolygon.html</li>
<li>test_Map.html</li>
</ul>