|
|
Title | Measure distances on a map with a scale in Visual Basic .NET |
Description | This example shows how to measure distances on a map with a scale in Visual Basic .NET |
Keywords | algorithms graphics map measure map measure distances map scale example example program Windows Forms programming, Visual Basic .NET, VB.NET |
Categories | Graphics, Algorithms, Graphics |
|
|
Recently I wanted to know how far a lap around my local park was. If you look at Google Maps, you can find maps of just about anywhere with the scale shown on them. This application lets you load such a map, calibrate by using the scale, and then measure distances on the map in various units.
This is a fairly involved example. Most of the pieces are relatively simple but there are a lot of details such as how to parse a distance string such as "1.5 miles."
I wanted to use this program with a map from Google Maps but their terms of use don't allow me to republish their maps so this example comes with a cartoonish map of a park that I drew. (Probably no one would care but there's no need to include one of their maps anyway.) To use a real Google Map, find the area that you want to use and press Alt-PrntScrn to capture a copy of your browser. Paste the result into Paint or some other drawing program and edit the image to create the map you want.
The following code shows variables and types defined by the program.
|
|
' The loaded map image.
Private Map As Bitmap = Nothing
' Known units.
Private Enum Units
Undefined
Miles
Yards
Feet
Kilometers
Meters
End Enum
' Key map values.
Private ScaleDistanceInUnits As Double = -1
Private ScaleDistanceInPixels As Double = -1
Private CurrentUnit As Units = Units.Miles
Private CurrentDistance As Double = -1
|
|
The Units enumeration defines the units of measure that this program can handle.
Use the File menu's Open command to open a map file. You can control the program by using its combo box and two buttons.
The combo box lets you select one of the known units. If you pick one of the choices, the following code executes.
|
|
' Set the desired units.
Private Sub btnUnits_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnYards.Click, _
btnMiles.Click, btnMeters.Click, btnKilometers.Click, _
btnFeet.Click
' Find a factor to convert from the old units to meters.
Dim conversion As Double = 1
If (CurrentUnit = Units.Feet) Then
conversion = 0.3048
ElseIf (CurrentUnit = Units.Yards) Then
conversion = 0.9144
ElseIf (CurrentUnit = Units.Miles) Then
conversion = 1609.344
ElseIf (CurrentUnit = Units.Kilometers) Then
conversion = 1000
End If
Dim menu_item As ToolStripMenuItem = DirectCast(sender, _
ToolStripMenuItem)
Select menu_item.Text
Case "Miles"
CurrentUnit = Units.Miles
Case "Yards"
CurrentUnit = Units.Yards
Case "Feet"
CurrentUnit = Units.Feet
Case "Kilometers"
CurrentUnit = Units.Kilometers
Case "Meters"
CurrentUnit = Units.Meters
End Select
btnUnits.Text = CurrentUnit.ToString()
' Find a factor to convert from meters to the new units.
If (CurrentUnit = Units.Feet) Then
conversion *= 3.28083
ElseIf (CurrentUnit = Units.Yards) Then
conversion *= 1.09361
ElseIf (CurrentUnit = Units.Miles) Then
conversion *= 0.000621
ElseIf (CurrentUnit = Units.Kilometers) Then
conversion *= 0.001
End If
' Convert and display the values.
ScaleDistanceInUnits *= conversion
CurrentDistance *= conversion
DisplayValues()
End Sub
|
|
The code checks the current units and makes a conversion factor to convert from the current unit to meters. It then looks at the new choice and multiplies on a conversion factor to convert from meters to the new units. That avoids the need to have a table giving conversion factors for every pair of old and new units.
When you click the Set Scale button, the following code executes.
|
|
' Reset the scale.
Private StartPoint, EndPoint As Point
Private Sub btnScale_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnScale.Click
lblInstructions.Text = "Click and drag from the start" & _
"and end point of the map's scale bar."
picMap.Cursor = Cursors.Cross
AddHandler picMap.MouseDown, AddressOf Scale_MouseDown
End Sub
Private Sub Scale_MouseDown(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.MouseEventArgs)
StartPoint = e.Location
RemoveHandler picMap.MouseDown, AddressOf _
Scale_MouseDown
AddHandler picMap.MouseMove, AddressOf Scale_MouseMove
AddHandler picMap.MouseUp, AddressOf Scale_Mouseup
End Sub
Private Sub Scale_MouseMove(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.MouseEventArgs)
EndPoint = e.Location
DisplayScaleLine()
End Sub
Private Sub Scale_MouseUp(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.MouseEventArgs)
RemoveHandler picMap.MouseMove, AddressOf _
Scale_MouseMove
RemoveHandler picMap.MouseUp, AddressOf Scale_MouseUp
picMap.Cursor = Cursors.Default
lblInstructions.Text = ""
' Get the scale.
Dim dlg As New ScaleDialog()
If (dlg.ShowDialog() = DialogResult.OK) Then
' Get the distance on the screen.
Dim dx As Integer = EndPoint.X - StartPoint.X
Dim dy As Integer = EndPoint.Y - StartPoint.Y
Dim dist As Double = Math.Sqrt(dx * dx + dy * dy)
If (dist < 1) Then Return
ScaleDistanceInPixels = dist
' Parse the distance.
ParseDistanceString(dlg.txtScaleLength.Text, _
ScaleDistanceInUnits, CurrentUnit)
' Display the units.
btnUnits.Text = CurrentUnit.ToString()
' Display the scale and measured distance.
CurrentDistance = -1
DisplayValues()
End If
End Sub
|
|
The button's Click event handler displays some instructions in the label at the bottom of the form and then installs a MouseDown event handler.
The MouseDown event handler saves the mouse's current location in the StartPoint variable. It then removes the MouseDown event handler and installs MouseMove and MouseUp eventhandlers.
The MouseMove event handler saves the mouse's current position in the EndPoint variable and calls the DisplayScaleLine method. That method simply draws a copy of the map with a red line between StartPoint and EndPoint so you can see where you are drawing.
The MouseUp event handler removes the MouseMove and MouseUp event handlers. It then displays a small dialog where you can enter the distance you selected as in "100 yards" or "1 kilometer." If you enter a value and click OK, the code calculates the length you selected in pixels. It also calls the ParseDistanceString method to determine what distance you entered in the dialog. It finishes by displaying the units you entered and the scale in units per pixel, and by clearing any previous distance.
The following code shows how the ParseDistanceString method parses the scale distance you enter in the dialog.
|
|
' Parse a distance string. Return the length in meters.
Private Sub ParseDistanceString(ByVal txt As String, ByRef _
distance As Double, ByRef unit As Units)
txt = txt.Trim()
' Find the longest substring that makes sense as a
' double.
Dim i As Integer = DoublePrefixLength(txt)
If (i <= 0) Then
distance = -1
unit = Units.Undefined
Else
' Get the distance.
distance = Double.Parse(txt.Substring(0, i))
' Get the unit.
Dim unit_string As String = _
txt.Substring(i).Trim().ToLower()
If (unit_string.StartsWith("mi")) Then
unit = Units.Miles
ElseIf (unit_string.StartsWith("y")) Then
unit = Units.Yards
ElseIf (unit_string.StartsWith("f")) Then
unit = Units.Feet
ElseIf (unit_string.StartsWith("'")) Then
unit = Units.Feet
ElseIf (unit_string.StartsWith("k")) Then
unit = Units.Kilometers
ElseIf (unit_string.StartsWith("m")) Then
unit = Units.Meters
Else
unit = Units.Undefined
End If
End If
End Sub
|
|
This method calls the DoublePrefixLength method to see how many characters at the beginning of the string should be interpreted as part of the number. It extracts those characters to calculate the numeric value. It then examines the beginning of the characters that follow to see what unit you entered. For example, if the following text starts with y, the unit is yards.
The following code shows the DoublePrefixLength method.
|
|
' Return the length of the longest prefix
' string that makes sense as a double.
Private Function DoublePrefixLength(ByVal txt As String) As _
Integer
For i As Integer = 1 To txt.Length
Dim test_string As String = txt.Substring(0, i)
Dim test_value As Double
If (Not Double.TryParse(test_string, test_value)) _
Then Return i - 1
Next i
Return txt.Length
End Function
|
|
This code considers prefixes of the string of increasing lengths until it finds one that it cannot parse as a double. For example, if you enter "100yards," the program can parse the prefixes 1, 10, and 100 but it cannot parse 100y so it concludes that the numeric part of the string contains 3 characters.
The program uses the following code to let you measure a distance on the map.
|
|
' Let the user draw something and calculate its length.
Private Sub btnDistance_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnDistance.Click
lblInstructions.Text = "Click and draw to define the" & _
"path that you want to measure."
picMap.Cursor = Cursors.Cross
DistancePoints = New List(Of Point)()
AddHandler picMap.MouseDown, AddressOf _
Distance_MouseDown
End Sub
Private DistancePoints As List(Of Point)
Private Sub Distance_MouseDown(ByVal sender As _
System.Object, ByVal e As _
System.Windows.Forms.MouseEventArgs)
DistancePoints.Add(e.Location)
RemoveHandler picMap.MouseDown, AddressOf _
Distance_MouseDown
AddHandler picMap.MouseMove, AddressOf _
Distance_MouseMove
AddHandler picMap.MouseUp, AddressOf Distance_MouseUp
End Sub
Private Sub Distance_MouseMove(ByVal sender As _
System.Object, ByVal e As _
System.Windows.Forms.MouseEventArgs)
DistancePoints.Add(e.Location)
DisplayDistanceCurve()
End Sub
Private Sub Distance_MouseUp(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.MouseEventArgs)
RemoveHandler picMap.MouseMove, AddressOf _
Distance_MouseMove
RemoveHandler picMap.MouseUp, AddressOf Distance_MouseUp
picMap.Cursor = Cursors.Default
lblInstructions.Text = ""
' Measure the curve.
Dim distance As Double = 0
For i As Integer = 1 To DistancePoints.Count - 1
Dim dx As Integer = DistancePoints(i).X - _
DistancePoints(i - 1).X
Dim dy As Integer = DistancePoints(i).Y - _
DistancePoints(i - 1).Y
distance += Math.Sqrt(dx * dx + dy * dy)
Next i
' Convert into the proper units.
CurrentDistance = distance * ScaleDistanceInUnits / _
ScaleDistanceInPixels
' Display the result.
DisplayValues()
End Sub
|
|
When you click the Measure button, the button's event handler displays some instructions, creates a List(Of Point), and installs a MouseDown event handler.
The MouseDown event handler adds the mouse's current location to the point list, removes the MouseDown event handler, and installs MouseMove and MouseUp event handlers.
The MouseMove event handler adds the mouse's current location to the point list. It also calls the DisplayDistanceCurve method to show a copy of the map with the distance drawn so far shown in red. Tha method is fairly straightforward so it isn't shown here. Download the example to see the details.
The MouseUp event handler removes the MouseMove and MouseUp event handlers. It then loops through the points and adds up the distances between successive points. It converts the distance from pixels to the currently selected units and displays the results.
I haven't spent too much time on bug proofing this program so I wouldn't be surprised if it shows some odd behavior. I'll leave it to you to experiment with it.
|
|
|
|
|
|
|
|
|