﻿// JScript File
var minStep=5; //declara el paso mínimo de incremento en el deepsearch (5 millas)
function wait()
{
            weatherIcons=new Array();//guarda los iconos del tiempo a poner en la tabla
             actualizarMillaje();
            var distance=0;
          var el;
          var query=false;
            if (document.getElementById("routeDetails")==null)
              {
                   el = document.createElement("div"); 
                    el.setAttribute('id',"routeDetails");
                }
             else
             
             {
                el=document.getElementById("routeDetails");
             }   
             
             
             elPrint=document.getElementById('printDetails');
             elMail=document.getElementById('mailRoute');
            consultingWeather=true;
            map.DeleteAllPushpins();
             
             
            pushPinLocations= new Array();
          
            var longitudRuta=routePoints.Itinerary.Distance;
            var totalTime=convertToMinutes(convertToText(routePoints.Itinerary.Distance / document.getElementById('dpDownMilesPerHour').value));
            var  timeFactor;
            if (totalTime>0)
            {
                timeFactor=longitudRuta/totalTime;
            }
            else
            {
                timeFactor=1;
            }
            
            var alturaAnterior=0;
            cantPushpins=(longitudRuta/pasoMillas)+2;
            cantPushpins=parseInt(cantPushpins);
           pinsToDisplay=0;
           var steps="";
           var stepsPrint="";
           var stepsEmail="";
           document.getElementById('print').innerHTML="<img id='image' src='Images/printer.gif' style='cursor:hand;' onclick='Clickheretoprint()' />";
           
           if (routePoints.StartLocation.Address.length<=0)
           {
              var el1=document.getElementById('txtStart');
              
            routePoints.StartLocation.Address=el1.value;
            displayPuntos[0].instruction="Start at " + el1.value;
                
                
           }
           if (routePoints.EndLocation.Address.length<=0)
           {
                var el2=document.getElementById('txtFinish');
                routePoints.EndLocation.Address=el2.value;  
                displayPuntos[displayPuntos.length-1].instruction="Arrive at " + el2.value;  
           }
           
           var routeinfo="<h2>Route Itinerary from " + routePoints.StartLocation.Address + " to " + routePoints.EndLocation.Address + ".<h2>   <br><br>"
                    +"<label> Total distance:</label> <p>" + routePoints.Itinerary.Distance + " " + routePoints.Itinerary.DistanceUnit + "<br><p>"
                    +" <label>Estimated total time: </label><p>" + convertToText(redondear(routePoints.Itinerary.Distance / document.getElementById('dpDownMilesPerHour').value,2)) + "<br><p>"
                    +"<label> Departure Time: <label><p>" + formatDate(startTime) + " " + formatHour(startTime) + "<br><p>";
             tableToPrint="<h1>Route Itinerary from " + routePoints.StartLocation.Address + " to " + routePoints.EndLocation.Address + ".</h1> <br/><br/>"
                    +"<h3><label> Total distance:</label> " + routePoints.Itinerary.Distance + " " + routePoints.Itinerary.DistanceUnit + "</h3>"
                    +"<h3> <label>Estimated total time: </label>" + convertToText(redondear(routePoints.Itinerary.Distance / document.getElementById('dpDownMilesPerHour').value,2)) + "</h3>"
                    +"<h3><label> Departure Time: </label>" + formatDate(startTime) + " " + formatHour(startTime) + "</h3><br/><table>";
             tableToEmail="<h1 style='color:#333;font-size:14px;text-decoration:underline;text-align:center;'>Route Itinerary from " + routePoints.StartLocation.Address + " to " + routePoints.EndLocation.Address + ".</h1> <br/><br/>"
                    +"<h3 style='color:#333;font-size:12px;text-align:left'><label style='font-weight:bold;'> Total distance:</label> " + routePoints.Itinerary.Distance + " " + routePoints.Itinerary.DistanceUnit + "</h3>"
                    +"<h3 style='color:#333;font-size:12px;text-align:left'> <label style='font-weight:bold;'>Estimated total time: </label>" + convertToText(redondear(routePoints.Itinerary.Distance / document.getElementById('dpDownMilesPerHour').value,2)) + "</h3>"
                    +"<h3 style='color:#333;font-size:12px;text-align:left'><label style='font-weight:bold;'> Departure Time: </label>" + formatDate(startTime) + " " + formatHour(startTime) + "</h3><br/><table>";
           
                    if (showBestLavel==true)
                    {
                       routeinfo +=" <div id='recommendLabel'><a onmousedown='proposeBestTime()'>Is this the best time to leave? (Click Here)</a></div> " 
                                    +" <table>";
                    }
                    else
                    {
                        showBestLavel=true;
                        routeinfo+=" <table>";
                    }
                    routeinfo+="<th width='10%' >TOTAL</th><th width='10%'>DISTANCE</th><th width='50%'>INSTRUCTION</th><th width='15%'>TIME</th><th width='15%'>WEATHER</th>";
                    tableToPrint+="<th width='10%'>TOTAL</th><th width='10%'>DISTANCE</th><th width='50%'>INSTRUCTION</th><th width='15%'>TIME</th><th width='15%'>WEATHER</th>";
                    tableToEmail+="<th width='10%' style='text-align:left;font-size:12px;' >TOTAL</th><th  width='10%' style='text-align:left;font-size:12px;' >DISTANCE</th><th  width='50%' style='text-align:left;font-size:12px;'>INSTRUCTION</th><th width='15%' style='text-align:left;font-size:12px;'>TIME</th><th width='15%' style='text-align:left;font-size:12px;'>WEATHER</th>";
                    
                     var ordenPunto=0;
            for (var i=0;i<displayPuntos.length;i++)
            
            {
                
                displayPuntos[i].weather="";
                    ordenPunto=ordenPunto+1;       
                stepTime=displayPuntos[i].acumulativeDistance/ timeFactor;
                 var stepDate= new Date (startTime.getTime()+stepTime*60*1000);
                       displayPuntos[i].time=stepDate;
                  displayPuntos[i].time=formatHour(stepDate);
                
                distance=displayPuntos[i].acumulativeDistance;
                if( (i==0) || (i==displayPuntos.length-1))
                {
                   if (i==displayPuntos.length-1)
                   {
                    var fin=true;
                     points=displayPuntos[i].latitude + ","+displayPuntos[i].longitude+ "*"+cantPushpins+"*"+fin+"*Show";
                     
                   }
                   if (i==0)
                   {
                    var fin=false;
                     points=displayPuntos[i].latitude + ","+displayPuntos[i].longitude + "*"+cantPushpins+"*"+fin+"*Show";
                     
                   }
                   pinsToDisplay=pinsToDisplay+1;
                   displayPuntos[i].weather=cantPushpins;
                   cantPushpins--;
                    query=true;
                    
                }
                else
                {
                
                
                   
               
                  if ((displayPuntos[i].acumulativeDistance-alturaAnterior) >=(pasoMillas) )
                    {
                        var fin=false;
                       
                       if (cantPushpins>0)
                       {
                       points= displayPuntos[i].latitude + ","+displayPuntos[i].longitude+ "*"+cantPushpins+"*"+fin+"*Show";
                       displayPuntos[i].weather=cantPushpins;
                       pinsToDisplay=pinsToDisplay+1;
                        cantPushpins--;
                        
                        query=true;
                        alturaAnterior=displayPuntos[i].acumulativeDistance;   
                       }
                       
                    }
                else
                    {
//                        points= displayPuntos[i].latitude + ","+displayPuntos[i].longitude+ "*"+cantPushpins+"*"+fin+"*Hide";
//                       
//                        if (!rutaCacheada)
//                        {
//                            query=true;
//                        }
//                        else
//                        {
//                            query=false;
//                        }
                    }
                }
              
               
               
                    
                    
               if (String(displayPuntos[i].weather).length>0 || (displayPuntos[i].instruction!=null && displayPuntos[i].instruction.length>0))
               {
                   // stepTime=displayPuntos[i].acumulativeDistance/ timeFactor;
                   // var stepDate= new Date (startTime.getTime()+stepTime*60*1000);
                    steps+="<tr><td width='5%' >"+ parseInt(displayPuntos[i].acumulativeDistance) + routePoints.Itinerary.DistanceUnit + ".</td>";
                    stepsPrint+="<tr><td width='5%' >"+ parseInt(displayPuntos[i].acumulativeDistance) + routePoints.Itinerary.DistanceUnit + ".</td>";
                    stepsEmail+="<tr><td width='5%' style='font-size:11px;'  >"+ parseInt(displayPuntos[i].acumulativeDistance) + routePoints.Itinerary.DistanceUnit + ".</td>";
                    if (! isNaN(displayPuntos[i].innitialDistance) && (displayPuntos[i].innitialDistance!=null) &&(String(displayPuntos[i].innitialDistance).length>0))
                    {
                        steps+="<td width='5%'>"+ redondear(displayPuntos[i].innitialDistance,2) + routePoints.Itinerary.DistanceUnit + ".</td>";
                        stepsPrint+="<td width='5%'>"+ redondear(displayPuntos[i].innitialDistance,2) + routePoints.Itinerary.DistanceUnit + ".</td>";
                        stepsEmail+="<td width='5%' style='font-size:11px;' >"+ redondear(displayPuntos[i].innitialDistance,2) + routePoints.Itinerary.DistanceUnit + ".</td>";
                     }
                     else
                     {
                        steps+="<td width='5%' style='text-align:center'> - </td>";
                        stepsPrint+="<td width='5%'></td>";
                        stepsEmail+="<td width='5%'></td>";
                     }
                     if (displayPuntos[i].instruction!=null && displayPuntos[i].instruction.length>0)
                     {
                        steps+="<td width='60%'>"+displayPuntos[i].instruction+"</td>";
                        stepsPrint+="<td width='60%'>"+displayPuntos[i].instruction+"</td>";
                        stepsEmail+="<td width='60%' style='font-size:11px;'>"+displayPuntos[i].instruction+"</td>";
                     }
                     else
                     {
                       steps+="<td width='60%' style='text-align:center'> - </td>";
                       stepsPrint+="<td width='60%' ></td>";
                       stepsEmail+="<td width='60%' ></td>";
                     }
                    steps+="<td width='20%'>" + formatHour(stepDate)+ "</td>";
                    stepsPrint+="<td width='20%'>" + formatHour(stepDate)+ "</td>";
                    stepsEmail+="<td width='20%' style='font-size:11px;'>" + formatHour(stepDate)+ "</td>";
                    if(displayPuntos[i].weather!=null && String(displayPuntos[i].weather).length>0 )
                    {
                       //alert(displayPuntos[i].weather); 
                        steps+="<td width='10%'><div id='icon" + displayPuntos[i].weather + "'></td></tr>";
                        stepsPrint+="<td width='10%'><div id='Printicon" + displayPuntos[i].weather + "'></td></tr>";
                        stepsEmail+="<td width='10%' style='font-size:11px;' ><div id='Emailicon" + displayPuntos[i].weather + "'></td></tr>";
                    }
                    else
                    {
                        steps+="<td width='10%' style='text-align:center'>-</td></tr>";
                        stepsPrint+="<td width='10%'></td></tr>";
                        stepsEmail+="<td width='10%'></td></tr>";
                    }
                    
                 
                }
                
               if (query==true)
               {
                 DoAjaxQuery(points,formatDate(stepDate),formatHour(stepDate),ordenPunto,displayPuntos[i].acumulativeDistance,displayPuntos[i].innitialDistance,displayPuntos[i].distance,displayPuntos[i].instruction,idRuta);
                 //alert(displayPuntos[i].instruction + stepDate );
                 
                }
                query=false;
                    
            }//cierra el for
                routeinfo+=steps+"</table><div id='print2'><a onclick='Clickheretoprint()'>Print</a></div><div id='desclaimer'><p>These directions and forecasts are for planning purposes only. " 
                + "Driving directions may not reflect changed traffic patterns, construction or road conditions. Mileage estimates, especially aggregate mileage, may not be identical individual odometer readings. "
                + "Weather forecasts, including the absence of severe weather, are best guesses, not guarantees.</p>"
                + "<br><p id='provided'>Information provided by <a href='http://www.trippish.com'>www.trippish.com</a></p></div>  ";
                
                tableToPrint+=stepsPrint+"</table><div id='print2'></div><div id='desclaimer'><p>These directions and forecasts are for planning purposes only. " 
                + "Driving directions may not reflect changed traffic patterns, construction or road conditions. Mileage estimates, especially aggregate mileage, may not be identical individual odometer readings. "
                + "Weather forecasts, including the absence of severe weather, are best guesses, not guarantees.</p>"
                + "<br><p id='provided'>Information provided by <a href='http://www.trippish.com'>www.trippish.com</a></p></div>  ";
                
                tableToEmail+=stepsEmail+"</table><div id='print2'></div><div id='desclaimer'><p>These directions and forecasts are for planning purposes only. " 
                + "Driving directions may not reflect changed traffic patterns, construction or road conditions. Mileage estimates, especially aggregate mileage, may not be identical individual odometer readings. "
                + "Weather forecasts, including the absence of severe weather, are best guesses, not guarantees.</p>"
                + "<br><p id='provided'>Information provided by <a href='http://www.trippish.com'>www.trippish.com</a></p></div>  ";
                
                el.innerHTML=routeinfo;
               elPrint.innerHTML=tableToPrint;
               elMail.innerHTML=tableToEmail;
              updateIcons();
              rutaCacheada=true;

}

function printPushPins(routePoints)
 {
 
    if(routePoints==null)
    
       {
          
            consultingWeather=false;
       }
    else
    {
         if (!entradaValida())
          {
              /*alert("You have to choose a root first!");*/
              consultingWeather=false;
           }   
           else
           {
             var timeOut;
             ShowLoading('Getting weather info');
             timeOut=window.setTimeout("wait()",100);
             
     } 
     }
//     setmapView();
}
  

function nearPointFound (pasomillas,i,alturaAnterior)
{
    pasomillas=parseInt(pasomillas);
    for (j=i;j>0;j--)
    {
        if ((displayPuntos[j].acumulativeDistance-alturaAnterior)>=(pasomillas-pasomillas*0.10) && (displayPuntos[j].weather!=null && String(displayPuntos[j].weather).length>0))
        {
            return j;
        }
        else
        if ((displayPuntos[j].acumulativeDistance-alturaAnterior)<(pasomillas-pasomillas*0.10))
       
            break;
       
    }
    
    for (j=i;j<displayPuntos.length;j++)
    {
        if (((displayPuntos[j].acumulativeDistance-alturaAnterior)<=(pasomillas+pasomillas*0.10))&& ((displayPuntos[j].acumulativeDistance-alturaAnterior)>=(pasomillas)) && (displayPuntos[j].weather!=null && String(displayPuntos[j].weather).length>0) )
        {
            return j;
        }else
        if ((displayPuntos[j].acumulativeDistance-alturaAnterior)>(pasomillas+pasomillas*0.10))
        
            break;
        
    }
    return 0;
    
}


function hacerDeepSearch(displayPuntos)
{
   
    for (var i=0;i<displayPuntos.length-1;i++)
    {
    
        if (displayPuntos[i].distance>minStep)
        {
            var cantSubSteps=parseInt(displayPuntos[i].distance/minStep);
            var nextPoint=obtenerProxLatLong(displayPuntos[i].latitude,displayPuntos[i].longitude,i);
            var nextAcDistance= obtenerProxAcumulativeDistance(displayPuntos[i].latitude,displayPuntos[i].longitude,i)
            //hacer el primer paso para que sea múltiplo de 5
            var distanciaActual=displayPuntos[i].acumulativeDistance;
            var deltaPoint=minStep-(distanciaActual % minStep);
            subStep(displayPuntos[i].latitude,displayPuntos[i].longitude,nextPoint.lat,nextPoint.lon,deltaPoint,displayPuntos[i].acumulativeDistance);
            for (j=1;j<=cantSubSteps;j++)
            {
                
               if (parseFloat(nextAcDistance)>(parseFloat(distanciaActual)+parseFloat(deltaPoint)+(j*minStep)))
               {
                 subStep(displayPuntos[i].latitude,displayPuntos[i].longitude,nextPoint.lat,nextPoint.lon,(j)*minStep,distanciaActual + deltaPoint);
               }
            }
        }
    }
    displayPuntos.sort(sortF);
    if (!rutaCacheada)
    {
        for (var k=0;k<displayPuntos.length;k++)
        {
           points= displayPuntos[k].latitude + ","+displayPuntos[k].longitude;
           DoAjaxCacheQuery(points,k+1,displayPuntos[k].acumulativeDistance,displayPuntos[k].innitialDistance,displayPuntos[k].distance,displayPuntos[k].instruction,idRuta);
        }
    }
    
}
function obtenerProxAcumulativeDistance(lat,lon,i)
{
    for (var j=i+1;j<displayPuntos.length;j++)
    {
        if (displayPuntos[j].latitude!=lat || displayPuntos[j].longitude!=lon)
        {
            var p=displayPuntos[j].acumulativeDistance; 
            break;
        }
    }
    
    return p; 
}

function obtenerProxLatLong(lat,lon,i)
{
    for (var j=i+1;j<displayPuntos.length;j++)
    {
        if (displayPuntos[j].latitude!=lat || displayPuntos[j].longitude!=lon)
        {
            var p= new LatLong(displayPuntos[j].latitude,displayPuntos[j].longitude); 
            break;
        }
    }
    p.lat=(p.lat*180/Math.PI).toFixed(4);
    
    p.lon=(p.lon*180/Math.PI).toFixed(4);
    return p; 
}

function sortF (a,b)
{
    if (a.acumulativeDistance!=b.acumulativeDistance)
    {
        return (a.acumulativeDistance-b.acumulativeDistance);
    }
    else
    {
        return (a.distance-b.distance);
    }
    
              
}
var intermmediatePoints= new Array();
function subStep(startLat,startLong,endLat,endLong,incrementalDistance,innitialDistance)
{
    var startLat=startLat;
    var startLong=startLong;
    var endLat=endLat;
    var endLong=endLong;
    var vector=LatLong.radToBrng(LatLong.bearing(new LatLong(startLat,startLong ), 
                                                  new LatLong(endLat,endLong)));
    
    var p1 = new LatLong(startLat, startLong); 
    
    var p2 = p1.destPoint(vector,incrementalDistance*1.609344).toString();
    
    var punto=p2.split(",");
    
    var endPoint=new LatLong(punto[0].trim(),punto[1].trim());
    endPoint.lat=(endPoint.lat*180/Math.PI).toFixed(4);
    
    endPoint.lon=(endPoint.lon*180/Math.PI).toFixed(4);
    
    var puntoRuta= createPoint(endPoint.lat,endPoint.lon,'',innitialDistance+incrementalDistance,'','','');    
    displayPuntos.push(puntoRuta);
    startLat=null;
    startLong=null;
    endLat=null;
    endLong=null;
    verctor=null;
    p1=null;
    p2=null;
    punto=null;
    endPoint=null;
    puntoRuta=null;
    
 }   


     
     
 
/*
 * LatLong object - methods summary
 *
 *   p = new LatLong('512839N', '0002741W')
 *   p = new LatLong(53.123, -1.987)
 *
 *   dist = LatLong.distHaversine(p1, p2)
 *   dist = LatLong.distCosineLaw(p1, p2)
 *   dist = LatLong.distVincenty(p1, p2)
 *
 *   brng = LatLong.bearing(p1, p2)
 *   dist = p1.distAlongVector(orig, dirn)
 *   p = LatLong.midPoint(p1, p2)
 *   p2 = p1.destPoint(initBrng, dist)
 *   brng = p.finalBrng(initBrng, dist)
 *
 *   dist = LatLong.distRhumb(p1, p2)
 *   brng = LatLong.brngRhumb(p1, p2)
 *   p2 = p1.destPointRhumb(brng, dist)
 *
 *   rad = LatLong.llToRad('51º28'39"N')
 *   latDms = p.latitude()
 *   lonDms = p.longitude()
 *   dms = LatLong.radToDegMinSec(0.1284563)
 *   dms = LatLong.radToBrng(0.1284563)
 *
 * properties:
 *   p.lat - latitude in radians (0=equator, pi/2=N.pole)
 *   p.lon - longitude in radians (0=Greenwich, E=+ve)
 *
 * © 2002-2005 Chris Veness, www.movable-type.co.uk
 */


/*
 * LatLong constructor:
 *
 *   arguments are in degrees: signed decimal or d-m-s + NSEW as per LatLong.llToRad()
 */
function LatLong(degLat, degLong) {
  this.lat = LatLong.llToRad(degLat);
  this.lon = LatLong.llToRad(degLong);
}


/*
 * Calculate distance (in km) between two points specified by latitude/longitude with Haversine formula
 *
 * from: Haversine formula - R. W. Sinnott, "Virtues of the Haversine",
 *       Sky and Telescope, vol 68, no 2, 1984
 *       http://www.census.gov/cgi-bin/geo/gisfaq?Q5.1
 */
LatLong.distHaversine = function(p1, p2) {
  var R = 6371; // earth's mean radius in km
  var dLat  = p2.lat - p1.lat;
  var dLong = p2.lon - p1.lon;

  var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
          Math.cos(p1.lat) * Math.cos(p2.lat) * Math.sin(dLong/2) * Math.sin(dLong/2);
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  var d = R * c;

  return d;
}


/*
 * Calculate distance (in km) between two points specified by latitude/longitude using law of cosines.
 */
LatLong.distCosineLaw = function(p1, p2) {
  var R = 6371; // earth's mean radius in km
  var d = Math.acos(Math.sin(p1.lat)*Math.sin(p2.lat) +
                    Math.cos(p1.lat)*Math.cos(p2.lat)*Math.cos(p2.lon-p1.lon)) * R;
  return d;
}


/*
 * calculate (initial) bearing (in radians clockwise) between two points
 *
 * from: Ed Williams' Aviation Formulary, http://williams.best.vwh.net/avform.htm#Crs
 */
LatLong.bearing = function(p1, p2) {
  var y = Math.sin(p2.lon-p1.lon) * Math.cos(p2.lat);
  var x = Math.cos(p1.lat)*Math.sin(p2.lat) -
          Math.sin(p1.lat)*Math.cos(p2.lat)*Math.cos(p2.lon-p1.lon);
  return Math.atan2(y, x);
}


/*
 * calculate distance of point along a given vector defined by origin point
 * and direction in radians (uses planar not spherical geometry, so only valid
 * for small distances).
 */
LatLong.prototype.distAlongVector = function(orig, dirn) {
  var dist = LatLong.distHaversine(this, orig);  // distance from orig to point
  var brng = LatLong.bearing(this, orig);        // bearing between orig and point
  return dist * Math.cos(brng-dirn);
}


/*
 * calculate midpoint of great circle line between p1 & p2.
 *   see http://mathforum.org/library/drmath/view/51822.html for derivation
 */
LatLong.midPoint = function(p1, p2) {
  var dLon = p2.lon - p1.lon;

  var Bx = Math.cos(p2.lat) * Math.cos(dLon);
  var By = Math.cos(p2.lat) * Math.sin(dLon);

  lat3 = Math.atan2(Math.sin(p1.lat)+Math.sin(p2.lat),
                    Math.sqrt((Math.cos(p1.lat)+Bx)*(Math.cos(p1.lat)+Bx) + By*By ) );
  lon3 = p1.lon + Math.atan2(By, Math.cos(p1.lat) + Bx);

  if (isNaN(lat3) || isNaN(lon3)) return null;
  return new LatLong(lat3*180/Math.PI, lon3*180/Math.PI);
}


/*
 * calculate destination point given start point, initial bearing and distance
 *   see http://williams.best.vwh.net/avform.htm#LL
 */
LatLong.prototype.destPoint = function(brng, dist) {
  var R = 6371; // earth's mean radius in km
  var p1 = this, p2 = new LatLong(0,0), d = parseFloat(dist)/R;  // d = angular distance covered on earth's surface
  brng = LatLong.degToRad(brng);

  p2.lat = Math.asin( Math.sin(p1.lat)*Math.cos(d) + Math.cos(p1.lat)*Math.sin(d)*Math.cos(brng) );
  p2.lon = p1.lon + Math.atan2(Math.sin(brng)*Math.sin(d)*Math.cos(p1.lat), Math.cos(d)-Math.sin(p1.lat)*Math.sin(p2.lat));

  if (isNaN(p2.lat) || isNaN(p2.lon)) return null;
  return p2;
}


/*
 * calculate final bearing arriving at destination point given start point, initial bearing and distance
 */
LatLong.prototype.finalBrng = function(brng, dist) {
  var p1 = this, p2 = p1.destPoint(brng, dist);
  // get reverse bearing point 2 to point 1 & reverse it by adding 180º
  var h2 = (LatLong.bearing(p2, p1) + Math.PI) % (2*Math.PI);
  return h2;
}


/*
 * calculate distance, bearing, destination point on rhumb line
 *   see http://williams.best.vwh.net/avform.htm#Rhumb
 */
LatLong.distRhumb = function(p1, p2) {
  var R = 6371; // earth's mean radius in km
  var dLat = p2.lat-p1.lat, dLon = Math.abs(p2.lon-p1.lon);
  var dPhi = Math.log(Math.tan(p2.lat/2+Math.PI/4)/Math.tan(p1.lat/2+Math.PI/4));
  var q = dLat/dPhi;
  if (!isFinite(q)) q = Math.cos(p1.lat);
  // if dLon over 180° take shorter rhumb across 180° meridian:
  if (dLon > Math.PI) dLon = 2*Math.PI - dLon;
  var d = Math.sqrt(dLat*dLat + q*q*dLon*dLon); 
  return d * R;
}


LatLong.brngRhumb = function(p1, p2) {
  var dLon = p2.lon-p1.lon;
  var dPhi = Math.log(Math.tan(p2.lat/2+Math.PI/4)/Math.tan(p1.lat/2+Math.PI/4));
  if (Math.abs(dLon) > Math.PI) dLon = dLon>0 ? -(2*Math.PI-dLon) : (2*Math.PI+dLon);
  return Math.atan2(dLon, dPhi);
}


LatLong.prototype.destPointRhumb = function(brng, dist) {
  var R = 6371; // earth's mean radius in km
  var p1 = this, p2 = new LatLong(0,0);
  var d = parseFloat(dist)/R;  // d = angular distance covered on earth's surface
  brng = LatLong.degToRad(brng);

  p2.lat = p1.lat + d*Math.cos(brng);
  var dPhi = Math.log(Math.tan(p2.lat/2+Math.PI/4)/Math.tan(p1.lat/2+Math.PI/4));
  var q = (p2.lat-p1.lat)/dPhi;
  if (!isFinite(q)) q = Math.cos(p1.lat);
  var dLon = d*Math.sin(brng)/q;
  // check for some daft bugger going past the pole
  if (Math.abs(p2.lat) > Math.PI/2) p2.lat = p2.lat>0 ? Math.PI-p2.lat : -Math.PI-p2.lat;
  p2.lon = (p1.lon+dLon+Math.PI)%(2*Math.PI) - Math.PI;
 
  if (isNaN(p2.lat) || isNaN(p2.lon)) return null;
  return p2;
}


/*
 * convert lat/long in degrees to radians, for handling input values
 *
 *   this is very flexible on formats, allowing signed decimal degrees (numeric or text), or
 *   deg-min-sec suffixed by compass direction (NSEW). A variety of separators are accepted 
 *   (eg 3º 37' 09"W) or fixed-width format without separators (eg 0033709W). Seconds and minutes
 *   may be omitted. Minimal validation is done.
 */
LatLong.llToRad = function(llDeg) {
  if (!isNaN(llDeg)) return llDeg * Math.PI / 180;  // signed decimal degrees without NSEW

  llDeg = llDeg.replace(/[\s]*$/,'');               // strip trailing whitespace
  var dir = llDeg.slice(-1).toUpperCase();          // compass dir'n
  if (!/[NSEW]/.test(dir)) return NaN;              // check for correct compass direction
  llDeg = llDeg.slice(0,-1);                        // and lose it off the end
  var dms = llDeg.split(/[\s:,°º′\'″\"]/);          // check for separators indicating d/m/s
  if (dms[dms.length-1] == '') dms.length--;        // trailing separator? (see note below)
  switch (dms.length) {                             // convert to decimal degrees...
    case 3:                                         // interpret 3-part result as d/m/s
      var deg = dms[0]/1 + dms[1]/60 + dms[2]/3600; break;
    case 2:                                         // interpret 2-part result as d/m
      var deg = dms[0]/1 + dms[1]/60; break;
    case 1:                                         // non-separated format dddmmss
      if (/[NS]/.test(dir)) llDeg = '0' + llDeg;    // - normalise N/S to 3-digit degrees
      var deg = llDeg.slice(0,3)/1 + llDeg.slice(3,5)/60 + llDeg.slice(5)/3600; break;
    default: return NaN;
  }
  if (/[WS]/.test(dir)) deg = -deg;                 // take west and south as -ve
  return deg * Math.PI / 180;                       // then convert to radians
}
// note: 'x-'.split(/-/) should give ['x',''] but in IE just gives ['x']


/* 
 * convert degrees to radians - used for bearing, so 360º with no N/S/E/W suffix
 *   can accept d/m/s, d/m, or decimal degrees
 */
LatLong.degToRad = function(brng) {
  var dms = brng.split(/[\s:,º°\'\"′″]/)          // check for separators indicating d/m/s
  switch (dms.length) {                           // convert to decimal degrees...
    case 3:                                       // interpret 3-part result as d/m/s
      var deg = dms[0]/1 + dms[1]/60 + dms[2]/3600; break;
    case 2:                                       // interpret 2-part result as d/m
      var deg = dms[0]/1 + dms[1]/60; break;
    default: 
      var deg = parseFloat(brng); break;          // otherwise decimal degrees
  }
  return deg * Math.PI / 180;                     // then convert to radians
}


/*
 * convert latitude into degrees, minutes, seconds; eg 51º28'38"N
 */
LatLong.prototype.latitude = function() {
  return LatLong._dms(this.lat).slice(1) + (this.lat<0 ? 'S' : 'N');
}


/*
 * convert longitude into degrees, minutes, seconds; eg 000º27'41"W
 */
LatLong.prototype.longitude = function() {
  return LatLong._dms(this.lon) + (this.lon>0 ? 'E' : 'W');
}


/*
 * convert radians to (signed) degrees, minutes, seconds; eg -0.1rad = -000°05'44"
 */
LatLong.radToDegMinSec = function(rad) {
  return (rad<0?'-':'') + LatLong._dms(rad);
}


/*
 * convert radians to compass bearing - 0°-360° rather than +ve/-ve
 */
LatLong.radToBrng = function(rad) {
  return LatLong.radToDegMinSec((rad+2*Math.PI) % (2*Math.PI));
}


/*
 * convert radians to deg/min/sec, with no sign or compass dirn (internal use)
 */
LatLong._dms = function(rad) {
  var d = Math.abs(rad * 180 / Math.PI);
  d += 1/7200;  // add ½ second for rounding
  var deg = Math.floor(d);
  var min = Math.floor((d-deg)*60);
  var sec = Math.floor((d-deg-min/60)*3600);
  // add leading zeros if required
  if (deg<100) deg = '0' + deg; if (deg<10) deg = '0' + deg;
  if (min<10) min = '0' + min;
  if (sec<10) sec = '0' + sec;
  return deg + '\u00B0' + min + '\u2032' + sec + '\u2033';
}


/*
 * override toPrecision method with one which displays trailing zeros in place
 *   of exponential notation
 *
 * (for Haversine, use 4 sf to reflect reasonable indication of accuracy)
 */
Number.prototype.toPrecision = function(fig) {
  var scale = Math.ceil(Math.log(this)*Math.LOG10E);
  var mult = Math.pow(10, fig-scale);
  return Math.round(this*mult)/mult;
}


/*
 * it's good form to include a toString method...
 */
LatLong.prototype.toString = function() {
  return this.latitude() + ', ' + this.longitude();
}
