Reputation: 352
If the formula for converting latitude or longitude to double is
((Degree) + (Minute) / 60 + (Second) / 3600) * ((South || West) ? -1 : 1)
then what's the formula for parsing degrees, minutes, seconds from a double?
It'd make sense to have two separate methods for parsing latitude and longitude, but I'm not sure how to parse the degrees, minutes, seconds from the double.
ParseLatitude(double value)
{
//value is South if negative, else is North.
}
ParseLongitude(double value)
{
//value is West if negative, else is East.
}
Example coordinates:
latitude: 43.81234123
longitude: -119.8374747
The final code to convert back and forth, thanks again to Peter and James for the answer. I had to convert value to Decimal because this is being used in Silverlight and Math.Truncate(double) is not available):
public class Coordinate
{
public double Degrees { get; set; }
public double Minutes { get; set; }
public double Seconds { get; set; }
public CoordinatesPosition Position { get; set; }
public Coordinate() { }
public Coordinate(double value, CoordinatesPosition position)
{
//sanity
if (value < 0 && position == CoordinatesPosition.N)
position = CoordinatesPosition.S;
//sanity
if (value < 0 && position == CoordinatesPosition.E)
position = CoordinatesPosition.W;
//sanity
if (value > 0 && position == CoordinatesPosition.S)
position = CoordinatesPosition.N;
//sanity
if (value > 0 && position == CoordinatesPosition.W)
position = CoordinatesPosition.E;
var decimalValue = Convert.ToDecimal(value);
decimalValue = Math.Abs(decimalValue);
var degrees = Decimal.Truncate(decimalValue);
decimalValue = (decimalValue - degrees) * 60;
var minutes = Decimal.Truncate(decimalValue);
var seconds = (decimalValue - minutes) * 60;
Degrees = Convert.ToDouble(degrees);
Minutes = Convert.ToDouble(minutes);
Seconds = Convert.ToDouble(seconds);
Position = position;
}
public Coordinate(double degrees, double minutes, double seconds, CoordinatesPosition position)
{
Degrees = degrees;
Minutes = minutes;
Seconds = seconds;
Position = position;
}
public double ToDouble()
{
var result = (Degrees) + (Minutes) / 60 + (Seconds) / 3600;
return Position == CoordinatesPosition.W || Position == CoordinatesPosition.S ? -result : result;
}
public override string ToString()
{
return Degrees + "º " + Minutes + "' " + Seconds + "'' " + Position;
}
}
public enum CoordinatesPosition
{
N, E, S, W
}
Unit Test (nUnit)
[TestFixture]
public class CoordinateTests
{
[Test]
public void ShouldConvertDoubleToCoordinateAndBackToDouble()
{
const double baseLatitude = 43.81234123;
const double baseLongitude = -119.8374747;
var latCoordN = new Coordinate(baseLatitude, CoordinatesPosition.N);
var latCoordS = new Coordinate(baseLatitude, CoordinatesPosition.S);
var lonCoordE = new Coordinate(baseLongitude, CoordinatesPosition.E);
var lonCoordW = new Coordinate(baseLongitude, CoordinatesPosition.W);
var convertedLatitudeS = latCoordS.ToDouble();
var convertedLatitudeN = latCoordN.ToDouble();
var convertedLongitudeW = lonCoordW.ToDouble();
var convertedLongitudeE = lonCoordE.ToDouble();
Assert.AreEqual(convertedLatitudeS, convertedLatitudeN);
Assert.AreEqual(baseLatitude, convertedLatitudeN);
Assert.AreEqual(convertedLongitudeE, convertedLongitudeW);
Assert.AreEqual(baseLongitude, convertedLongitudeE);
}
}
Upvotes: 8
Views: 18746
Reputation: 43056
ParseLatitude(double Value)
{
var direction = Value < 0 ? Direction.South : Direction.North;
Value = Math.Abs(Value);
var degrees = Math.Truncate(Value);
Value = (Value - degrees) * 60; //not Value = (Value - degrees) / 60;
var minutes = Math.Truncate(Value);
var seconds = (Value - minutes) * 60; //not Value = (Value - degrees) / 60;
//...
}
ParseLongitude(double Value)
{
var direction = Value < 0 ? Direction.West : Direction.East;
Value = Math.Abs(Value);
var degrees = Math.Truncate(Value);
Value = (Value - degrees) * 60; //not Value = (Value - degrees) / 60;
var minutes = Math.Truncate(Value);
var seconds = (Value - minutes) * 60; //not Value = (Value - degrees) / 60;
//...
}
EDIT
I came back to this because of a recent upvote. Here's a DRY-er version, with the Value
parameter renamed to reflect the most common coding convention, in which parameters start with lower-case letters:
ParseLatitude(double value)
{
var direction = value < 0 ? Direction.South : Direction.North;
return ParseLatituteOrLongitude(value, direction);
}
ParseLongitude(double value)
{
var direction = value < 0 ? Direction.West : Direction.East;
return ParseLatituteOrLongitude(value, direction);
}
//This must be a private method because it requires the caller to ensure
//that the direction parameter is correct.
ParseLatitudeOrLongitude(double value, Direction direction)
{
value = Math.Abs(value);
var degrees = Math.Truncate(value);
value = (value - degrees) * 60; //not Value = (Value - degrees) / 60;
var minutes = Math.Truncate(value);
var seconds = (value - minutes) * 60; //not Value = (Value - degrees) / 60;
//...
}
Upvotes: 10
Reputation: 736
With just multiplying you'll get conversion errors without noticing it, I noticed it when mapping the points on a map. You'll need to take into account the slope and other variables, like this:
public static void GeoToMercator(double xIn, double yIn, out double xOut, out double yOut)
{
double xArg = xIn / 100000, yArg = yIn / 100000;
xArg = 6371000.0 * Math.PI / 180 * xArg;
yArg = 6371000.0 * Math.Log(Math.Tan(Math.PI / 4 + Math.PI / 180 * yArg * 0.5));
xOut = xArg / 10000;
yOut = yArg / 10000;
}
I'm guessing you're using Mercator values as double representation. To convert the Mercator value back into the correct longitude/latitude values, just use the reverse:
public static void MercatorToGeo(double xIn, double yIn, out double xOut, out double yOut)
{
double xArg = xIn, yArg = yIn;
xArg = 180 / Math.PI * xArg / 6371000.0;
yArg = 180 / Math.PI * (Math.Atan(Math.Exp(yArg / 6371000.0)) - Math.PI / 4) / 0.5;
xOut = xArg * 10;
yOut = yArg * 10;
}
This did the trick for me.
Upvotes: 0
Reputation: 8823
In addition to parsing out the degree, minutes, seconds (which is just radix 60 arithmetic), you may also want to deal with sign of the doubles being converted to "North/South" for latitude and "East/West" for longitude.
It's pretty standard to identify positive degrees latitude with the Northern Hemisphere and negative degrees latitude with the Southern Hemisphere. It's also common here in the Western Hemisphere to take positive degrees longitude to mean degrees West of the Greenwich Meridian and conversely negative degrees longitude to mean degrees East of that Meridian. However the preferred convention for this is the opposite, to take degrees East of the Greenwich Meridian as negative. You may want to consult with your client/analyze the application design to determine which choice applies to this conversion.
Note also that the discontinuity of longitude at ±180 is a cause for care in converting coordinates that may result from calculations. If the conversion is not intended to handle wrap-around at the 180° meridian, then it's likely an exception should be thrown for such inputs. Of course the design decision should be documented either way.
Certainly latitudes outside the ±90° range are errors on input.
Added: Given the above differences in parsing latitude and longitude, issues that would best be handled in the distinct ParseLatitude & ParseLongitude routines, we could use a common utility to do the conversion from double to degrees/minutes/seconds.
I'm not sure what the target language should be here, so I wrote something in plain vanilla C:
#include <math.h>
void double2DegMinSec(double angle, int *Sign, int *Deg, int *Min, double *Sec)
{ /* extract radix 60 Degrees/Minutes/Seconds from "angle" */
Sign = 1;
if (angle < 0.0) /* reduce to case of nonnegative angle */
{
Sign = -Sign;
angle = -angle;
}
*Deg = floor(angle);
angle -= *Deg;
angle *= 60.0;
*Min = floor(angle);
angle -= *Min;
angle *= 60.0;
*Sec = angle;
return;
}
Likely ParseLatitude and ParseLongitude should manage the conversion of angle's sign to the appropriate geographic designation, but I've included an argument Sign that will allow that sign checking to be done after conversion (though it would be fine if the conversion were only ever called with nonnegative angles).
I made the function double2DegMinSec have a return type of void. Results are thus to be returned through its formal arguments of type pointer to int and pointer to double (in the case of seconds Sec, which might have fractional part).
Calling the conversion in C might be done like this:
double longitude = -119.8374747;
int Sign, Degrees, Minutes;
double Seconds;
double2DegMinSec(longitude, &Sign, &Degrees, &Minutes, &Seconds);
In C++ we would make the calling syntax a bit glibber by using call-by-reference instead of pointers.
Upvotes: 1
Reputation:
#include <math.h>
void ParseLatitude(double Value, bool &north, double °, double &min, double &sec)
{
if ( Value < 0 )
{
ParseLatitude( -Value, north, deg, min, sec );
north = false;
}
else
{
north = true;
deg = floor(Value);
Value = 60*(Value - deg);
min = floor(Value);
Value = 60*(Value - min);
sec = Value;
}
}
// ParseLongitude is similar
Upvotes: 2
Reputation: 17539
I've written a class in C# which does a lot of this. Perhaps it is useful, otherwise you can check out the implementation:
http://code.google.com/p/exif-utils/source/browse/trunk/ExifUtils/ExifUtils/GpsCoordinate.cs
Upvotes: 1