Home
Search
 
What's New
Index
Books
Links
Q & A
Newsletter
Banners
 
Feedback
Tip Jar
 
C# Helper...
 
XML RSS Feed
Follow VBHelper on Twitter Follow VBHelper on Twitter
 
 
 
MSDN Visual Basic Community
 
 
 
 
 
TitleMeasure distances on a map with a scale in Visual Basic .NET
DescriptionThis example shows how to measure distances on a map with a scale in Visual Basic .NET
Keywordsalgorithms graphics map measure map measure distances map scale example example program Windows Forms programming, Visual Basic .NET, VB.NET
CategoriesGraphics, 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.

 
 
 
 
Copyright © 1997-2010 Rocky Mountain Computer Consulting, Inc.   All rights reserved.
  Updated