Convert GPS Degrees Minutes Seconds to Decimal Degrees - vb.net

I have a formula that I am using to calculate decimal degrees from gps coordinates.
GPS Coordinates:
3800.5825,N
08735.5417,W
Formula:
Private Function DMStoDD(ByVal toConvert As Double, ByVal Dir As String) As Double
Dim DD As Double
Dim deg As Double
Dim min As Double
Dim sec As Double
deg = CDbl(toConvert.ToString.Substring(0, 2))
min = CDbl(toConvert.ToString.Substring(2, 2))
sec = CDbl(toConvert.ToString.Split(".")(1)) * 0.01
DD = deg + (min / 60) + (sec / 3600)
'Negative for West
If Dir = "W" Then DD = DD * -1
Return DD
End Function
Returns:
38.0161805555556
-87.5983805555556 (negative for west)
The results are very close, but not quite right. I believe that they are just a little bit North West of where they actually should fall. I have searched and looked at a lot of different formulas, but from what I've read, I think that mine should be right. Thanks in advance for the help.

Your formula looks wrong, the part with the seconds! it tries a DMS to Degrees conversion. But you coordinates are in DM notation ( Degrees + Decimal minutes)
so you need a DM to DEG conversion.
08735.5417,W = 87deg 35.5147 minutes W
just do 87 + 35.5147/60, then multiply with -1

Related

Convert Integer Formatted Degrees Decimal Minutes to Decimal Degrees (strange usecase)

I have a radio with GPS APRS that's storing a fixed location as a 4 byte integer and I'm trying to retrieve them back as decimal degrees.
The software for the radio will display the value as a unintuitive decimal. e.g 12° 34.567 is displayed as 12.34567.
Which makes it look like it's in decimal degrees, but it's really degrees decimal minutes.
Ideally I want to read the 4 bytes as an int32 and convert them back to DDM, then to DD and back to int32 to pass to the radio.
I have managed to do just that but there is cases where it doesn't return the correct value.
I've tried dividing the integer based on number of digits. 1234567 / 100000 = 12.34567 or 123456 / 10000 = 12.3456 but that doesn't always work.
Sample code will generate a random set of DD co-ordinates, convert them to the Int32 format the radio uses and then attempts to get the DD co-ordinates back.. which works about 50% of the time.
Using substrings obviously isn't the way to go about it but as mention, I have tried using division and can get the same results as the code below but still fails too often to be reliable. I'm probably overlooking something simple but trying to get a decimal from an integer when it can be 1.0, 10.0 or 100.0 doesn't seem very intuitive :)
//Generate a random set of DD co-ordinates
double Lat_DD = Math.Round(GetRandomNumber(-89.99999, 89.99999), 5); textBox2.Text = Lat_DD.ToString();
double Long_DD = Math.Round(GetRandomNumber(-179.99999, 179.99999), 5); textBox3.Text = Long_DD.ToString();
//Get N_S_E_W direction
string Lat_N_S = (Lat_DD >= 0 ? "N" : "S");
string Long_E_W = (Long_DD >= 0 ? "E" : "W");
//Make negative co-ordinate positive
Lat_DD = Math.Abs(Lat_DD);
Long_DD = Math.Abs(Long_DD);
//Get degrees
int lat_degrees = (int)Math.Truncate(Lat_DD);
int long_degrees = (int)Math.Truncate(Long_DD);
//Get decimal minutes
double lat_decimal_minutes = Math.Truncate(Math.Round((Lat_DD - lat_degrees) * 60.0, 3) * 100.0);
double long_decimal_minutes = Math.Truncate(Math.Round((Long_DD - long_degrees) * 60.0, 3) * 100.0);
//Format degrees + decimal minutes as int32
int lat_ddm = Convert.ToInt32(string.Format("{0}{1}", lat_degrees, lat_decimal_minutes));
int lon_ddm = Convert.ToInt32(string.Format("{0}{1}", long_degrees, long_decimal_minutes));
//Show the result in textboxes
textBox4.Text = lat_ddm + " " + Lat_N_S;
textBox5.Text = lon_ddm + " " + Long_E_W;
//Get decimal minutes from the int32 DDM - probably not the way to do it :)
double latddm = Convert.ToDouble(lat_ddm.ToString().Substring(lat_ddm.ToString().Length - 4, 4));
double londdm = Convert.ToDouble(lon_ddm.ToString().Substring(lon_ddm.ToString().Length - 4, 4));
//Left over values = degrees
double latd = Convert.ToDouble(lat_ddm.ToString().Replace(latddm.ToString(),""));
double lond = Convert.ToDouble(lon_ddm.ToString().Replace(londdm.ToString(), ""));
//Calculate decimal
latddm = Math.Truncate(Math.Round((latddm / 60.0), 3) * 1000.0);
londdm = Math.Truncate(Math.Round((londdm / 60.0), 3) * 1000.0);
//Make degrees negative if S or W
if (Lat_N_S=="S") latd = latd * -1;
if (Long_E_W == "W") lond = lond * -1;
//Show the decimal degrees result
textBox6.Text = string.Format("{0}.{1}", latd, latddm);
textBox7.Text = string.Format("{0}.{1}", lond, londdm);

Calculating distance in kilometers between coordinates

I'm trying to calculate distance in kilometers between two geographical coordinates using the haversine formula.
Code:
Dim dbl_dLat As Double
Dim dbl_dLon As Double
Dim dbl_a As Double
dbl_P = WorksheetFunction.Pi / 180
dbl_dLat = dbl_P * (dbl_Latitude2 - dbl_Latitude1)
dbl_dLon = dbl_P * (dbl_Longitude2 - dbl_Longitude1)
dbl_a = Sin(dbl_dLat / 2) * Sin(dbl_dLat / 2) + Cos(dbl_Latitude1 * dbl_P) * Cos(dbl_Latitude2 * dbl_P) * Sin(dbl_dLon / 2) * Sin(dbl_dLon / 2)
dbl_Distance_KM = 6371 * 2 * WorksheetFunction.Atan2(Sqr(dbl_a), Sqr(1 - dbl_a))
I'm testing with these coordinates:
dbl_Longitude1 = 55.629178
dbl_Longitude2 = 29.846686
dbl_Latitude1 = 37.659466
dbl_Latitude2 = 30.24441
And the code returns 20015.09, which is obviously wrong. It should be 642 km according to Yandex maps.
Where am I wrong? Are the longitude and latitude in wrong format?
As far as I can tell, the issue is that the order of arguments to atan2() varies by language. The following works* for me:
Option Explicit
Public Sub Distance()
Dim dbl_Longitude1 As Double, dbl_Longitude2 As Double, dbl_Latitude1 As Double, dbl_Latitude2 As Double
dbl_Longitude1 = 55.629178
dbl_Longitude2 = 29.846686
dbl_Latitude1 = 37.659466
dbl_Latitude2 = 30.24441
Dim dbl_dLat As Double
Dim dbl_dLon As Double
Dim dbl_a As Double
Dim dbl_P As Double
dbl_P = WorksheetFunction.Pi / 180
dbl_dLat = dbl_P * (dbl_Latitude2 - dbl_Latitude1) 'to radians
dbl_dLon = dbl_P * (dbl_Longitude2 - dbl_Longitude1) 'to radians
dbl_a = Sin(dbl_dLat / 2) * Sin(dbl_dLat / 2) + _
Cos(dbl_Latitude1 * dbl_P) * Cos(dbl_Latitude2 * dbl_P) * Sin(dbl_dLon / 2) * Sin(dbl_dLon / 2)
Dim c As Double
Dim dbl_Distance_KM As Double
c = 2 * WorksheetFunction.Atan2(Sqr(1 - dbl_a), Sqr(dbl_a)) ' *** swapped arguments to Atan2
dbl_Distance_KM = 6371 * c
Debug.Print dbl_Distance_KM
End Sub
*Output: 2507.26205401321, although gcmap.com says the answer is 2512 km. This might be a precision issue --- I think it's close enough to count as working. (Edit it might also be that gcmap uses local earth radii rather than the mean radius; I am not sure.)
Explanation
I found this description of the haversine formula for great-circle distance, which is what you are implementing. The JavaScript implementation on that page gives this computation for c:
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
In JavaScript, atan2() takes parameters y, x. However, in Excel VBA, WorksheetFunction.Atan2 takes parameters x, y. Your original code passed Sqr(dbl_a) as the first parameter, as it would be in JavaScript. However, Sqr(dbl_a) needs to be the second parameter in Excel VBA.
A comment on naming
Building on #JohnColeman's point, there are lots of ways to name variables. In this case, I would recommend using the prefixes for unit rather than for type: e.g., deg_Latitude1, RadPerDeg = Pi/180, and rad_dLat = RadPerDeg * (deg_Latitude2 - deg_Latitude1). I personally think that helps avoid unit-conversion mishaps.
My VBA code that returns the answer in feet; However 'd' is the answer in kilometers.
Imports System.Math
Module Haversine
Public Function GlobalAddressDistance(sLat1 As String, sLon1 As String, sLat2 As String, sLon2 As String) As String
Const R As Integer = 6371
Const cMetersToFeet As Single = 3.2808399
Const cKiloMetersToMeters As Integer = 1000
Dim a As Double = 0, c As Double = 0, d As Double = 0
'Convert strings to numberic double values
Dim dLat1 As Double = Val(sLat1)
Dim dLat2 As Double = Val(sLat2)
Dim dLatDiff As Double = DegreesToRadians(CDbl(sLat2) - CDbl(sLat1))
Dim dLonDiff As Double = DegreesToRadians(CDbl(sLon2) - CDbl(sLon1))
a = Pow(Sin(dLatDiff / 2), 2) + Cos(DegreesToRadians(dLat1)) * Cos(DegreesToRadians(dLat2)) * Pow(Sin(dLonDiff / 2), 2)
c = 2 * Atan2(Sqrt(a), Sqrt(1 - a))
d = R * c
'Convert kilometers to feet
Return Format((d * cKiloMetersToMeters * cMetersToFeet), "0.##").ToString
End Function
Private Function DegreesToRadians(ByVal dDegrees As Double) As Double
Return (dDegrees * PI) / 180
End Function
End Module

run time error '1004' - not able to specify a range of cells

I have spent hours working on the simplest code to just take a person's input of height and weight, convert the two to meters and kilograms respectively, and calculate the person's BMI. I have worked on the conversions and the logic is simple and working but I cannot specify a range of cells and keep getting this error.
Option Explicit
Const KgRate As Double = 0.45359237 'number of kg in one pound
Const PoundsInStone As Integer = 14 'number of pounds in one stone
Const InchesInFeet As Integer = 12 'number of inches in one foot
Const CmsInInch As Double = 2.54 'number of centimetres in an inch
Sub calc_BMI()
'convert height in inches to metres
Range("Sheet1!C7") = ((Range("Sheet1!C4") * InchesInFeet * CmsInInch) + (Range("Sheet1!C5") * CmsInInch)) / 100
'convert stones and pounds to kilograms
Sheets("Sheet1").Range("E10:E") = ((Sheets("Sheet1").Range("C10:C") * PoundsInStone * KgRate) + (Sheets("Sheet1").Range("D10:D") * KgRate))
End Sub
Perhaps:
Option Explicit
Const KgRate As Double = 0.45359237 'number of kg in one pound
Const PoundsInStone As Integer = 14 'number of pounds in one stone
Const InchesInFeet As Integer = 12 'number of inches in one foot
Const CmsInInch As Double = 2.54 'number of centimetres in an inch
Sub calc_BMI()
Dim N As Long, i As Long
With Sheets("Sheet1")
N = .Cells(Rows.Count, "C").End(xlUp).Row
'convert height in inches to metres
.Range("C7") = ((.Range("C4") * InchesInFeet * CmsInInch) + (.Range("C5") * CmsInInch)) / 100
'convert stones and pounds to kilograms
For i = 10 To N
.Range("E" & i) = ((.Range("C" & i) * PoundsInStone * KgRate) + (.Range("D" & i) * KgRate))
Next i
End With
End Sub

Using Goal Seek

I'm taking apart a very old spreadsheet I wrote and trying to put it back together using VBA. So far I've this, which seems to work:
Sub PipeData()
Dim FlowRate As Single
Dim Density As Single
Dim DynamicViscosity As Single
Dim PipeSize As Single
Dim Pi As Single
Dim ReynoldsNumber As Single
Dim Lamda As Single
Dim EquivalentRoughness As Single
Dim RelativeRoughness As Single
Dim Velocity As Single
Dim PressureDrop As Single
Density = 977.8
DynamicViscosity = 0.0004
PipeSize = 36.1
Pi = WorksheetFunction.Pi()
EquivalentRoughness = 0.046
RelativeRoughness = EquivalentRoughness / PipeSize
FlowRate = Cells(2, 7)
ReynoldsNumber = (4 * FlowRate) / (DynamicViscosity * Pi * (PipeSize / 1000))
If ReynoldsNumber < 2000 Then
Lamda = 64 / ReynoldsNumber
Else
Lamda = ((1 / (-1.8 * WorksheetFunction.Log((6.9 / ReynoldsNumber) + ((RelativeRoughness / 3.71) ^ 1.11)))) ^ 2)
End If
Velocity = ((4 * FlowRate) / (Pi * Density * ((PipeSize / 1000) ^ 2)))
PressureDrop = ((Lamda * Density) * (Velocity ^ 2)) / (2 * (PipeSize / 1000))
End Sub
Some of the constants listed here (for example density, pipe size, etc.) I eventually intend to read from a worksheet or automatically calculate but for now I'm proceeding one step at a time.
Now that I'm satisfied that this works, which I've checked by outputting the numbers generated, I want to use Goal Seek to find the flow rate value at a certain pre-defined flow rate.
So what I want to do is have VBA cycle through different flow rate values until the desired pressure drop value is reached. I will tell VBA the desired pressure drop in a cell in the Excel sheet. I want this calculation to exist entirely inside VBA without any worksheet formulas.
So I've got, in very simplified terms, the following:
(1) A starting flow rate (I guess this should be defined in the VBA code otherwise Goal Seek won't have a starting point)
(2) Some calculations
(3) A resulting pressure drop.
(4) If the resulting pressure drop is not equal to a pre-defined value (located in cell G3) the flow rate value in (1) should be adjusted and the calculations run again.
(5) When the resulting pressure drop equals the pre-defined value tell me what the flow rate value used to calculate this is.
Any ideas?
OK, I took a crack at this..there may be a better way and this assumes a direct relationship (not inverse)..i moved some of your variables into constants and put the pressure calc in a function, and changed data types to double. It is a UDF with you can use in the worksheet.
Const Density As Double = 977.8
Const DynamicViscosity As Double = 0.0004
Const PipeSize As Double = 36.1
Const Pi As Double = 3.14159265358979
Const EquivalentRoughness As Double = 0.046
Const RelativeRoughness As Double = EquivalentRoughness / PipeSize
Const Sig As Double = 0.0000000001 'this indicates how accurate you want your answer
Dim FlowRate As Double
Dim ReynoldsNumber As Double
Dim Lamda As Double
Dim Velocity As Double
Function PipeData(IdealPressureDrop As Long)
FlowRate = 1000 + Sig
Stepper = 100
If PressureDrop(FlowRate) > IdealPressureDrop Then
FlowRateGoal = GoalSeek(FlowRate, Stepper, -1, IdealPressureDrop)
Else
FlowRateGoal = GoalSeek(FlowRate, Stepper, 1, IdealPressureDrop)
End If
PipeData = FlowRateGoal
End Function
Function GoalSeek(FlowRate, Stepper, Direction, IdealPressureDrop)
calcagain:
Select Case Direction
Case 1
Do While PressureDrop(FlowRate) < IdealPressureDrop
oFR = FlowRate
FlowRate = FlowRate + Stepper
Loop
Case -1
Do While PressureDrop(FlowRate) > IdealPressureDrop
oFR = FlowRate
FlowRate = FlowRate - Stepper
Loop
End Select
Stepper = Stepper / 10
If Stepper < Sig Then GoTo getout
FlowRate = oFR
GoTo calcagain
getout:
GoalSeek = FlowRate
End Function
Function PressureDrop(FlowRate)
ReynoldsNumber = (4 * FlowRate) / (DynamicViscosity * Pi * (PipeSize / 1000))
If ReynoldsNumber < 2000 Then
Lamda = 64 / ReynoldsNumber
Else
Lamda = ((1 / (-1.8 * WorksheetFunction.Log((6.9 / ReynoldsNumber) + ((RelativeRoughness / 3.71) ^ 1.11)))) ^ 2)
End If
Velocity = ((4 * FlowRate) / (Pi * Density * ((PipeSize / 1000) ^ 2)))
PressureDrop = ((Lamda * Density) * (Velocity ^ 2)) / (2 * (PipeSize / 1000))
End Function
This can now be referenced in the worksheet with
=PipeData(A3)
Where "A3" is your ideal pressure drop number

How do I convert longitude and latitude to GPS Exif byte array

I am trying to put latitude = 8°50'34.46" and longitude = 125° 9'50.82" into the exif file of an image. i'm using vb.net.
I'm not having problem converting the degrees and minutes into bytes because it is a whole number but when i convert the seconds (34.46") which has decimal values into bytes. it gives different result like 0.9856.
Please help me guys how to convert numbers with decimal values into bytes.
here the code:
Private Shared Function intToByteArray(ByVal int As Int32) As Byte()
' a necessary wrapper because of the cast to Int32
Return BitConverter.GetBytes(int)
End Function
Private Shared Function doubleToByteArray(ByVal dbl As Double) As Byte()
Return BitConverter.GetBytes(Convert.ToDecimal(dbl))
End Function
Private Shared Function doubleCoordinateToRationalByteArray(ByVal doubleVal As Double) As Byte()
Dim temp As Double
temp = Math.Abs(doubleVal)
Dim degrees = Math.Truncate(temp)
temp = (temp - degrees) * 60
Dim minutes = Math.Truncate(temp)
temp = (temp - minutes) * 60
Dim seconds = temp
Dim result(24) As Byte
Array.Copy(intToByteArray(degrees), 0, result, 0, 4)
Array.Copy(intToByteArray(1), 0, result, 4, 4)
Array.Copy(intToByteArray(minutes), 0, result, 8, 4)
Array.Copy(intToByteArray(1), 0, result, 12, 4)
Array.Copy(doubleToByteArray(seconds), 0, result, 16, 4)
Array.Copy(intToByteArray(1), 0, result, 20, 4)
Return result
End Function
According to this specification, longitude and latitude are encoded as a
PropertyTagTypeRational
Specifies that the value data member is an array of pairs of unsigned long integers. Each pair represents a fraction; the first integer is the numerator and the second integer is the denominator.
The encoded layout should be (24 bytes total)
Byte Offset Length Encoding Field
0 4 uint Degrees Nominator
4 4 uint Degrees Denominator
8 4 uint Minutes Nominator
12 4 uint Minutes Denominator
16 4 uint Seconds Nominator
20 4 uint Seconds Denominator
Given that your input is using whole degrees and minutes and not fractions, your encoding for those two will work fine, by using the value of 1 as the denominator.
For the seconds, that you have as a floating point value, this is not the case. You will have to encode it as a rational, using a nominator and denominator part.
I am not sure what the precision is that you would like to have, but given your example of 34.46 seconds, it would seem that multiplying by 1000 and using 1000 for the denominator would be more than good enough:
Dim secondsNominator = Math.Truncate(1000 * seconds)
Dim secondsDenoninator = 1000
Then your encoding function becomes:
Private Shared Function doubleCoordinateToRationalByteArray(ByVal doubleVal As Double) As Byte()
Dim temp As Double
temp = Math.Abs(doubleVal)
Dim degrees = Math.Truncate(temp)
temp = (temp - degrees) * 60
Dim minutes = Math.Truncate(temp)
temp = (temp - minutes) * 60
Dim secondsNominator = Math.Truncate(1000 * temp)
Dim secondsDenoninator = 1000
Dim result(24) As Byte
' Degrees (nominator, and 1 for denominator)
Array.Copy(intToByteArray(degrees), 0, result, 0, 4)
Array.Copy(intToByteArray(1), 0, result, 4, 4)
' Minutes (nominator, and 1 for denominator)
Array.Copy(intToByteArray(minutes), 0, result, 8, 4)
Array.Copy(intToByteArray(1), 0, result, 12, 4)
' Seconds (1000 for denominator: ms resolution)
Array.Copy(intToByteArray(secondsNominator), 0, result, 16, 4)
Array.Copy(intToByteArray(secondsDenominator), 0, result, 20, 4)
Return result
End Function
The GPS latitude and longitude for exif data are "rational" data type, or two 32-bit integers. To represent 34.46, for example, you could use the two 32-bit integers 3,446 (numerator) and 100 (denominator), or 344,600 and 10,000. For the integer value of degrees, for example you could use 8 with a denominator of 1.
You can get the exif specification here.