Source: maps/geometry.js


/**
 * @param {google.maps.Polygon|google.maps.Polyline} poly  The polygon or polyline.
 * @return {google.maps.LatLngBounds}  The bounding box for the polygon or polyline.
 */
lucid.maps.geometry.computeBounds = function( poly )
{
	var polyline = poly.getPath();
	// NB: Polyline and a Polygon both support the 'getPath' method.
	//     In the case of a polygon 'getPath' will return the first path (the exterior path).
	//     We only need to process the exterior path to compute a Polygon's bounds.
	
	var bounds = new google.maps.LatLngBounds();
	
	var points = polyline.getArray();
	for (var i=0; i<points.length; i++)
	{
		bounds.extend( points[i] );
	}
	
	return bounds;
};

/**
 * @param {google.maps.LatLng} from  The point the heading is measured from.
 * @param {google.maps.LatLng} to  The point the heading is measured to.
 * @return {google.maps.LatLng}  The heading from one LatLng to the other, normalised into the range 0 to 360.
 */
lucid.maps.geometry.computeNormalisedHeading = function( from, to )
{
	// TODO Use non-geodesic heading.
	//      I have raised a feature request for this in the Google Maps API: https://code.google.com/p/gmaps-api-issues/issues/detail?id=5928
	var heading = google.maps.geometry.spherical.computeHeading( from, to );

	// The function above can return a negative value. So normalise the value into the range 0 to 360.
	return lucid.maps.geometry.normaliseHeading( heading );
};

/**
 * @param {google.maps.LatLngBounds} boundingBox  The bounding box.
 * @param {number} heading  The heading from the centre of the bounding box.
 *                          The heading must be normalised (i.e. in the range 0 to 360).
 * @return {google.maps.LatLng}  The point on the edge of the bounding box that is on the given heading.
 */
lucid.maps.geometry.computeLatLngBoundsIntersection = function( boundingBox, heading )
{
	var boxWidth = boundingBox.getNorthEast().lng() - boundingBox.getSouthWest().lng();
	var boxHeight = boundingBox.getNorthEast().lat() - boundingBox.getSouthWest().lat();
	
	var intersection = lucid.maps.geometry.computeBoundingBoxIntersection( boxWidth, boxHeight, boundingBox.getCenter().lng(), boundingBox.getCenter().lat(), heading );
	
	return new google.maps.LatLng( intersection.y, intersection.x );
};

/**
 * @param {number} width    The width of the bounding box.
 * @param {number} height   The height of the bounding box.
 * @param {number} centreX  The x coordinate of the centre of the bounding box.
 * @param {number} centreY  The y coordinate of the centre of the bounding box.
 * @param {number} heading  The heading, in degrees, measured clockwise from North. Must be in the range 0 to 360.
 * @return {object}  An object containing the x and y coordinates of the intersection on the edge of the bounding box.
 */
lucid.maps.geometry.computeBoundingBoxIntersection = function( width, height, centreX, centreY, heading )
{
	var angleInRadians = lucid.maps.geometry.convertHeadingToAngle( heading );
	
	return lucid.maps.geometry.computeBoundingBoxIntersectionAtAngle( width, height, centreX, centreY, angleInRadians );
}

/**
 * @param {number} width    The width of the bounding box.
 * @param {number} height   The height of the bounding box.
 * @param {number} centreX  The x coordinate of the centre of the bounding box.
 * @param {number} centreY  The y coordinate of the centre of the bounding box.
 * @param {number} angle    The angle, in radians, measured anti-clockwise from the right (East). Must be in the range 0 to 2PI.
 * @return {object}  An object containing the x and y coordinates of the intersection on the edge of the bounding box.
 */
lucid.maps.geometry.computeBoundingBoxIntersectionAtAngle = function( width, height, centreX, centreY, angle )
{
	// NB: Maps tend to be landscape.
	//     So probability states the heading to a target object will exit through the top or bottom edge of the map.
	
	// Look for an intersection on the top or bottom edge.
	var intersectionY = (angle > Math.PI) ? -(height / 2) : (height / 2);
	var distanceToIntersection = intersectionY / Math.sin( angle );
	var intersectionX = distanceToIntersection * Math.cos( angle );
	
	if (Math.abs( intersectionX ) > (width / 2))
	{
		// The intersection hits the left/right edge before the top/bottom edge.
		// So re-compute the intersection for the left or right edge.
		intersectionX = ((angle > (Math.HALF_PI)) && (angle < (Math.THREE_HALVES_PI))) ? -(width / 2) : (width / 2);
		distanceToIntersection = intersectionX / Math.cos( angle );
		intersectionY = distanceToIntersection * Math.sin( angle );
	}
	
	return { x: centreX + intersectionX,
	         y: centreY + intersectionY };
};

/**
 * Adjust a heading to account for the view of the map being tilted.
 * 
 * @param {number} heading  The heading, in degrees, measured clockwise from North.
 * @param {number} tilt  The angle of incidence of the map, in degrees from the viewport plane to the map plane.
 *                       An angle of 0 indicates the map is being viewed from directly overhead.
 * @return {number}  The angle the heading makes with the viewport.
 */
lucid.maps.geometry.tiltedHeading = function( heading, tilt )
{
	var headingInRadians = lucid.maps.geometry.convertHeadingToAngle( heading );
	
	return lucid.maps.geometry.tiltedAngle( headingInRadians, tilt );
};

/**
 * Adjust an angle to account for the view of the map being tilted.
 * 
 * @param {number} angle  The angle, in radians, measured anti-clockwise from the right (East).
 * @param {number} tilt  The angle of incidence of the map, in degrees from the viewport plane to the map plane.
 *                       An angle of 0 indicates the map is being viewed from directly overhead.
 * @return {number}  The angle the initial angle makes with the viewport.
 */
lucid.maps.geometry.tiltedAngle = function( angle, tilt )
{
	var tiltInRadians = lucid.maps.geometry.convertToRadians( tilt );
	
	var xLength = Math.cos( angle );
	var yLength = Math.sin( angle );
	
	// adjust for the tilt
	yLength = yLength * Math.cos( tiltInRadians );
	
	return Math.vectorAngle( xLength, yLength );
};

/**
 * Convert a heading from degrees to radians.
 * 
 * @param {number} heading  The heading, in degrees, measured clockwise from North.
 * @return {number}  The mathematical angle, in radians, measured anti-clockwise from the right (East).
 */
lucid.maps.geometry.convertHeadingToAngle = function( heading )
{
	return lucid.maps.geometry.convertToRadians( 90 - heading );
};

/**
 * Convert an angle from degrees to radians.
 * 
 * @param {number} angle  The angle in degrees.
 * @return {number}  The angle in radians.
 */
lucid.maps.geometry.convertToRadians = function( angle )
{
	return angle * Math.PI / 180;
};

/**
 * @param {number} heading  The heading, measured in degrees.
 * @return {number}  The heading normalised into the range 0 to 360.
 */
lucid.maps.geometry.normaliseHeading = function( heading )
{
	while (heading > 360)
		heading -= 360;
	while (heading < 0)
		heading += 360;
	
	return heading;
};

/**
 * @param {number} angle  The angle, measured in radians.
 * @return {number}  The angle normalised into the range 0 to 2PI.
 */
lucid.maps.geometry.normaliseAngle = function( angle )
{
	while (angle > Math.TWO_PI)
		angle -= Math.TWO_PI;
	while (angle < 0)
		angle += Math.TWO_PI;
	
	return angle;
};