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.
Related
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);
I'm trying to calculate how many layers a commodity will be stacked in. I have a variable quantity (iQty), a given width for the loadbed (dRTW), a width per unit for the commodity (dWidth) and a quantity per layer (iLayerQty).
The quantity per layer is calculated as iLayerQty = Int(dRTW/dWidth)
Now I need to divide the total quantity by the quantity per layer and round up. In an Excel formula it would be easy, but I'm trying to avoid WorksheetFunction calls to minimise A1/R1C1 confusion. At the moment I'm approximating it with this:
(Number of layers) = ((Int(iQty / iLayerQty) + 1)
And that works fine most of the time - except when the numbers give an integer (a cargo width of 0.5 m, for instance, fitting onto a 2.5 m rolltrailer). In those instances, of course, adding the one ruins the result.
Is there any handy way of tweaking that formula to get a better upward rounding?
I don't see any reason to avoid WorksheetFunction; I don't see any confusion here.
Number_of_layers = WorksheetFunction.RoundUp(iQty / iLayerQty, 0)
You could also roll your own function:
Function RoundUp(ByVal Value As Double)
If Int(Value) = Value Then
RoundUp = Value
Else
RoundUp = Int(Value) + 1
End If
End Function
Call it like this:
Number_of_layers = RoundUp(iQty / iLayerQty)
If using a WorksheetFunction object to access a ROUNDUP or CEILING function is off the table then the same can be accomplished with some maths.
Number of layers = Int(iQty / iLayerQty) - CBool(Int(iQty / iLayerQty) <> Round(iQty / iLayerQty, 14))
A VBA True is the equivalent of (-1) when used mathematically. The VBA Round is there to avoid 15 digit floating point errors.
I use -int(-x) to get the ceiling.
?-int(-1.1) ' get ceil(1.1)
2
?-int(1.1) ' get ceil(-1.1)
-1
?-int(-5) ' get ceil(5)
5
These are the functions I put together for this purpose.
Function RoundUp(ByVal value As Double) as Integer
Dim intVal As Integer
Dim delta As Double
intVal = CInt(value)
delta = intVal - value
If delta < 0 Then
RoundUp = intVal + 1
Else
RoundUp = intVal
End If
End Function
Function RoundDown(ByVal value As Double) as Integer
Dim intVal As Integer
Dim delta As Double
intVal = CInt(value)
delta = intVal - value
If delta <= 0 Then
RoundDown = intVal
ElseIf delta > 0 Then
RoundDown = intVal - 1
End If
End Function
This is my Ceiling in VBA.
Function Ceiling(ByVal Number As Double, ByVal Significance As Double) As Double
Dim intVal As Long
Dim delta As Double
Dim RoundValue As Double
Dim PreReturn As Double
If Significance = 0 Then
RoundValue = 1
Else
RoundValue = 1 / Significance
End If
Number = Number * RoundValue
intVal = CLng(Number)
delta = intVal - Number
If delta < 0 Then
PreReturn = intVal + 1
Else
PreReturn = intVal
End If
Ceiling = PreReturn / RoundValue
End Function
how can I make sure that the results dont remove the last 0 for example, this produces "1.454" instead of "1.4540"
Dim test1 As Decimal = 14540
Debug.Print(TransformDecimal(test1, 1))
Private Shared Function TransformDecimal(value As Decimal, numberOfPlaces As Integer) As Decimal
Dim min = CDec(Math.Pow(10, numberOfPlaces - 1))
Dim max = CDec(Math.Pow(10, numberOfPlaces))
If (value >= max) Then
While value >= max
value /= 10
End While
ElseIf (value < min) Then
While value < min
value *= 10
End While
End If
Return value
End Function
You really must put Option Explicit On.
When you don't and you have, for example, this code Dim x As Decimal = 1.0 you are creating a Double and then converting it to Decimal. With Option Strict On this code won't compile.
When you write this code Dim x As Decimal = 1.0d you are immediately creating a Decimal- and that preserves the decimal places.
So, if you now write your method like this:
Private Shared Function TransformDecimal(value As Decimal, numberOfPlaces As Integer) As Decimal
Dim min = CDec(Math.Pow(10, numberOfPlaces - 1))
Dim max = CDec(Math.Pow(10, numberOfPlaces))
If (value >= max) Then
While value >= max
Dim bits = Decimal.GetBits(value)
bits(3) = ((bits(3) \ 65536) + 1) * 65536
value = New Decimal(bits)
End While
ElseIf (value < min) Then
While value < min
Dim bits = Decimal.GetBits(value)
bits(3) = ((bits(3) \ 65536) - 1) * 65536
value = New Decimal(bits)
End While
End If
Return value
End Function
And call it like this:
Dim test1 As Decimal = 14540d
Debug.Print(TransformDecimal(test1, 1).ToString())
You'll find that you get the result you want:
1.4540
The crux of this code is the lines bits(3) = ((bits(3) \ 65536) + 1) * 65536 & bits(3) = ((bits(3) \ 65536) - 1) * 65536 which shift the exponent to change the decimal by multiples of 10.
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
i was studying some projects and i started The ColorPicker Project. i could not understand the LongToRgb Function >>> which as follow :
**
Private Function LongToRGB(lColor As Long) As String
Dim iRed As Long, iGreen As Long, iBlue As Long
iRed = lColor Mod 256
iGreen = ((lColor And &HFF00) / 256&) Mod 256&
iBlue = (lColor And &HFF0000) / 65536
LongToRGB = Format$(iRed, "000") & ", " & Format$(iGreen, "000") & ", " & Format$(iBlue, "000")
End Function
**
i want someone to explain to me in plain English ...
Mod is the modulo operation, which is ofter referred as % too.
It takes the remainder of the integer division between the two values. In your situation it's useful to get the meaningful part of component color (red, green, blue) from a long that contains all of them packed.
Eg:
1234 Mod 100 = 34
Mod Operator (% in C#)
Basically, it returns the remainder of a division operation. For example, 13 Mod 4 = 1, because 13 / 4 = 3 w/ a remainder of 1. It's important to understand how the long value is created to understand why the function does what it does.
All the colors (Red, Green, Blue) are represented in the amounts of 0-255. For example, imagine the following value: R(8), G(3), B(1).
To understand why the function does what it does, let's look at a scenario where number values range from 0-9 (10 values) instead of 0-255 (256 values). How would you represent a single value that you can reverse engineer the values from? You can not simply add the values together (8 + 3 + 1 = 12) because it would be impossible to reverse engineer the original values. Instead, you must multiply values by the base. The base depends on the value range... in the example it is 10 because there are 10 values. The position is a zero-based index. Red's position is 0, Green's position is 1, Blue's position is 2.
Value * (Base^Position))
Red(8) = (8 * 10^0) = 8 * 1 = 8
Green(3) = (3 * 10^1) = 3 * 10 = 30
Blue(1) = (1 * 10^2) = 1 * 100 = 100
8 + 30 + 100 = 138. And 138 can easily be reverse engineered (in fact, just by looking at it!). Mathematically reverse engineering it is done as so:
(CombinedValue / (Base^Position)) % Base = OriginalValue.
(138 / (10^0)) % 10 = (138 / 1) % 10 = 138 % 10 = 8 (Red)
(138 / (10^1)) % 10 = (138 / 10) % 10 = 13 (decimal is truncated) % 10 = 3 (Green)
(138 / (10^2)) % 10 = (138 / 100) %10 = 1 (decimal is truncated) % 10 = 1 (Blue)
The function does a few things:
It uselessly does a bitwise operator (lColor And &HFF00) and (lColor And &HFF0000) for some reason.
It simplifies the mathematics. There is no point in dividing by 1 for red (256^0 = 1), and there is no point to using the modulo operator to retrieve Green because X % 256 = X for all X where X < 256. Also, 256^2 is equal to 65536.
It uses the actual range that color values can be represented (0-255, 256 values).
You can actually use a simplified version of the function instead:
Private Function LongToRGB(lColor As Long) As String
Dim iRed As Long, iGreen As Long, iBlue As Long
iRed = lColor Mod 256
iGreen = (lColor / 256) Mod 256
iBlue = lColor / 65536
LongToRGB = Format$(iRed, "000") & ", " & Format$(iGreen, "000") & ", " & Format$(iBlue, "000")
End Function
Note that the last method is simply a string formatting function and has nothing to do with the mathematics.