/* Copyright (c) 2006-2010 by OpenLayers Contributors (see authors.txt for
* full list of contributors). Published under the Clear BSD license.
* See http://svn.openlayers.org/trunk/openlayers/license.txt for the
* full text of the license. */
/**
* @requires OpenLayers/Geometry.js
*/
/**
* Class: OpenLayers.Geometry.Collection
* A Collection is exactly what it sounds like: A collection of different
* Geometries. These are stored in the local parameter <components> (which
* can be passed as a parameter to the constructor).
*
* As new geometries are added to the collection, they are NOT cloned.
* When removing geometries, they need to be specified by reference (ie you
* have to pass in the *exact* geometry to be removed).
*
* The <getArea> and <getLength> functions here merely iterate through
* the components, summing their respective areas and lengths.
*
* Create a new instance with the <OpenLayers.Geometry.Collection> constructor.
*
* Inerhits from:
* - <OpenLayers.Geometry>
*/
OpenLayers.Geometry.Collection = OpenLayers.Class(OpenLayers.Geometry, {
/**
* APIProperty: components
* {Array(<OpenLayers.Geometry>)} The component parts of this geometry
*/
components: null,
/**
* Property: componentTypes
* {Array(String)} An array of class names representing the types of
* components that the collection can include. A null value means the
* component types are not restricted.
*/
componentTypes: null,
/**
* Constructor: OpenLayers.Geometry.Collection
* Creates a Geometry Collection -- a list of geoms.
*
* Parameters:
* components - {Array(<OpenLayers.Geometry>)} Optional array of geometries
*
*/
initialize: function (components) {
OpenLayers.Geometry.prototype.initialize.apply(this, arguments);
this.components = [];
if (components != null) {
this.addComponents(components);
}
},
/**
* APIMethod: destroy
* Destroy this geometry.
*/
destroy: function () {
this.components.length = 0;
this.components = null;
OpenLayers.Geometry.prototype.destroy.apply(this, arguments);
},
/**
* APIMethod: clone
* Clone this geometry.
*
* Returns:
* {<OpenLayers.Geometry.Collection>} An exact clone of this collection
*/
clone: function() {
var geometry = eval("new " + this.CLASS_NAME + "()");
for(var i=0, len=this.components.length; i<len; i++) {
geometry.addComponent(this.components[i].clone());
}
// catch any randomly tagged-on properties
OpenLayers.Util.applyDefaults(geometry, this);
return geometry;
},
/**
* Method: getComponentsString
* Get a string representing the components for this collection
*
* Returns:
* {String} A string representation of the components of this geometry
*/
getComponentsString: function(){
var strings = [];
for(var i=0, len=this.components.length; i<len; i++) {
strings.push(this.components[i].toShortString());
}
return strings.join(",");
},
/**
* APIMethod: calculateBounds
* Recalculate the bounds by iterating through the components and
* calling calling extendBounds() on each item.
*/
calculateBounds: function() {
this.bounds = null;
if ( this.components && this.components.length > 0) {
this.setBounds(this.components[0].getBounds());
for (var i=1, len=this.components.length; i<len; i++) {
this.extendBounds(this.components[i].getBounds());
}
}
},
/**
* APIMethod: addComponents
* Add components to this geometry.
*
* Parameters:
* components - {Array(<OpenLayers.Geometry>)} An array of geometries to add
*/
addComponents: function(components){
if(!(components instanceof Array)) {
components = [components];
}
for(var i=0, len=components.length; i<len; i++) {
this.addComponent(components[i]);
}
},
/**
* Method: addComponent
* Add a new component (geometry) to the collection. If this.componentTypes
* is set, then the component class name must be in the componentTypes array.
*
* The bounds cache is reset.
*
* Parameters:
* component - {<OpenLayers.Geometry>} A geometry to add
* index - {int} Optional index into the array to insert the component
*
* Returns:
* {Boolean} The component geometry was successfully added
*/
addComponent: function(component, index) {
var added = false;
if(component) {
if(this.componentTypes == null ||
(OpenLayers.Util.indexOf(this.componentTypes,
component.CLASS_NAME) > -1)) {
if(index != null && (index < this.components.length)) {
var components1 = this.components.slice(0, index);
var components2 = this.components.slice(index,
this.components.length);
components1.push(component);
this.components = components1.concat(components2);
} else {
this.components.push(component);
}
component.parent = this;
this.clearBounds();
added = true;
}
}
return added;
},
/**
* APIMethod: removeComponents
* Remove components from this geometry.
*
* Parameters:
* components - {Array(<OpenLayers.Geometry>)} The components to be removed
*/
removeComponents: function(components) {
if(!(components instanceof Array)) {
components = [components];
}
for(var i=components.length-1; i>=0; --i) {
this.removeComponent(components[i]);
}
},
/**
* Method: removeComponent
* Remove a component from this geometry.
*
* Parameters:
* component - {<OpenLayers.Geometry>}
*/
removeComponent: function(component) {
OpenLayers.Util.removeItem(this.components, component);
// clearBounds() so that it gets recalculated on the next call
// to this.getBounds();
this.clearBounds();
},
/**
* APIMethod: getLength
* Calculate the length of this geometry
*
* Returns:
* {Float} The length of the geometry
*/
getLength: function() {
var length = 0.0;
for (var i=0, len=this.components.length; i<len; i++) {
length += this.components[i].getLength();
}
return length;
},
/**
* APIMethod: getArea
* Calculate the area of this geometry. Note how this function is overridden
* in <OpenLayers.Geometry.Polygon>.
*
* Returns:
* {Float} The area of the collection by summing its parts
*/
getArea: function() {
var area = 0.0;
for (var i=0, len=this.components.length; i<len; i++) {
area += this.components[i].getArea();
}
return area;
},
/**
* APIMethod: getGeodesicArea
* Calculate the approximate area of the polygon were it projected onto
* the earth.
*
* Parameters:
* projection - {<OpenLayers.Projection>} The spatial reference system
* for the geometry coordinates. If not provided, Geographic/WGS84 is
* assumed.
*
* Reference:
* Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for
* Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion
* Laboratory, Pasadena, CA, June 2007 http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409
*
* Returns:
* {float} The approximate geodesic area of the geometry in square meters.
*/
getGeodesicArea: function(projection) {
var area = 0.0;
for(var i=0, len=this.components.length; i<len; i++) {
area += this.components[i].getGeodesicArea(projection);
}
return area;
},
/**
* APIMethod: getCentroid
*
* Compute the centroid for this geometry collection.
*
* Parameters:
* weighted - {Boolean} Perform the getCentroid computation recursively,
* returning an area weighted average of all geometries in this collection.
*
* Returns:
* {<OpenLayers.Geometry.Point>} The centroid of the collection
*/
getCentroid: function(weighted) {
if (!weighted) {
return this.components.length && this.components[0].getCentroid();
}
var len = this.components.length;
if (!len) {
return false;
}
var areas = [];
var centroids = [];
var areaSum = 0;
var minArea = Number.MAX_VALUE;
var component;
for (var i=0; i<len; ++i) {
component = this.components[i];
var area = component.getArea();
var centroid = component.getCentroid(true);
if (isNaN(area) || isNaN(centroid.x) || isNaN(centroid.y)) {
continue;
}
areas.push(area);
areaSum += area;
minArea = (area < minArea && area > 0) ? area : minArea;
centroids.push(centroid);
}
len = areas.length;
if (areaSum === 0) {
// all the components in this collection have 0 area
// probably a collection of points -- weight all the points the same
for (var i=0; i<len; ++i) {
areas[i] = 1;
}
areaSum = areas.length;
} else {
// normalize all the areas where the smallest area will get
// a value of 1
for (var i=0; i<len; ++i) {
areas[i] /= minArea;
}
areaSum /= minArea;
}
var xSum = 0, ySum = 0, centroid, area;
for (var i=0; i<len; ++i) {
centroid = centroids[i];
area = areas[i];
xSum += centroid.x * area;
ySum += centroid.y * area;
}
return new OpenLayers.Geometry.Point(xSum/areaSum, ySum/areaSum);
},
/**
* APIMethod: getGeodesicLength
* Calculate the approximate length of the geometry were it projected onto
* the earth.
*
* projection - {<OpenLayers.Projection>} The spatial reference system
* for the geometry coordinates. If not provided, Geographic/WGS84 is
* assumed.
*
* Returns:
* {Float} The appoximate geodesic length of the geometry in meters.
*/
getGeodesicLength: function(projection) {
var length = 0.0;
for(var i=0, len=this.components.length; i<len; i++) {
length += this.components[i].getGeodesicLength(projection);
}
return length;
},
/**
* APIMethod: move
* Moves a geometry by the given displacement along positive x and y axes.
* This modifies the position of the geometry and clears the cached
* bounds.
*
* Parameters:
* x - {Float} Distance to move geometry in positive x direction.
* y - {Float} Distance to move geometry in positive y direction.
*/
move: function(x, y) {
for(var i=0, len=this.components.length; i<len; i++) {
this.components[i].move(x, y);
}
},
/**
* APIMethod: rotate
* Rotate a geometry around some origin
*
* Parameters:
* angle - {Float} Rotation angle in degrees (measured counterclockwise
* from the positive x-axis)
* origin - {<OpenLayers.Geometry.Point>} Center point for the rotation
*/
rotate: function(angle, origin) {
for(var i=0, len=this.components.length; i<len; ++i) {
this.components[i].rotate(angle, origin);
}
},
/**
* APIMethod: resize
* Resize a geometry relative to some origin. Use this method to apply
* a uniform scaling to a geometry.
*
* Parameters:
* scale - {Float} Factor by which to scale the geometry. A scale of 2
* doubles the size of the geometry in each dimension
* (lines, for example, will be twice as long, and polygons
* will have four times the area).
* origin - {<OpenLayers.Geometry.Point>} Point of origin for resizing
* ratio - {Float} Optional x:y ratio for resizing. Default ratio is 1.
*
* Returns:
* {OpenLayers.Geometry} - The current geometry.
*/
resize: function(scale, origin, ratio) {
for(var i=0; i<this.components.length; ++i) {
this.components[i].resize(scale, origin, ratio);
}
return this;
},
/**
* APIMethod: distanceTo
* Calculate the closest distance between two geometries (on the x-y plane).
*
* Parameters:
* geometry - {<OpenLayers.Geometry>} The target geometry.
* options - {Object} Optional properties for configuring the distance
* calculation.
*
* Valid options:
* details - {Boolean} Return details from the distance calculation.
* Default is false.
* edge - {Boolean} Calculate the distance from this geometry to the
* nearest edge of the target geometry. Default is true. If true,
* calling distanceTo from a geometry that is wholly contained within
* the target will result in a non-zero distance. If false, whenever
* geometries intersect, calling distanceTo will return 0. If false,
* details cannot be returned.
*
* Returns:
* {Number | Object} The distance between this geometry and the target.
* If details is true, the return will be an object with distance,
* x0, y0, x1, and y1 properties. The x0 and y0 properties represent
* the coordinates of the closest point on this geometry. The x1 and y1
* properties represent the coordinates of the closest point on the
* target geometry.
*/
distanceTo: function(geometry, options) {
var edge = !(options && options.edge === false);
var details = edge && options && options.details;
var result, best, distance;
var min = Number.POSITIVE_INFINITY;
for(var i=0, len=this.components.length; i<len; ++i) {
result = this.components[i].distanceTo(geometry, options);
distance = details ? result.distance : result;
if(distance < min) {
min = distance;
best = result;
if(min == 0) {
break;
}
}
}
return best;
},
/**
* APIMethod: equals
* Determine whether another geometry is equivalent to this one. Geometries
* are considered equivalent if all components have the same coordinates.
*
* Parameters:
* geom - {<OpenLayers.Geometry>} The geometry to test.
*
* Returns:
* {Boolean} The supplied geometry is equivalent to this geometry.
*/
equals: function(geometry) {
var equivalent = true;
if(!geometry || !geometry.CLASS_NAME ||
(this.CLASS_NAME != geometry.CLASS_NAME)) {
equivalent = false;
} else if(!(geometry.components instanceof Array) ||
(geometry.components.length != this.components.length)) {
equivalent = false;
} else {
for(var i=0, len=this.components.length; i<len; ++i) {
if(!this.components[i].equals(geometry.components[i])) {
equivalent = false;
break;
}
}
}
return equivalent;
},
/**
* APIMethod: transform
* Reproject the components geometry from source to dest.
*
* Parameters:
* source - {<OpenLayers.Projection>}
* dest - {<OpenLayers.Projection>}
*
* Returns:
* {<OpenLayers.Geometry>}
*/
transform: function(source, dest) {
if (source && dest) {
for (var i=0, len=this.components.length; i<len; i++) {
var component = this.components[i];
component.transform(source, dest);
}
this.bounds = null;
}
return this;
},
/**
* APIMethod: intersects
* Determine if the input geometry intersects this one.
*
* Parameters:
* geometry - {<OpenLayers.Geometry>} Any type of geometry.
*
* Returns:
* {Boolean} The input geometry intersects this one.
*/
intersects: function(geometry) {
var intersect = false;
for(var i=0, len=this.components.length; i<len; ++ i) {
intersect = geometry.intersects(this.components[i]);
if(intersect) {
break;
}
}
return intersect;
},
/**
* APIMethod: getVertices
* Return a list of all points in this geometry.
*
* Parameters:
* nodes - {Boolean} For lines, only return vertices that are
* endpoints. If false, for lines, only vertices that are not
* endpoints will be returned. If not provided, all vertices will
* be returned.
*
* Returns:
* {Array} A list of all vertices in the geometry.
*/
getVertices: function(nodes) {
var vertices = [];
for(var i=0, len=this.components.length; i<len; ++i) {
Array.prototype.push.apply(
vertices, this.components[i].getVertices(nodes)
);
}
return vertices;
},
CLASS_NAME: "OpenLayers.Geometry.Collection"
});