Thursday, 4 October 2007

An Englishman In Google Maps - Plotting Easting/Northing on Google Maps

Plotting Easting/Northing on Google Maps or Virtual Earth


I work for a public sector organisation in the UK and wanted to be able to display various bits of information using Google Maps. (Nothing personal Mr Gates but the first map-related RSS article I read was about how to add Google Maps to your site in 10 minutes - not Virtual Earth.)
Great I thought: we've got the OS Easting/Northing for every local property so putting that on the maps will be easy.

As if.

Anyone who's tried this before will know that it's actually quite tedious and is marginally preferable to drilling your own teeth with a Black & Decker. Personally I wished that I learned more javascript earlier in my career and have advised my kids that they should pay attention at school when sines and cosines are mentioned.

To get a point on Google Maps it seemed I would have to convert the OS Grid Ref to a Latitude/Longitude and plot that on the map. Almost correct except that this would plot an OS Lat/Lon against a WGS84 map, so the OS Lat/Long needs to be converted to a WGS84 Lat/Lon first.

Luckily the conversions were already available (thanks to Chris Veness) on the web but I couldn't locate a VB based version. So I decided to write my own VB.Net stuff to do the conversions so I could do these calculations server-side rather than in Javascript. To do this I converted Chris Veness' scripts which I barely understood. Once I'd written the converter I used it to bulk convert all property easting/northings and save the WGS84 Lat/Long in the databases, meaning that I'd never have to convert them again.

Caveat: I've tested this for various properties within my organisation's local area and it works for me. If it works for you too, great, if not then let me know where the errors are.

I created a .Net class project named GISFunctions.


How To Test The Class Project


    Dim strEasting as String = "462533"
Dim strNorthing as String = "104496"
Dim objEastingNorthing As New GISFunctions.EastingNorthing(strEasting & strNorthing)
' create OS Lat/Long
Dim latlngOS As GISFunctions.LatitudeLongitude
latlngOS = GISFunctions.Conversions.OSGridToLatLong(dr("Xref").ToString & dr("Yref").ToString)
' create WGS84 Lat/Long
Dim latlngWGS84 As GISFunctions.LatitudeLongitude
latlngWGS84 = GISFunctions.Conversions.OSGB36toWGS84(latlngOS)


The Code For The Class Project


Public Class EastingNorthing
Private _easting As String
Private _northing As String
Private _gridSquare As String
Private _gridReference As String
Private _gridReferenceDigitsOnly As String
Public Sub New(ByVal reference As String)
_gridReference = reference
ConvertGridReference(reference)
End Sub

Public ReadOnly Property Easting() As String
Get
Return _easting
End Get
End Property

Public ReadOnly Property Northing() As String
Get
Return _northing
End Get
End Property

Public ReadOnly Property GridSquare() As String
Get
Return _gridSquare
End Get
End Property

Public ReadOnly Property EastingNorthing() As String
Get
Return _easting & _northing
End Get
End Property

Public ReadOnly Property GridReference() As String
Get
Return _gridReference
End Get
End Property

Public ReadOnly Property GridReferenceDigitsOnly() As String
Get
Return _gridReferenceDigitsOnly
End Get
End Property

Private Sub ConvertGridReference(ByVal reference As String)
If IsNumeric(reference.Substring(0, 2)) = False Then
_gridSquare = reference.Substring(0, 2)
_easting = reference.Substring(2, (reference.Length - 2) / 2)
_northing = reference.Substring(2 + ((reference.Length - 2) / 2))
Dim tmpString As String = Conversions.ConvertOSGridLetterToNumber(_gridSquare)
_easting = tmpString.Substring(0, 1) + _easting
_northing = tmpString.Substring(1, 1) + _northing
Else
_easting = reference.Substring(0, reference.Length / 2)
_northing = reference.Substring(reference.Length / 2)
_gridSquare = Conversions.ConvertOSGridNumberToLetter(_easting, _northing)
End If
_gridReferenceDigitsOnly = _easting.ToString & _northing.ToString
_gridReference = _gridSquare & _easting.Substring(1) & _northing.Substring(1)
End Sub
End Class


''' <summary>
''' A class which contains latitude, longitude and height
''' </summary>
''' <remarks></remarks>
Public Class LatitudeLongitude
Private _latitude As Double
Private _longitude As Double
Private _height As Double
Public Sub New(ByVal latitude As Double, ByVal longitude As Double, ByVal height As Double)
_latitude = latitude
_longitude = longitude
_height = height
End Sub

Public Property latitude() As Double
Get
Return _latitude
End Get
Set(ByVal value As Double)
_latitude = value
End Set
End Property

Public Property longitude() As Double
Get
Return _longitude
End Get
Set(ByVal value As Double)
_longitude = value
End Set
End Property

Public Property height() As Double
Get
Return _height
End Get
Set(ByVal value As Double)
_height = value
End Set
End Property
End Class


''' <summary>
''' EllipseParameter contains ellipse definition parameters used for conversion
''' </summary>
''' <remarks></remarks>
Public Class EllipseParameter
Private _semiMajorAxisA As Double
Private _semiMinorAxisB As Double
Private _f As Double
Public Sub New(ByVal EllipseType As String)
Select Case LCase(EllipseType)
Case "wgs84"
_semiMajorAxisA = 6378137
_semiMinorAxisB = 6356752.3142
_f = 1 / 298.257223563
Case "airy1830"
_semiMajorAxisA = 6377563.396
_semiMinorAxisB = 6356256.91
_f = 1 / 299.3249646
Case Else
Throw New Exception("EllipseType should be either WGS84 or Airy1830")
End Select
End Sub

Public ReadOnly Property SemiMajorAxisA() As Double
Get
Return _semiMajorAxisA
End Get
End Property

Public ReadOnly Property SemiMinorAxisB() As Double
Get
Return _semiMinorAxisB
End Get
End Property

Public ReadOnly Property f() As Double
Get
Return _f
End Get
End Property
End Class


''' <summary>
''' ConversionParameter is a class of parameters used when converting from OSGB36 to WGS84 and vice versa
''' </summary>
''' <remarks></remarks>
Public Class ConversionParameter
Private _tx As Double
Private _ty As Double
Private _tz As Double
Private _rx As Double
Private _ry As Double
Private _rz As Double
Private _s As Double
Private _conversionType As String

Public Sub New(ByVal ConversionType As String)
_conversionType = ConversionType
Select Case LCase(ConversionType)
Case "osgb36towgs84"
_tx = 446.448
_ty = -125.157
_tz = 542.06
_rx = 0.1502
_ry = 0.247
_rz = 0.8421
_s = -20.4894
Case "wgs84toosgb36"
_tx = -446.448
_ty = 125.157
_tz = -542.06
_rx = -0.1502
_ry = -0.247
_rz = -0.8421
_s = 20.4894
Case Else
Throw New Exception("ConversionType should be either WGS84toOSGB36 or OSGB36toWGS84")
End Select
End Sub

Public ReadOnly Property ConversionType() As String
Get
Return _conversionType
End Get
End Property

Public ReadOnly Property tx() As Double
Get
Return _tx
End Get
End Property

Public ReadOnly Property ty() As Double
Get
Return _ty
End Get
End Property

Public ReadOnly Property tz() As Double
Get
Return _tz
End Get
End Property

Public ReadOnly Property rx() As Double
Get
Return _rx
End Get
End Property

Public ReadOnly Property ry() As Double
Get
Return _ry
End Get
End Property

Public ReadOnly Property rz() As Double
Get
Return _rz
End Get
End Property

Public ReadOnly Property s() As Double
Get
Return _s
End Get
End Property
End Class


Public Class Conversions
Public Shared Function OSGB36toWGS84(ByVal p1) As LatitudeLongitude
Dim latlngNew = ConvertCoordinates(p1, New EllipseParameter("Airy1830"), New ConversionParameter("OSGB36toWGS84"), _
New EllipseParameter("WGS84"))
Return latlngNew
End Function

Public Shared Function WGS84toOSGB36(ByVal p1) As LatitudeLongitude
Dim latlngNew = ConvertCoordinates(p1, New EllipseParameter("WGS84"), New ConversionParameter("WGS84toOSGB36"), _
New EllipseParameter("Airy1830"))
Return latlngNew
End Function

Private Shared Function ConvertCoordinates(ByVal OriginalLatLong As LatitudeLongitude, ByVal EllipseParamSource As EllipseParameter, _
ByVal ConversionParam As ConversionParameter, ByVal ElliseParamTarget As EllipseParameter)
' convert polar to cartesian coordinates (using source ellipse)
Dim dblOriginalLatitudeInRadians As Double = ConvertDegreesToRadians(OriginalLatLong.latitude)
Dim dblOriginalLongitudeInRadians As Double = ConvertDegreesToRadians(OriginalLatLong.longitude)
Dim dblTempSemiMajorAxis As Double = EllipseParamSource.SemiMajorAxisA
Dim dblTempSemiMinorAxis As Double = EllipseParamSource.SemiMinorAxisB
Dim f As Double = EllipseParamSource.f
Dim dblsinPhi = Math.Sin(dblOriginalLatitudeInRadians)
Dim dblcosPhi = Math.Cos(dblOriginalLatitudeInRadians)
Dim dblsinLambda = Math.Sin(dblOriginalLongitudeInRadians)
Dim dblcosLambda = Math.Cos(dblOriginalLongitudeInRadians)
Dim H As Integer = 1 'p1.height
Dim eSq = (dblTempSemiMajorAxis * dblTempSemiMajorAxis - dblTempSemiMinorAxis * dblTempSemiMinorAxis) / (dblTempSemiMajorAxis * dblTempSemiMajorAxis)
Dim nu = dblTempSemiMajorAxis / Math.Sqrt(1 - eSq * dblsinPhi * dblsinPhi)
Dim x1 = (nu + H) * dblcosPhi * dblcosLambda
Dim y1 = (nu + H) * dblcosPhi * dblsinLambda
Dim z1 = ((1 - eSq) * nu + H) * dblsinPhi
' apply helmert transform using appropriate params
Dim rx As Double = ConversionParam.rx / 3600 * Math.PI / 180 ' normalise seconds to radians
Dim ry As Double = ConversionParam.ry / 3600 * Math.PI / 180
Dim rz As Double = ConversionParam.rz / 3600 * Math.PI / 180
Dim s1 As Double = ConversionParam.s / 1000000.0 + 1 ' normalise ppm to (s+1)
' apply transform
Dim x2 As Double = ConversionParam.tx + x1 * s1 - y1 * rz + z1 * ry
Dim y2 As Double = ConversionParam.ty + x1 * rz + y1 * s1 - z1 * rx
Dim z2 As Double = ConversionParam.tz - x1 * ry + y1 * rx + z1 * s1
' convert cartesian to polar coordinates (using target ellipse)
dblTempSemiMajorAxis = ElliseParamTarget.SemiMajorAxisA
dblTempSemiMinorAxis = ElliseParamTarget.SemiMinorAxisB
Dim precision = 4 / dblTempSemiMajorAxis ' results accurate to around 4 metres
eSq = (dblTempSemiMajorAxis * dblTempSemiMajorAxis - dblTempSemiMinorAxis * dblTempSemiMinorAxis) / _
(dblTempSemiMajorAxis * dblTempSemiMajorAxis)
Dim p = Math.Sqrt(x2 * x2 + y2 * y2)
Dim phi = Math.Atan2(z2, p * (1 - eSq)), phiP = 2 * Math.PI
Do While (Math.Abs(phi - phiP) > precision)
nu = dblTempSemiMajorAxis / Math.Sqrt(1 - eSq * Math.Sin(phi) * Math.Sin(phi))
phiP = phi
phi = Math.Atan2(z2 + eSq * nu * Math.Sin(phi), p)
Loop
Dim lambda = Math.Atan2(y2, x2)
H = p / Math.Cos(phi) - nu
Dim LatLng As New LatitudeLongitude(ConvertRadiansToDegrees(phi), ConvertRadiansToDegrees(lambda), H)
Return LatLng
End Function

''' <summary>
''' convert geodesic co-ordinates to OS grid reference
''' </summary>
''' <param name="OriginalLatLong"></param>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Function LatLongToOSGrid(ByVal OriginalLatLong As LatitudeLongitude)
Dim lat = ConvertDegreesToRadians(OriginalLatLong.latitude)
Dim lon = ConvertDegreesToRadians(OriginalLatLong.longitude)
Dim dblSemiMajorAxisA As Double = 6377563.396
Dim dblSemiMinorAxisB As Double = 6356256.91 ' Airy 1830 major & minor semi-axes
Dim dboScaleFactorF0 As Double = 0.9996012717 ' NatGrid scale factor on central meridian
Dim dblTrueOriginLat0 As Double = ConvertDegreesToRadians(49)
Dim dblTrueOriginLon0 As Double = ConvertDegreesToRadians(-2) ' NatGrid true origin
Dim dblMapCoordTrueOriginN0 As Double = -100000
Dim dblMapCoordTrueOriginE0 As Double = 400000 ' northing & easting of true origin, metres
Dim dblEccentricitySquared As Double = 1 - (dblSemiMinorAxisB * dblSemiMinorAxisB) / (dblSemiMajorAxisA * dblSemiMajorAxisA) ' eccentricity squared
Dim n As Double = (dblSemiMajorAxisA - dblSemiMinorAxisB) / (dblSemiMajorAxisA + dblSemiMinorAxisB), n2 = n * n, n3 = n * n * n
Dim cosLat As Double = Math.Cos(lat), sinLat = Math.Sin(lat)
Dim nu As Double = dblSemiMajorAxisA * dboScaleFactorF0 / Math.Sqrt(1 - dblEccentricitySquared * sinLat * sinLat) ' transverse radius of curvature
Dim rho As Double = dblSemiMajorAxisA * dboScaleFactorF0 * (1 - dblEccentricitySquared) / Math.Pow(1 - dblEccentricitySquared * sinLat * sinLat, 1.5) ' meridional radius of curvature
Dim eta2 As Double = nu / rho - 1
Dim Ma As Double = (1 + n + (5 / 4) * n2 + (5 / 4) * n3) * (lat - dblTrueOriginLat0)
Dim Mb As Double = (3 * n + 3 * n * n + (21 / 8) * n3) * Math.Sin(lat - dblTrueOriginLat0) * Math.Cos(lat + dblTrueOriginLat0)
Dim Mc As Double = ((15 / 8) * n2 + (15 / 8) * n3) * Math.Sin(2 * (lat - dblTrueOriginLat0)) * Math.Cos(2 * (lat + dblTrueOriginLat0))
Dim Md As Double = (35 / 24) * n3 * Math.Sin(3 * (lat - dblTrueOriginLat0)) * Math.Cos(3 * (lat + dblTrueOriginLat0))
Dim M As Double = dblSemiMinorAxisB * dboScaleFactorF0 * (Ma - Mb + Mc - Md) ' meridional arc
Dim cos3lat As Double = cosLat * cosLat * cosLat
Dim cos5lat As Double = cos3lat * cosLat * cosLat
Dim tan2lat As Double = Math.Tan(lat) * Math.Tan(lat)
Dim tan4lat As Double = tan2lat * tan2lat
Dim I As Double = M + dblMapCoordTrueOriginN0
Dim II As Double = (nu / 2) * sinLat * cosLat
Dim III As Double = (nu / 24) * sinLat * cos3lat * (5 - tan2lat + 9 * eta2)
Dim IIIA As Double = (nu / 720) * sinLat * cos5lat * (61 - 58 * tan2lat + tan4lat)
Dim IV As Double = nu * cosLat
Dim V As Double = (nu / 6) * cos3lat * (nu / rho - tan2lat)
Dim VI As Double = (nu / 120) * cos5lat * (5 - 18 * tan2lat + tan4lat + 14 * eta2 - 58 * tan2lat * eta2)
Dim dLon As Double = lon - dblTrueOriginLon0
Dim dLon2 As Double = dLon * dLon
Dim dLon3 As Double = dLon2 * dLon
Dim dLon4 As Double = dLon3 * dLon
Dim dLon5 As Double = dLon4 * dLon
Dim dLon6 As Double = dLon5 * dLon
Dim dblNorthing = I + II * dLon2 + III * dLon4 + IIIA * dLon6
Dim dblEasting = dblMapCoordTrueOriginE0 + IV * dLon + V * dLon3 + VI * dLon5
Return ConvertOSGridNumberToLetter(dblEasting, dblNorthing) ', 8)
End Function

''' <summary>
''' Convert OS Grid Reference to a Latitude/Longitude
''' </summary>
''' <param name="GridReference">An OS Grid Reference in the form TG1234512345 or 612345312345</param>
''' <returns>A LatitudeLongitude class containing the OS Lat/Long</returns>
''' <remarks>For a lat/long that can be used in GoogleMaps or VirtualEarth the returned class needs to be converted using ConvertOSGB36toWGS84</remarks>
Public Shared Function OSGridToLatLong(ByVal GridReference As String) As LatitudeLongitude
' convert grid reference to Easting/Northing
Dim gr As New EastingNorthing(GridReference)
' convert Easting/Northing to Latitude/Longitude
Return OSGridToLatLong(gr.Easting, gr.Northing)
End Function

''' <summary>
''' Convert OS Grid Easting and Northing to Latitude/Longitude
''' </summary>
''' <param name="Easting">An OS Grid Reference Easting in the form nnn or nnnnnn etc.</param>
''' <param name="Northing">An OS Grid Reference Northing in the form nnn or nnnnnn etc.</param>
''' <returns>A LatitudeLongitude class containing the OS Lat/Long</returns>
''' <remarks></remarks>
Public Shared Function OSGridToLatLong(ByVal Easting As Double, ByVal Northing As Double) As LatitudeLongitude
Dim dblEasting As Double = CType((Easting.ToString & "000000").Substring(0, 6), Double)
Dim dblNorthing As Double = CType((Northing.ToString & "000000").Substring(0, 6), Double)
Dim dblSemiMajorAxisA As Double = 6377563.396 ' Airy 1830 major semi-axis
Dim dblSemiMinorAxisB As Double = 6356256.91 ' Airy 1830 minor semi-axis
Dim dboScaleFactorF0 As Double = 0.9996012717 ' NatGrid scale factor on central meridian
Dim dblTrueOriginLat0 As Double = 49 * Math.PI / 180 ' NatGrid true origin latitude
Dim dblTrueOriginLon0 As Double = -2 * Math.PI / 180 ' NatGrid true origin longitude
Dim dblMapCoordTrueOriginN0 As Double = -100000 ' northing of true origin, metres
Dim dblMapCoordTrueOriginE0 As Double = 400000 ' easting of true origin, metres
Dim dblEccentricitySquared As Double = 1 - (dblSemiMinorAxisB * dblSemiMinorAxisB) / (dblSemiMajorAxisA * dblSemiMajorAxisA) ' eccentricity squared
Dim n As Double = (dblSemiMajorAxisA - dblSemiMinorAxisB) / (dblSemiMajorAxisA + dblSemiMinorAxisB)
Dim n2 As Double = n * n
Dim n3 As Double = n * n * n
Dim lat As Double = dblTrueOriginLat0
Dim M As Double = 0
Do
lat = (dblNorthing - dblMapCoordTrueOriginN0 - M) / (dblSemiMajorAxisA * dboScaleFactorF0) + lat
Dim Ma As Double = (1 + n + (5 / 4) * n2 + (5 / 4) * n3) * (lat - dblTrueOriginLat0)
Dim Mb As Double = (3 * n + 3 * n * n + (21 / 8) * n3) * Math.Sin(lat - dblTrueOriginLat0) * Math.Cos(lat + dblTrueOriginLat0)
Dim Mc As Double = ((15 / 8) * n2 + (15 / 8) * n3) * Math.Sin(2 * (lat - dblTrueOriginLat0)) * Math.Cos(2 * (lat + dblTrueOriginLat0))
Dim Md As Double = (35 / 24) * n3 * Math.Sin(3 * (lat - dblTrueOriginLat0)) * Math.Cos(3 * (lat + dblTrueOriginLat0))
M = dblSemiMinorAxisB * dboScaleFactorF0 * (Ma - Mb + Mc - Md) ' meridional arc
Loop While (dblNorthing - dblMapCoordTrueOriginN0 - M >= 0.00001) ' ie until < 0.01mm
Dim sinLat As Double = Math.Sin(lat)
Dim nu As Double = dblSemiMajorAxisA * dboScaleFactorF0 / Math.Sqrt(1 - dblEccentricitySquared * sinLat * sinLat) ' transverse radius of curvature
Dim rho As Double = dblSemiMajorAxisA * dboScaleFactorF0 * (1 - dblEccentricitySquared) / Math.Pow(1 - dblEccentricitySquared * sinLat * sinLat, 1.5) ' meridional radius of curvature
Dim eta2 As Double = nu / rho - 1
Dim tanLat As Double = Math.Tan(lat)
Dim tan2lat As Double = tanLat * tanLat
Dim tan4lat As Double = tan2lat * tan2lat
Dim tan6lat As Double = tan4lat * tan2lat
Dim secLat As Double = 1 / Math.Cos(lat)
Dim VII As Double = tanLat / (2 * rho * nu)
Dim VIII As Double = tanLat / (24 * rho * (nu ^ 3)) * (5 + 3 * (tanLat ^ 2) + eta2 - 9 * (tanLat ^ 2) * eta2)
Dim IX As Double = tanLat / (720 * rho * (nu ^ 5)) * (61 + 90 * (tanLat ^ 2) + 45 * (tanLat ^ 4))
Dim X As Double = secLat / nu
Dim XI As Double = secLat / (6 * (nu ^ 3)) * (nu / rho + 2 * (tanLat ^ 2))
Dim XII As Double = secLat / (120 * (nu ^ 5)) * (5 + 28 * (tanLat ^ 2) + 24 * (tanLat ^ 4))
Dim XIIA As Double = secLat / (5040 * (nu ^ 7)) * (61 + 662 * (tanLat ^ 2) + 1320 * (tanLat ^ 4) + 720 * (tanLat ^ 6))
Dim dE As Double = (dblEasting - dblMapCoordTrueOriginE0)
lat = lat - VII * (dE ^ 2) + VIII * (dE ^ 4) - IX * (dE ^ 6)
Dim lon As Double = dblTrueOriginLon0 + X * dE - XI * (dE ^ 3) + XII * (dE ^ 5) - XIIA * (dE ^ 7)
Return New LatitudeLongitude(ConvertRadiansToDegrees(lat), ConvertRadiansToDegrees(lon), 0)
End Function

''' <summary>Convert standard grid reference ('SU6253304496') to fully numeric ref ([462533,104496])</summary>
''' <param name="GridReference">Ordnance Survey grid reference in the form SUen, e.g. SU123123 or TG1234512345</param>
''' <returns>The converted grid offset digits, e.g. SU123123 returns 41 (4 squares along, 1 grid up)</returns>
Public Shared Function ConvertOSGridLetterToNumber(ByVal GridReference As String) As String
' get numeric values of letter references, mapping A->0, B->1, C->2, etc:
Dim intFirstChar As Integer = Asc(GridReference.ToUpper.Chars(0)) - 65
Dim intSecondChar As Integer = Asc(GridReference.ToUpper.Chars(1)) - 65
' shuffle down letters after 'I' since 'I' is not used in grid:
If (intFirstChar > 7) Then intFirstChar -= 1
If (intSecondChar > 7) Then intSecondChar -= 1
' convert grid letters into 100km-square indexes from false origin (grid square SV):
Dim intEasting As Integer = ((intFirstChar - 2) Mod 5) * 5 + (intSecondChar Mod 5)
Dim intNorthing As Integer = (19 - Math.Floor(intFirstChar / 5) * 5) - Math.Floor(intSecondChar / 5)
Return intEasting.ToString & intNorthing.ToString
End Function

''' <summary>Convert fully numeric ref ([462533104496]) to standard grid reference ('SU6253304496')</summary>
''' <param name="Easting">Ordnance Survey Easting in the form n, e.g. 123 or 12345</param>
''' <param name="Northing">Ordnance Survey Northing in the form n, e.g. 123 or 12345</param>
''' <returns>The grid square equivalent of the input parameters, e.g. 412345 and 1e.g. SU123123 returns 41 (4 squares along, 1 grid up)</returns>
Public Shared Function ConvertOSGridNumberToLetter(ByVal Easting As Integer, ByVal Northing As Integer) As String
' get the 100km-grid indices
Dim e100k As Integer = Math.Floor(Easting / 100000)
Dim n100k As Integer = Math.Floor(Northing / 100000)
If (e100k < 0 OrElse e100k > 6 OrElse n100k < 0 OrElse n100k > 12) Then Return ""
' translate those into numeric equivalents of the grid letters
Dim intFirstChar = (19 - n100k) - (19 - n100k) Mod 5 + Math.Floor((e100k + 10) / 5)
Dim intSecondChar = (19 - n100k) * 5 Mod 25 + e100k Mod 5
' compensate for skipped 'I' and build grid letter-pairs
If (intFirstChar > 7) Then intFirstChar += 1
If (intSecondChar > 7) Then intSecondChar += 1
Return Chr(intFirstChar + 65) + Chr(intSecondChar + 65) ' String.fromCharCode(l1+'A'.charCodeAt(0), l2+'A'.charCodeAt(0))
End Function

''' <summary>
''' convert degrees to radians
''' </summary>
''' <param name="Degrees">Value to be converted</param>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Function ConvertDegreesToRadians(ByVal Degrees As Double) As Double
Return Degrees * Math.PI / 180
End Function

''' <summary>
''' convert radians to degrees (signed)
''' </summary>
''' <param name="Radians">Value to be converted</param>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Function ConvertRadiansToDegrees(ByVal Radians As Double) As Double
Return Radians * 180 / Math.PI
End Function

''' <summary>
''' Return co-ordinate in degrees, minutes and seconds from a decimal value
''' </summary>
''' <param name="NumericDegrees">Numeric degrees</param>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Function ConvertNumericDegreesToDegreesMinutesSeconds(ByVal NumericDegrees As Double) As String
Dim dblTemp As Double = Math.Abs(NumericDegrees)
dblTemp += 1 / 720000000 ' add .005 millisecond for rounding
Dim deg As Double = Math.Floor(dblTemp)
Dim min As Double = Math.Floor((dblTemp - deg) * 60)
Dim sec As Double = Math.Floor((dblTemp - deg - min / 60) * 3600)
Return deg.ToString("000") + Chr("176") + min.ToString("00") + "'" + sec.ToString("0") + """"
End Function

''' <summary>
''' convert numeric degrees to deg/min/sec longitude
''' </summary>
''' <param name="NumericDegrees">Numeric degrees</param>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Function ConvertNumericDegreesToLatitude(ByVal NumericDegrees As Double) As String
Dim strTemp As String = ""
strTemp = ConvertNumericDegreesToDegreesMinutesSeconds(NumericDegrees).Substring(1)
If NumericDegrees < 0 Then strTemp &= "S" Else strTemp &= "N"
Return strTemp
End Function

''' <summary>
''' convert numeric degrees to deg/min/sec longitude
''' </summary>
''' <param name="NumericDegrees">Numeric degrees</param>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Function ConvertNumericDegreesToLongitude(ByVal NumericDegrees As Double) As Double
Dim strTemp As String = ""
strTemp = ConvertNumericDegreesToDegreesMinutesSeconds(NumericDegrees)
If NumericDegrees < 0 Then strTemp &= "W" Else strTemp &= "E"
Return strTemp
End Function
End Class

31 comments:

Tony Bullock said...

Kev,

This is going to be really useful to my organisation too. Many thanks for posting it. I'll let you know how we get on.

Cheers,

Tony

Ben Hiron-Grimes said...

Hi Kev
thanks for the post but I'm a bit lost with the variable names xref and yref ??
guess i'm being stupid.. please advise if you get this thanks :-)

Anonymous said...

After getting more than 10000 visitors/day to my website I thought your kwinchcombe.blogspot.com website also need unstoppable flow of traffic...

Use this BRAND NEW software and get all the traffic for your website you will ever need ...

= = > > http://get-massive-autopilot-traffic.com

In testing phase it generated 867,981 visitors and $540,340.

Then another $86,299.13 in 90 days to be exact. That's $958.88 a
day!!

And all it took was 10 minutes to set up and run.

But how does it work??

You just configure the system, click the mouse button a few
times, activate the software, copy and paste a few links and
you're done!!

Click the link BELOW as you're about to witness a software that
could be a MAJOR turning point to your success.

= = > > http://get-massive-autopilot-traffic.com

Anonymous said...

Today there is still plenty of activity in all the area mines.
It also happens sometimes that your character will say something silly.
Thanks for reading this article about Oregon gem mining on Associatedcontent.

Anonymous said...

Located near Statesville North Carolina in the town of
Hiddenite, which is named after the gem found at the mine.
If you have lower defence you can survive as well, but it won't be as easy. Thanks for reading this article about Oregon gem mining on Associatedcontent.

Anonymous said...

Nanokeratin locks onto the hair, forming a fine, smooth coat of keratin.
As natural products don't have any side-effects, you can rest assured for healthy hair. Generally, these products are more expensive than their watered-down counterparts, but you can find a few affordable pure silicone hair products (see below).

Anonymous said...

The city of Danbury's metro options into NYC, give anyone in New Milford, Newtown, Bethel, New Fairfield and Sherman options for travelling south. First, ready mixed concrete in Sunderland can help you get your construction job done faster. For faster setting concrete, use less water, for more workability, use more water.

Anonymous said...

Pandora is built around algorithms that will
customize the stations based on input from
the user and will match other songs based on melody, tone,
and other aspects of the music selected. In addition
to music, they broadcast preaching and teaching from Jimmy Swaggert and other pastors and teachers who work with the ministry.

So you can imagine how much your choice has increased from before.


Check out my page https://vle.um.edu.mt/mvu/user/view.php?id=65724&course =1

Anonymous said...

The developers are saying that Defiance is a pixel
perfect shooter, so if you aim at your target's head it WILL be a headshot instead of the invisible dice rolling a 5 and telling you that you somehow missed. However, every character in the game (even supporting players) should be presented in this same detail. In this addictive puzzle game, catch that damn fly and avoid those tricky obstacles.

Take a look at my homepage - excelsior college

Anonymous said...

The developers are saying that Defiance is a pixel perfect shooter, so if you aim at your target's head it WILL be a headshot instead of the invisible dice rolling a 5 and telling you that you somehow missed. The only downside of it in a lot of people's eyes is
the fact that there is no multiplayer content.
If you want to be able to hit the ball further and harder,
you should keep in mind to keep your grip on the bat loose, your swing
should begin with your legs and hips, and finally, you must always
follow your bat through.

Feel free to surf to my site: http://www.spielespielen24.de/r-23155

Anonymous said...

You don't have to hit the gym for two and three hours each day to lose weight, but it does help to squeeze in 30 minutes of physical activity each day. Withdrawal from levothyroxine can be done but it takes 6 weeks of withdrawal for the remaining thyroid tissue to be completely starved. Many people don't have the time to weight themselves
every day, but checking the scale on a regular basis can definitely help when you're working to lose weight and keep it off.

Feel free to surf to my website :: Recommended Site

Anonymous said...

"Worked All Zones Award" is the same concept with
time zones. They also apparently believe that their customers
who like a particular song they hear on the radio,
are likely to purchase that song, which could add to downloads
from i - Tunes. What s more is that 2G phones can come in a tinier
and slimmer package, even its batteries.

Here is my web site: radio merseyside presenters

Anonymous said...

"Worked All Zones Award" is the same concept with time zones.
They also apparently believe that their customers
who like a particular song they hear on the radio, are likely to purchase that
song, which could add to downloads from i - Tunes.
Many people will be happy with replaceable batteries for
home use and occasional outings.

Look at my site: Xm radio boombox Power cord

Anonymous said...

Instead of saying buy my product, ask for feedback.
It allows you in one page to see your search, your Twitter followers, allows you to
shortens URLs and schedule tweets in advance.
Another thing you'll want to avoid is relying on Twitter as your sole source of marketing.

Here is my blog ... Thoughts on Secrets For twitter

Anonymous said...

But with Nexus Radio the only settings you have to worry about are where you want to
save your files and what file type you want to save it as.
Hardware mechanisms used in the manufacturing of a Wi
- Fi internet radio system is less complicated and the point
of ergonomics is kept in mind by the manufactures. Perfect
for long trips and for up to date information on road conditions ahead.


Also visit my weblog - radio streams

Anonymous said...

So now you can listen to any music from any where non-stop and
cost free. "It's hard to believe, " says Randy Gilbert, host of “The Inside Success Secrets Radio Show"-an Internet radio broadcast which has been “airing" for several years.
Raima has options to limit individual file sizes in the preferences dialog.


Feel free to visit my webpage :: internetradio aussetzer

Anonymous said...

Broadcast satellite "in the Star on the 9th," was successfully launched in June last year, it can be said is a milestone in the field of
live satellite event. Next, build a list of prospects and develop a
relationship with those prospects on your list. What s more is that 2G phones can come in a tinier and slimmer package, even its batteries.


My website :: radio guide app

Anonymous said...

She loves to share hers positive and negative experiences,
and staying at , booked through chilloutbali. Palma has a range of top-notch
clubs, and Tito’s is one of the most popular. We are literally
not the same person we were a minute ago, let alone a day, a month or a year ago.


My homepage http://www.nuffieldtheatre.co.uk/member/38223/

Anonymous said...

You don't have to hit the gym for two and three hours each day to lose weight, but it does help to squeeze in 30 minutes of physical activity each day. It’s old news that tracking food intake could lead to losing a few pounds [2]. Who does not need that little bit of elevation when trying to diet.

Here is my web-site; Click The Following Internet Site

Anonymous said...

Enjoying the service of Odyssey - Streaming -
Radio is so easy. And once you have your own project that you want to promote, you must have considered the
radio as one of the best alternatives to introduce your music to the crowds.

Many people will be happy with replaceable batteries for home use
and occasional outings.

Here is my homepage ... www.cgm.ru

Anonymous said...

Upon examining some etymological dictionaries, one
can conclude that games are a creative expression of the human spirit
through the creation of an activity that has an entertaining,
instructive and competing element. Finding
good outsourcing companies is the key, as naturally you do
not want to lower the quality of the service. Well, you can have that same chess engine on your Android mobile phone, courtesy of Droid -
Fish.

my homepage; mouse click the following webpage

Anonymous said...

It bears mentioning that there are also games and simulations available to those wanting to learn to trade
stock index futures. All spaceship game leveling systems are designed slightly differently,
but there are some general concepts that apply to
all games in this genre. If you want to be able
to hit the ball further and harder, you should keep in mind to keep
your grip on the bat loose, your swing should begin with your legs and hips, and finally, you must
always follow your bat through.

my page - Click at plumbers-talk.com

Anonymous said...

Let's have a look at few of the things which will help you. 1MP resolution and you can be sure of high quality photos with it. There are more important things to consider when deciding if you will pay more for a camera.

My weblog; Recommended Site

Anonymous said...

With internet radio, you get pretty much what you want,
when you want. According to online music provider Pandora the legislation
will help end the discrimination against internet radio.
Organizing employees, maintenance of radio station as well as layout and other details
are very simple and therefore creating a radio station online is a lucrative deal for those who
want their own stations.

my site - http://www.dhes.cyc.edu.tw/userinfo.php?uid=1625

Anonymous said...

This helps business managers to determine what the general public thinks about their products or services.
I think what corporate America has forgotten is that the airwaves are public.
Back in the day, people had little choice but to take what was given to them as their "lot in life.

Feel free to visit my blog: Get More Information

Anonymous said...

Scroll down to the music section and click on the edit link.

The table top radio connects to the internet using Wi-Fi or Ethernet cable, and searches for stations by country, genre or call letters.
You will also need to be sure that you have enough bandwidth from your web host to run the programming smoothly.


my webpage ... Profile

Anonymous said...

While in the past you might have been restricted to
a rather blocky rendition of the content you were looking for, the modern You - Tube affords
you the option of changing from the usual default 240p or 320p up to much
higher quality, even displaying video clips in HD. The latest movies are located there and more
movies are added daily to ensure the best action,
drama, comedy, sci-fi movie range available anywhere online.
As to how many were actually persuaded to buy i - Phone because of You
- Tube marketing, you can search them yourself in this incredible video sharing website.


Here is my page :: Www.Weibgram.Com

Anonymous said...

Though the levels look really simple, they are actually quite
challenging. Leveling up to defeat all the enemies can be time arresting so apprehend to absorb a acceptable bulk
of hours anniversary day accepting the a lot of out of this game.
This classic game integrates all-out entertainment
with vocabulary enhancement in one amazing game.


Here is my blog born shoes - www.spielespielen24.de

Anonymous said...

The pc 2007 elite TV software is therefore legitimate software with over 3000
TV channels. There are hundreds of people working in foreign aid missions, expats, Peace Corps, and
others in relief and business sectors. Organizing employees, maintenance of radio station as well as layout and
other details are very simple and therefore creating a radio station online is a lucrative
deal for those who want their own stations.

My page; http://www.moda.net.pl/fryzury/rude-wlosy/attachment/5-3/comment-page-70

Anonymous said...

This is a small selection of great music I listen to, written from
my own perspective as a fan and a user, with the aim of inspiring others to listen too.
Compassion also arises out of a sense of vulnerability and shared humanity'the realization that we are all connected to everyone and everything at all times, that we are not isolated or separate. Quicker to produce and available in lower costs, they often feature a lot of complex designs.

Also visit my web-site; chillout radio

Anonymous said...

Carrots (or Daucus carota) are among the list of vegetables that are easy to grow, tasty, high in nutrition,
and can be easily added to our diet. Both protein and fiber
will help you feel satisfied or full longer. Let's say that this should refer to metabolism that drives the mobilization of stored fat.

Visit my web-site ... click through the next web site