DataQuery Tutorial |
The DataQuery object allows you to locate features that are at or near a specific location. It provides a number of static methods that let you obtain information about features of a specific type. These methods fall into two basic categories: queries that locate features near a specified location and queries that locate features within a specific region (bounding box). For an overview of the DataQuery object, see Data Query Concept.
In this tutorial, we will create an application that allows the user to select a type of information to query from a set of list boxes, and to select a location or region on the map using the mouse. The results of the query will then be displayed on the map using BalloonPushPins for each feature located near the specified location or in the specified region.
This tutorial assumes you are familiar with setting up a simple GeoBase application. If you have never created a GeoBase application before, you may want to read through the following topics first:
Control (Type) | Name | Properties to set | ||
Form | Form1 | Set Text to Data Query Tutorial | ||
ListBox | lbTarget | Set Items to the following set of strings: Lines Polygons Points POIs Streets Street Links
| ||
ListBox | lbTargetType | |||
MapCtrl | mapCtrl | Set DragBehavior to None.
|
When we created the form, we populated the lbTarget list box with the types of features we want to query. However, we did not populate the lbTargetType list box, which will contain subtypes for each of these main feature types. Since each main feature type has a different set of subtypes, we populate this list of subtypes by adding a SelectedIndexChanged handler to the lbTarget list box.
Add the following handler for the SelectedIndexChanged event of lbTarget:
private void lbTarget_SelectedIndexChanged(object sender, EventArgs e) { lbTargetType.Items.Clear(); switch (lbTarget.SelectedItem.ToString()) { case "Lines": LoadLineTypes(); break; case "Polygons": LoadPolygonTypes(); break; case "Points": LoadPointTypes(); break; case "POIs": LoadPOITypes(); break; case "Streets": LoadStreetTypes(); break; case "StreetLinks": // there are no subtypes for street link queries break; } if (lbTargetType.Items.Count > 0) lbTargetType.SelectedIndex = 0; }
Note that this event handler relies on a number of helper functions (LoadLineTypes, LoadPolygonTypes, LoadPointTypes, and so on.) Add the following code to define these helper functions:
private void LoadLineTypes() { lbTargetType.Items.Add("all"); lbTargetType.Items.Add("railways"); lbTargetType.Items.Add("rivers"); lbTargetType.Items.Add("canals"); } private void LoadPolygonTypes() { lbTargetType.Items.Add("all"); lbTargetType.Items.Add("countries"); lbTargetType.Items.Add("states"); lbTargetType.Items.Add("counties"); lbTargetType.Items.Add("islands"); lbTargetType.Items.Add("cities"); lbTargetType.Items.Add("airports"); lbTargetType.Items.Add("runways"); lbTargetType.Items.Add("cemeteries"); lbTargetType.Items.Add("hospitals"); lbTargetType.Items.Add("industrial"); lbTargetType.Items.Add("major_parks"); lbTargetType.Items.Add("state_parks"); lbTargetType.Items.Add("parks"); lbTargetType.Items.Add("shopping"); lbTargetType.Items.Add("sports"); lbTargetType.Items.Add("universities"); lbTargetType.Items.Add("golf_courses"); lbTargetType.Items.Add("native"); lbTargetType.Items.Add("military"); lbTargetType.Items.Add("buildings"); lbTargetType.Items.Add("oceans"); lbTargetType.Items.Add("bays"); lbTargetType.Items.Add("water"); } private void LoadPointTypes() { lbTargetType.Items.Add("all"); lbTargetType.Items.Add("small_villages"); lbTargetType.Items.Add("villages"); lbTargetType.Items.Add("large_villages"); lbTargetType.Items.Add("towns"); lbTargetType.Items.Add("large_towns"); lbTargetType.Items.Add("small_cities"); lbTargetType.Items.Add("cities"); lbTargetType.Items.Add("major_cities"); } private void LoadPOITypes() { foreach (int i in Enum.GetValues(typeof(PoiType))) { lbTargetType.Items.Add(Enum.GetName(typeof(PoiType), i)); } } private void LoadStreetTypes() { foreach (int i in Enum.GetValues(typeof(StreetType))) { lbTargetType.Items.Add(Enum.GetName(typeof(StreetType), i)); } }
For Lines, Polygons, and Points, the sub-types are hard-coded to the names of Predefined query tables. For Points of interest (POIs) and for Streets, we can extract the names from the PoiType and StreetType enumerations, respectively.
When our application first starts up, we need to add a perform some initialization so that we are ready to display query results on the map. We also want to make sure that the lbTargetType list box is properly populated.
using Telogis.GeoBase;
Point mouseDownPoint; RendererList renderList = new RendererList(); const int maxPoints = 25; // maximum number of points to show on map
The declaration of mouseDownPoint will allow us to capture the mouse position when the user first clicks on the map. The RendererList will allow us to add BalloonPushPin objects to the map so that it displays our query results. Finally, the maxPoints constant will be used to limit the number of results we display on the map, so that it does not get too crowded.
// Initialize the map Control mapCtrl.Renderer = renderList; mapCtrl.Zoom = ZoomLevel.CityLevel; // Initialize the list boxes lbTarget.SelectedIndex = 0;
This links our RendererList to the map control, sets the starting zoom level, and selects a default feature type to query. Note that when we select an item in lbTarget, the event handler we added previously causes the lbTargetType list box to be populated with subtypes.
Before we add event handlers to the map for performing our data queries and displaying them on the map, let's define some helper functions to make that process easier.
private bool addPointToMap(string name, LatLon point) { BalloonPushPin bb = new BalloonPushPin(name, point); bb.ShowInformation = false; renderList.Add(bb); return renderList.Count > maxPoints; } private bool addPointToMap(string name, LatLon[] points) { foreach (LatLon ll in points) { if (mapCtrl.Contains(ll)) { return addPointToMap(name, ll); } } // no points on map -- don't bother return false; }
These methods generate a BalloonPushPin object and add it to our RendererList so that they are drawn on the map. To simplify the display, we set the push pin's ShowInformation property to false, so that each balloon shows only the name of a feature identified in our data query. After adding the push pin, the method checks whether we have reached the maximum number of features we want to display, and if so, it returns true.
Note that the second overload takes an array of locations rather than a single location. We will only be displaying a single balloon push pin for each feature our data query identifies. However, many types of features include a number of points. For handling these types, the second overload selects a single location to use from an array of possible locations.
private string GetNameForLine(Line line) { if (line.Name.Length > 0) return line.Name; switch (line.Flags) { case 0: return "railway tunnel"; case 1: return "railway"; case 2: return "river"; case 3: return "canal"; case 4: return "county line"; case 5: return "state line"; case 6: return "border"; } return "unknown"; } private string GetNameForPolygon(Polygon poly) { if (poly.Name.Length > 0) return poly.Name; switch (poly.Flags) { case 0: return "ocean"; case 1: return "state"; case 2: return "city"; case 3: return "bay"; case 4: return "water"; case 5: return "building"; case 8: return "airport"; case 9: return "cemetery"; case 10: return "hospital"; case 11: return "industrial complex"; case 12: return "military base"; case 13: return "national park"; case 14: return "state park"; case 15: return "county park"; case 16: return "shopping center"; case 17: return "Sports complex"; case 18: return "college"; case 19: return "aircraft road"; case 20: return "golf course"; case 21: return "reservation"; case 22: return "country"; case 23: return "county"; case 24: return "island"; } return "unknown"; } private string GetNameForPoint(PointFeature pf) { if (pf.Name.Length > 0) return pf.Name; switch (pf.Flags) { case 0: return "small village"; case 1: return "village"; case 2: return "large village"; case 3: return "town"; case 4: return "large town"; case 5: return "small city"; case 6: return "city"; case 7: return "major city"; } return "unknown"; } private string GetNameForPoi(Poi poi) { if (poi.Name.Length > 0) return poi.Name; return Enum.GetName(typeof(PoiType), poi.Type); } private string GetNameForStreet(Street street) { foreach (string s in street.Name) { if (s.Length > 0) return s; } return "unknown"; } private string GetNameForLink(StreetLink sl) { foreach (string s in sl.Names) { if (s.Length > 0) return s; } return "type " + sl.Flags.FUNC_CLASS.ToString(); }
Each of these methods uses the name of the feature if it is available. If it is not available, a name is constructed based on the other information in the object.
We are now ready to use the DataQuery object to query our map data about the different feature types.
When the user clicks down on the mouse button, we will capture the current location of the mouse.
When the user releases the mouse button, we will check the location of the mouse again. If it is the same as when the mouse went down, (a simple click), we will use the DataQuery to locate the closest features of the target type that are near the point where the user clicked. If the mouse was released in a different location (a click, drag, and release), we will use the DataQuery to locate all features of the target type that lie within the region defined by the user's click and drag action.
Finally, we will use the helper functions we have already defined to draw BalloonPushPin objects on the map, displaying the results of our query.
private void mapCtrl_MouseDown(object sender, MouseEventArgs e) { mouseDownPoint = e.Location; }
This captures and saves the location of the mouse when the user clicked the mouse button.
private void mapCtrl_MouseUp(object sender, MouseEventArgs e) { BoundingBox bb; renderList.Clear(); // we are replacing any push pins added previously switch (lbTarget.SelectedItem.ToString()) { case "Lines": Line[] lineResults; // if mouse goes up where it went down, this means find nearest to point if (e.Location == mouseDownPoint) { LatLon location = mapCtrl.XYtoLatLon(e.X, e.Y); lineResults = DataQuery.FindNearestLines(location, lbTargetType.SelectedItem.ToString()); } else { // if mouse moved before going up, this means find lines inside the defined region bb = new BoundingBox( mapCtrl.XYtoLatLon(mouseDownPoint.X, mouseDownPoint.Y), mapCtrl.XYtoLatLon(e.X, e.Y)); lineResults = DataQuery.QueryLines(bb, lbTargetType.SelectedItem.ToString()); } // add push pins for the first point in each line of the results foreach (Line l in lineResults) { if (addPointToMap(GetNameForLine(l), l.Points)) break; } break; case "Polygons": Polygon[] polygonResults; // if mouse goes up where it went down, this means find nearest to point if (e.Location == mouseDownPoint) { LatLon location = mapCtrl.XYtoLatLon(e.X, e.Y); polygonResults = DataQuery.FindNearestPolygons(location, lbTargetType.SelectedItem.ToString()); } else { // if mouse moved before going up, this means find polygons inside the defined region bb = new BoundingBox( mapCtrl.XYtoLatLon(mouseDownPoint.X, mouseDownPoint.Y), mapCtrl.XYtoLatLon(e.X, e.Y)); polygonResults = DataQuery.QueryPolygons(bb, lbTargetType.SelectedItem.ToString()); } // add push pins for the first point in the outer ring of each polygon of the results foreach (Polygon p in polygonResults) { if (addPointToMap(GetNameForPolygon(p), p.Geometry.OuterRing.Points.Points)) break; } break; case "Points": PointFeature[] pointResults; // if mouse goes up where it went down, this means find nearest to point if (e.Location == mouseDownPoint) { LatLon location = mapCtrl.XYtoLatLon(e.X, e.Y); pointResults = DataQuery.FindNearestPoints(location, lbTargetType.SelectedItem.ToString()); } else { // if mouse moved before going up, this means find points inside the defined region bb = new BoundingBox( mapCtrl.XYtoLatLon(mouseDownPoint.X, mouseDownPoint.Y), mapCtrl.XYtoLatLon(e.X, e.Y)); pointResults = DataQuery.QueryPoints(bb, lbTargetType.SelectedItem.ToString()); } // add push pins for each point feature of the results foreach (PointFeature p in pointResults) { if (addPointToMap(GetNameForPoint(p), p.Point)) break; } break; case "POIs": // POIs can only be located within a region bb = new BoundingBox( mapCtrl.XYtoLatLon(mouseDownPoint.X, mouseDownPoint.Y), mapCtrl.XYtoLatLon(e.X, e.Y)); if (e.Location == mouseDownPoint) { // if we only have a single point, inflate the bounding box bb.Inflate(0.5); } // we are filtering POIs by type but not name Poi[] poiResults = DataQuery.QueryPoi(bb, new PoiType[] {(PoiType) Enum.Parse(typeof(PoiType),lbTargetType.SelectedItem.ToString())}, null); // add push pins with the name and location of each poi foreach (Poi poi in poiResults) { if (addPointToMap(GetNameForPoi(poi), poi.Location)) break; } break; case "Streets": // Streets can only be located within a region bb = new BoundingBox( mapCtrl.XYtoLatLon(mouseDownPoint.X, mouseDownPoint.Y), mapCtrl.XYtoLatLon(e.X, e.Y)); if (e.Location == mouseDownPoint) { // if we only have a single point, inflate the bounding box bb.Inflate(0.5); } Street[] streetResults = DataQuery.QueryStreets(bb, (StreetType)Enum.Parse(typeof(StreetType), lbTargetType.SelectedItem.ToString())); // add push pins with the first name of each street and the first point on the street foreach (Street s in streetResults) { if (addPointToMap(GetNameForStreet(s), s.Points)) break; } break; case "Street Links": // Street Links can only be located within a region bb = new BoundingBox( mapCtrl.XYtoLatLon(mouseDownPoint.X, mouseDownPoint.Y), mapCtrl.XYtoLatLon(e.X, e.Y)); if (e.Location == mouseDownPoint) { // if we only have a single point, inflate the bounding box bb.Inflate(0.5); } StreetLink[] slResults = DataQuery.QueryLinks(bb); // add push pins with the first name of each street link and the first point on the street link foreach (StreetLink sl in slResults) { if (addPointToMap(GetNameForLink(sl), sl.Points)) break; } break; } mapCtrl.Invalidate(); mapCtrl.Update(); }
This clears the RendererList of any push pins from the last query. It then checks the point where the mouse came up and either calls the appropriate FindNearest... method or constructs a bounding box for the implied region and calls the appropriate Query... method. The results of the data query are then added to the map using one of our addPointToMap helper functions. Finally, we invalidate the map control so that it repaints with the new query results.
Run your application. When the form first appears, it should look like the following screen shot:
Try selecting different types of features and then clicking on the map or clicking and dragging on the map. The map updates to show the features nearest where you clicked or within the region you dragged out: