Source Code for JavaScript Mapping Tools: geoCircle.js

Source code of a graphical tool for drawing and computing distances over Google maps.

Run Tool | index.html | main.css | formatters.js | geoCircle.js | geoCode.js | geo.js | index.js | mapControls.js | tableManager.js | util.js | wayPoint.js | wayPointsManager.js


// Copyright 2006-2008 (c) Paul Demers  <paul@acscdg.com>

// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA., or visit one 
// of the links here:
//  http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
//  http://www.acscdg.com/LICENSE.txt

//////////////////////////////////////////////////////////////////

//
//  map drawing and distance tools.

// Web site with this code running: http://www.acscdg.com/

//
// Geo Circle object.  A geo circle is a circle of equal distance around
//  a point.  It might represent range, or an area withing a certain distance.
//  For example, a search radius, or the range of a rocket.
//

// Dependency on other modules:
//  geo.js
//  formatters.js
//  Google maps (for geo point object).


//
//// Renders a GeoCircle object as an HTML table row element.
function geoCircleToElement(distanceUnits)
{

  var trElement = document.createElement("tr");

  // Allow different formats for odd and even rows, for example to shade them
  //  differently for legibility.  The classes are defined in a CSS file.
  var isEven = (Math.round(this.circleNumber / 2) * 2) == this.circleNumber;
  if (isEven)
    trElement.className = "ptsroweven";
  else
    trElement.className = "ptsrowodd";

  var tdElement;

  var distanceMultiplier = getDistanceMultiplier(distanceUnits);

  tdElement = document.createElement("td");
  tdElement.appendChild(document.createTextNode(this.circleNumber.toString()));
  tdElement.className = "numcol";
  trElement.appendChild(tdElement);  // 0

  tdElement = document.createElement("td");
  tdElement.appendChild(document.createTextNode(" ")); // Unused Point # column
  tdElement.className = "numcol";
  trElement.appendChild(tdElement);  // 1

  tdElement = document.createElement("td");
  tdElement.appendChild(document.createTextNode(myLatToString(this.centerPoint)));
  tdElement.className = "llcol";
  trElement.appendChild(tdElement);  // 2

  tdElement = document.createElement("td");
  tdElement.appendChild(document.createTextNode(myLngToString(this.centerPoint)));
  tdElement.className = "llcol";
  trElement.appendChild(tdElement); // 3

  var radiusString = formatFloat(this.radiusNM * distanceMultiplier);
  tdElement = document.createElement("td");
  tdElement.appendChild(document.createTextNode(radiusString));  // radius
  tdElement.className = "distcol";
  trElement.appendChild(tdElement);  // 4

  tdElement = document.createElement("td");
  tdElement.appendChild(document.createTextNode(" "));  // Unused azimuth column.
  tdElement.className = "distcol";
  trElement.appendChild(tdElement);  // 5

  var circumferenceString;
  if (this.circumferenceNM == null)
    circumferenceString = " ";
  else
    circumferenceString = formatFloat(this.circumferenceNM * distanceMultiplier);

  tdElement = document.createElement("td");
  tdElement.appendChild(document.createTextNode(circumferenceString));  // circumference
  tdElement.className = "distcol";
  trElement.appendChild(tdElement);  // 6

  return trElement;
}


//
//// Replace the radius and circumference TD elements in a TR element
////  created by geoCircleToElement.
function geoCircleUpdateElement(distanceUnits)
{
  var distanceMultiplier = getDistanceMultiplier(distanceUnits);

  // child 0 is course number.
  // child 1 is unused point number
  // child 2 is center lat 
  // child 3 is center lng

  var radiusString = formatFloat(this.radiusNM * distanceMultiplier);
  var tdElement = this.tableRowElement.childNodes[4]; // radius
  tdElement.replaceChild(document.createTextNode(formatFloat(radiusString)), tdElement.childNodes[0]);

  // child 5 is unused azimuth column.

  var circumferenceString = formatFloat(this.circumferenceNM * distanceMultiplier);
  tdElement = this.tableRowElement.childNodes[6]; // circumference
  tdElement.replaceChild(document.createTextNode(formatFloat(circumferenceString)), tdElement.childNodes[0]);

  return this.tableRowElement;
}

//
//// Removes the circle line (and inner circle line), but leaves the center marker.
function geoCircleRemoveLines(map)
{
  if (this.circleLine != null)
    map.removeOverlay(this.circleLine);
  if (this.innerCircleLine != null)
    map.removeOverlay(this.innerCircleLine);
}


//
//// Removes all the overlays for a GeoCircle object.
function geoCircleRemoveOverlays(map)
{
  this.removeLines(map);
  if (this.centerMarker != null)
    map.removeOverlay(this.centerMarker);
}


//
/// Update radius, circumference, and circle line by recomputing from new endPoint.
function geoCircleUpdate(map, edgePoint, finalP, distanceUnits)
{
  this.removeLines(map);

  this.updateEdge(edgePoint, finalP);

  map.addOverlay(this.circleLine);

  if (this.innerCircleLine != null)
    map.addOverlay(this.innerCircleLine);

  this.updateElement(distanceUnits);
}



//
//// Compute the points around the GeoCircle from two points.
//// This method has two modes.  The first mode is not final, which is
//// very fast.  There are less points around the circle, and the inner helper
////  lines are not drawn.
//// The inner line is a set of lines that point to the center of the circle.
//// Circles with a large radius (more than 8,000 NM or so) curve around so much
////  it is hard to know where the center vs. the antipodal center is.  
//// Very very large radius create circles around the points outside of
////  radius (that is, the circle is the points greater than the radius).
//// For these circles, the inner points line is essential.
function geoCircleComputeFromTwoPoints(edgePoint, finalP)
{
  // Compute distance between points.
  var bestCourse = new BestCourse(this.centerPoint, edgePoint);
  var radiusVector = bestCourse.getVector();

  if (!finalP && (this.radiusNM == radiusVector.distanceNM))
    return;   // No difference from last time.

  this.radiusNM = radiusVector.distanceNM;

  // Compute necessary number of steps
  // TODO: Change number of degrees based on radiusNM.
  var angleDelta = null; // must be a factor of 360
  if (finalP)
    angleDelta = 3;
  else
    angleDelta = 12;

  // Start at 0 degrees, walk around in steps, compute lat/lng of distance from center 
  var outerPointsList = new Array();

  for (var nextAngle = 0; nextAngle <= 360; nextAngle += angleDelta)
  {
    var nextAngleR = nextAngle / radiansToDegrees;
    // TODO: Pass EarthVector.  Set radiusR just once.
    var nextPoint = directSolution(this.centerPoint, nextAngleR, this.radiusNM);
    outerPointsList.push(nextPoint);
  }
  // the circle will close because both 0 and 360 are points.

  // Create a polyline from all the points.
  this.circleLine = new GPolyline(outerPointsList, vectorColor, vectorWeight, vectorOpacity, _optsNotClickable);

  //
  // The inner points is drawn on the final drawing of the circle only, because it is so slow.
  if (finalP)
  {
    var innerPointsList = new Array();

    var innerRadiusNM = this.radiusNM * 0.95;

    this.circumferenceNM = 0.0;
    var lastPoint = outerPointsList[0];  // handle first point as special case, because of circumference math.
    innerPointsList.push(lastPoint);  // Because 0 is even.

    for (var p = 1; p < outerPointsList.length; p++)
    {
      // Compute circumference:
      var thisPoint = outerPointsList[p];
      var innerbestCourse = new BestCourse(lastPoint, thisPoint);
      var innerVector = innerbestCourse.getVector();
      this.circumferenceNM += innerVector.distanceNM;

      // Build the inner points:
      var isEven = (Math.round(p / 2) * 2) == p;
      if (isEven)
      {
        innerPointsList.push(thisPoint);
      }
      else
      {
        var nextAngleR = (p * angleDelta) / radiansToDegrees;
        var innerPoint = directSolution(this.centerPoint, nextAngleR, innerRadiusNM);
        innerPointsList.push(innerPoint);
      }

      lastPoint = thisPoint;
    }

    this.innerCircleLine = new GPolyline(innerPointsList, vectorColor, 1, .5, _optsNotClickable);
  }
  else
  {
    this.innerCircleLine = null; 
  }

//  this.outerPointsList = outerPointsList;  // multi circles need the points.

}


//
//// Constructor for the GeoCircle object.
function GeoCircle(map, circleNumber, centerPoint)
{
  this.toElement = geoCircleToElement;
  this.updateElement = geoCircleUpdateElement;
  this.update = geoCircleUpdate;
  this.removeOverlays = geoCircleRemoveOverlays;
  this.removeLines = geoCircleRemoveLines;
  this.updateEdge = geoCircleComputeFromTwoPoints;

  this.circleNumber = circleNumber;
  this.centerPoint = centerPoint;
  this.radiusNM = 0;
  this.circumferenceNM = null;

  var circleTitle = "Circle # " + circleNumber.toString();

  this.centerMarker = new GMarker(centerPoint, {title: circleTitle, icon: _wayPointIcon, clickable: true });
  map.addOverlay(this.centerMarker);

  this.tableRowElement = this.toElement(0);  // Pass distance units of 0, since radius is 0 anyway.
}