Title: Measure distance on a map with a scale in C#
Recently I wanted to measure the distance around my local park. 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, use its scale to calibrate, and then measure a distance 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 at Google 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.
private Bitmap Map = null;
// Known units.
enum Units
{
Undefined,
Miles,
Yards,
Feet,
Kilometers,
Meters,
};
// Key map values.
private double ScaleDistanceInUnits = -1;
private double ScaleDistanceInPixels = -1;
private Units CurrentUnit = Units.Miles;
private double CurrentDistance = 0;
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. The program's toolbar holds 3 buttons. The first sets the program's units, the second sets the map's scale, and the third lets you measure a distance.
The units button is a dropdown that lets you select one of the known units. If you pick one of the choices, the following code executes.
// Set the desired units.
private void btnUnit_Click(object sender, EventArgs e)
{
Units old_unit = CurrentUnit;
ToolStripMenuItem menu_item = sender as ToolStripMenuItem;
switch (menu_item.Text)
{
case "Miles":
CurrentUnit = Units.Miles;
break;
case "Yards":
CurrentUnit = Units.Yards;
break;
case "Feet":
CurrentUnit = Units.Feet;
break;
case "Kilometers":
CurrentUnit = Units.Kilometers;
break;
case "Meters":
CurrentUnit = Units.Meters;
break;
}
btnUnits.Text = CurrentUnit.ToString();
// Display the map scale and distance in this unit.
// Find a factor to convert from the old units to meters.
double conversion = 1;
if (old_unit == Units.Feet) conversion = 0.3048;
else if (old_unit == Units.Yards) conversion = 0.9144;
else if (old_unit == Units.Miles) conversion = 1609.344;
else if (old_unit == Units.Kilometers) conversion = 1000;
// Find a factor to convert from meters to the new units.
if (CurrentUnit == Units.Feet) conversion *= 3.28083;
else if (CurrentUnit == Units.Yards) conversion *= 1.09361;
else if (CurrentUnit == Units.Miles) conversion *= 0.000621;
else if (CurrentUnit == Units.Kilometers) conversion *= 0.001;
// Convert and display the values.
ScaleDistanceInUnits *= conversion;
CurrentDistance *= conversion;
btnScale.Text = string.Format("{0} {1}/pixel",
ScaleDistanceInUnits / ScaleDistanceInPixels,
CurrentUnit.ToString());
btnDistance.Text = string.Format("Distance: {0} {1}",
CurrentDistance, CurrentUnit);
}
The code first checks the text on the selected choice and sets the current unit. It then converts the current scale (in units per pixel) and measured distance (in units) to the new unit. To do that, it first converts the previous units into meters and then converts meters into 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 second button to set the map's scale, the following code executes.
// Reset the scale.
private Point StartPoint, EndPoint;
private void btnScale_Click(object sender, EventArgs e)
{
lblInstructions.Text =
"Click and drag from the start and end point of " +
"the map's scale bar.";
picMap.Cursor = Cursors.Cross;
picMap.MouseDown += Scale_MouseDown;
}
private void Scale_MouseDown(object sender, MouseEventArgs e)
{
StartPoint = e.Location;
picMap.MouseDown -= Scale_MouseDown;
picMap.MouseMove += Scale_MouseMove;
picMap.MouseUp += Scale_MouseUp;
}
private void Scale_MouseMove(object sender, MouseEventArgs e)
{
EndPoint = e.Location;
DisplayScaleLine();
}
private void Scale_MouseUp(object sender, MouseEventArgs e)
{
picMap.MouseMove -= Scale_MouseMove;
picMap.MouseUp -= Scale_MouseUp;
picMap.Cursor = Cursors.Default;
lblInstructions.Text = "";
// Get the scale.
ScaleDialog dlg = new ScaleDialog();
if (dlg.ShowDialog() == DialogResult.OK)
{
// Get the distance on the screen.
int dx = EndPoint.X - StartPoint.X;
int dy = EndPoint.Y - StartPoint.Y;
double dist = Math.Sqrt(dx * dx + dy * dy);
if (dist < 1) return;
ScaleDistanceInPixels = dist;
// Parse the distance.
ParseDistanceString(dlg.txtScaleLength.Text,
out ScaleDistanceInUnits, out CurrentUnit);
// Display the units.
btnUnits.Text = CurrentUnit.ToString();
// Display the scale.
btnScale.Text = string.Format("{0} {1}/pixel",
ScaleDistanceInUnits / ScaleDistanceInPixels,
CurrentUnit.ToString());
// Clear the distance.
btnDistance.Text = "Distance <undefined>";
}
}
The button's Click event handler displays some instructions in the form's StatusBar and then registers a MouseDown event handler.
The MouseDown event handler saves the mouse's current location in the StartPoint variable. It then unregisters the MouseDown event handler and registers MouseMove and MouseUp event handlers.
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 unregisters 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 void ParseDistanceString(string txt,
out double distance, out Units unit)
{
txt = txt.Trim();
// Find the longest substring that makes sense as a double.
int i = DoublePrefixLength(txt);
if (i <= 0)
{
distance = -1;
unit = Units.Undefined;
}
else
{
// Get the distance.
distance = double.Parse(txt.Substring(0, i));
// Get the unit.
string unit_string = txt.Substring(i).Trim().ToLower();
if (unit_string.StartsWith("mi")) unit = Units.Miles;
else if (unit_string.StartsWith("y")) unit = Units.Yards;
else if (unit_string.StartsWith("f")) unit = Units.Feet;
else if (unit_string.StartsWith("'")) unit = Units.Feet;
else if (unit_string.StartsWith("k"))
unit = Units.Kilometers;
else if (unit_string.StartsWith("m")) unit = Units.Meters;
else unit = Units.Undefined;
}
}
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 int DoublePrefixLength(string txt)
{
for (int i = 1; i <= txt.Length; i++)
{
string test_string = txt.Substring(0, i);
double test_value;
if (!double.TryParse(test_string, out test_value))
return i - 1;
}
return txt.Length;
}
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 void btnDistance_Click(object sender, EventArgs e)
{
lblInstructions.Text =
"Click and draw to define " +
"the path that you want to measure.";
picMap.Cursor = Cursors.Cross;
DistancePoints = new List<Point>();
picMap.MouseDown += Distance_MouseDown;
}
private List<Point> DistancePoints;
private void Distance_MouseDown(object sender, MouseEventArgs e)
{
DistancePoints.Add(e.Location);
picMap.MouseDown -= Distance_MouseDown;
picMap.MouseMove += Distance_MouseMove;
picMap.MouseUp += Distance_MouseUp;
}
private void Distance_MouseMove(object sender, MouseEventArgs e)
{
DistancePoints.Add(e.Location);
DisplayDistanceCurve();
}
private void Distance_MouseUp(object sender, MouseEventArgs e)
{
picMap.MouseMove -= Distance_MouseMove;
picMap.MouseUp -= Distance_MouseUp;
picMap.Cursor = Cursors.Default;
lblInstructions.Text = "";
// Measure the curve.
double distance = 0;
for (int i = 1; i < DistancePoints.Count; i++)
{
int dx = DistancePoints[i].X - DistancePoints[i - 1].X;
int dy = DistancePoints[i].Y - DistancePoints[i - 1].Y;
distance += Math.Sqrt(dx * dx + dy * dy);
}
// Convert into the proper units.
CurrentDistance =
distance * ScaleDistanceInUnits / ScaleDistanceInPixels;
// Display the result.
btnDistance.Text = string.Format("Distance: {0} {1}",
CurrentDistance, CurrentUnit);
}
When you click the third button, the button's event handler displays some instructions, creates a List<Point>, and registers a MouseDown event handler.
The MouseDown event handler adds the mouse's current location to the point list, removes the MouseDown event handler, and registers 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. That 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 also tried the toolbar interface mostly to see how it would work. It would probably be better to put commands in menus and only display information in the toolbar, or perhaps to remove the toolbar and display all information in the status bar. I'll leave it to you to experiment with it.
Download the example to experiment with it and to see additional details.
|