Introduction
This brief article describes how to transform an NMEA-string (i.e. from a
a GPS-device) to the Swedish National Grid (RT90).
I will not go through the SerialPort communication or NMEA interpretation issues
as they are well described by Jon Person in:
- Writing Your Own GPS Applications: Part 1.
- Writing Your Own GPS Applications: Part 2.
and by Alex@UEA:
- GPS- Deriving British Ordnance Survey Grid Reference from NMEA data
Background
As I was writing my Physics application at work (which included communicating with a GPS-device) I came across the articles listed above.
The NMEA-Interpreter-article by Jon Person was especially helpful (thanks Jon), but naturally none of them covered how to transform
the NMEA-string (based upon the WGS 84 Ellipsoid) to the Swedish National Grid (Rikets Koordinatsystem 1990 or just RT90) which
seems to be used by most maps in Sweden. I also found a lot of helpful information at Lantm�teriet,
where projection/transform formulas were provided. All that was needed was to implement the transform, preferably in C#. So here it is.
Using the code
The code is simply a single class which reads NMEA strings, such as those given by Jon Person's NMEA-Interpreter, and transforms
them into RT90 Cartesian coordinates (x, y). I also included support for 6 different regions in Sweden, ranging from Lule� in the East to
G�teborg in the West, as recommended by Lantm�teriet. The regions are called 5 gon O, 2.5 gon O, 0 gon V, 2.5 gon V, 5 gon V and 7.5 gon V.
Major (in a Swedish sense) cities are from East to West: Lule�, Ume�, Stockholm, �rebro, Malm� and G�teborg.
(Note:The 'V' and 'O' is used instead of 'W' and 'E' since the Swedish words for west/east starts with a 'V'/'O'...)
The Class is called GaussKrugerProjection
as it is the conformal projection method in use, i.e.
a "Projection which preserves the original shape of the area of interest but not the area or distance".
It is simply used as follows:
GaussKrugerProjection myTransformInStockholm = new GaussKrugerProjection("0V");
or if you're around G�teborg (Gothenburg):
GaussKrugerProjection myTransformInGothenburg = new
GaussKrugerProjection("7.5V");
If we have received an NMEA string from, for example, the NMEA-Interpreter mentioned above, we just send it to the projection
using the GetRT90
method in the GaussKrugerProjection
class:
public int[] GetRT90(string lat, string lon)
...using the created projection instance above and assuming we have a NMEAInterpreter:
private void NMEAInterpreter_PositionReceived(string lat, string lon)
{
int[] XandY = myTransformInStockholm.GetRT90(lat, lon);
int x = XandY[0];
int y = XandY[1];
Console.WriteLine("x = {0}, y = {1}", x.ToString(), y.ToString());
}
The Class is written for NMEA-strings in the format
string lat = "20�18.2274\"E";
Where 20 is hours, 18 minutes, 22.74 seconds and E represents the Hemisphere.
Make sure your NMEA string is formatted in the same way.
The GaussKrugerProjection
class is basically divided into two parts:
- A NMEA-string to radians Parser (smilar to that in article (3) above)
- A Projection Calculator.
The parser looks like this:
private double GetLatLong(string LatorLong, bool isLong)
{
double deciLatLon = Convert.ToDouble(
LatorLong.Substring(0, LatorLong.IndexOf("�"))
);
LatorLong = LatorLong.Substring(LatorLong.IndexOf("�") + 1);
deciLatLon +=
(Convert.ToDouble(LatorLong.Substring(
0, LatorLong.IndexOf(".")), enUSCultureInfo)
) / 60.0;
LatorLong = LatorLong.Substring(LatorLong.IndexOf(".") + 1);
string sec = LatorLong.Substring(0, LatorLong.IndexOf("\""));
deciLatLon +=
(
Convert.ToDouble(sec.Insert(2, "."), enUSCultureInfo)
) / 3600.0;
LatorLong = LatorLong.Substring(
LatorLong.IndexOf("\"") + 1);
if (isLong && LatorLong == "S" ||
!isLong && LatorLong == "W")
{
deciLatLon = -deciLatLon;
}
return deciLatLon * (Math.PI / 180.0);
}
The projection calculator can be divided into two parts:
- Declaring variables / pre-projection calculation and
- Performing projection.
First thing's first: here are the variable declarations:
#region Fields
private static double a = 6378137.0;
private static double f = (1.0 / 298.2572221010);
private static string CM_0V = "18�03.2268\"E";
private static double k0_0V = 1.000005400000;
private static double FN_0V = -668.844;
private static double FE_0V = 1500083.521;
private string CM;
private double k0;
private double FN;
private double FE;
private double lat;
private double lon;
private double A, B, C, D, Beta1, Beta2, Beta3, Beta4,
e2, n, aHat;
private double x;
private double y;
private static CultureInfo enUSCultureInfo =
new CultureInfo("en-US");
#endregion
There exists similar Fields
for all 6 different gons. These are then used by the Constructor as:
public GaussKrugerProjection()
{
CM = CM_25V;
k0 = k0_25V;
FN = FN_25V;
FE = FE_25V;
this.Initialize();
}
private void Initialize()
{
e2 = f * (2.0 - f);
n = f / (2.0 - f);
aHat = (a / (1.0 + n)) * (1.0 +
(0.25 * Math.Pow(n, 2)) +
((1.0 / 64.0) * Math.Pow(n, 4)));
A = e2;
B = (1.0 / 6.0) * (5.0 * Math.Pow(A, 2) -
6.0 * Math.Pow(A, 3));
C = (1.0 / 120.0) * (104.0 * Math.Pow(A, 3) -
45.0 * Math.Pow(A, 4));
D = (1.0 / 1260.0) * (1237.0 * Math.Pow(A, 4));
Beta1 = (0.5 * n) - ((2.0 / 3.0) * Math.Pow(n, 2)) +
((5.0 / 16.0) * Math.Pow(n, 3)) +
((41.0 / 180.0) * Math.Pow(n, 4));
Beta2 = ((13.0 / 48.0) * Math.Pow(n, 2)) -
((3.0 / 5.0) * Math.Pow(n, 3)) +
((557.0 / 1440.0) * Math.Pow(n, 4));
Beta3 = ((61.0 / 240.0) * Math.Pow(n, 3)) -
((103.0 / 140.0) * Math.Pow(n, 4));
Beta4 = ((49561.0 / 161280.0) * Math.Pow(n, 4));
}
And finally the actual projection:
private void CalcGaussKrugerProjection(double lat, double lon)
{
double phiStar = lat - (Math.Sin(lat) * Math.Cos(lat) * (
A +
B * Math.Pow(Math.Sin(lat), 2) +
C * Math.Pow(Math.Sin(lat), 4) +
D * Math.Pow(Math.Sin(lat), 6)));
double dLon = lon - GetLatLong(CM, true);
double chi = Math.Atan(Math.Tan(phiStar) / Math.Cos(dLon));
double z = Math.Cos(phiStar) * Math.Sin(dLon);
double eta = 0.5 * Math.Log((1.0 + z) / (1.0 - z));
x = k0 * aHat * (chi +
Beta1 * Math.Sin(2.0 * chi) * Math.Cosh(2.0 * eta) +
Beta2 * Math.Sin(4.0 * chi) * Math.Cosh(4.0 * eta) +
Beta3 * Math.Sin(6.0 * chi) * Math.Cosh(6.0 * eta) +
Beta4 * Math.Sin(8.0 * chi) * Math.Cosh(8.0 * eta)) +
FN;
y = k0 * aHat * (eta +
Beta1 * Math.Cos(2.0 * chi) * Math.Sinh(2.0 * eta) +
Beta2 * Math.Cos(4.0 * chi) * Math.Sinh(4.0 * eta) +
Beta3 * Math.Cos(6.0 * chi) * Math.Sinh(6.0 * eta) +
Beta4 * Math.Cos(8.0 * chi) * Math.Sinh(8.0 * eta)) +
FE;
}
If you're interested in the Mathematical details, see:
this page or
this PDF for more information (the first is in Swedish).
History
This is the first version.
- Cheers!