Location Search Tutorial |
The LocationSearch class enables you to search for locations across different sources, such as geocoders, POI sources or custom data sources.
This feature can be used to search for street addresses, LatLon locations, regions, POI types (for example 'petrol', 'pizza' or 'italian food'), named POIs, or even custom terms that you define yourself; all from a single unified search interface.
In this tutorial, we will create a Windows Forms application with a map, a drop-down list of POI types, a search box for addresses or query strings, and a results list. You can select a POI type from the list and either enter the address or coordinates into the search box, or double click on the map to select a location. Alternatively you can enter the query string straight into the search box. The resulting location suggestions will be displayed on the map as PushPins and in the results list with their address. For locations selected from the map, the distance from the search location will also be displayed.
The complete code for the project example created in this tutorial is included at the bottom of this page.
Note |
---|
The LocationSearch requires requires GBFS files that are compatible with the Autocomplete Geocoder. If you are using files from Q3 2014 onwards, the files will have an uppercase A among the letters immediately prior to the .gbfs extension, for example HERE_USAWestUSA_15Q4_30x151208-1141_CaIAZ.gbfs. If you are using files from before Q3 2014, these files will have an _acgc suffix immediately prior to the .gbfs extension, for example HERE_USAWestUSA_14Q1_acgc.gbfs. If you do not have the appropriate files, contact GeoBase Support via gbsupport@verizonconnect.com to arrange a trial. |
Create a new Visual Studio Project (a Windows Forms Application), targeting .NET Framework 4.0 or higher. Name it 'LocationSearch'.
Add a reference to geobase.net.dll. Then, to make the code simpler and more readable, add the following using directives to the top of the project form (Form1.cs).
using System; using System.Drawing; using System.Text; using System.Windows.Forms; using Telogis.GeoBase; using Telogis.GeoBase.ImageUtils; using Telogis.GeoBase.Geocoding; using Telogis.GeoBase.Repositories;
Return to the 'Design' view (Shift+F7) and add the following controls to the form:
When run, your new application should appear similar to the screenshot below:
Go back to the 'Design' view (Shift+F7). Using the Events view on the Properties panel, double click to create handler stubs for the following events:
Next, return to 'Code' view (F7), and add the following code to the top of the project such that the items are global (immediately above the Form1 constructor).
This code will:
// Create rendererlist RendererList renderList = new RendererList(); private LatLon lastClickLocation = LatLon.Empty; private const string instructionText = "Select a POI type and either enter an address/location or double-click a location on the map. Alternatively enter a full search query. Then click the Search button.";
Update the Form1 constructor method with the following code. It will:
public Form1() { InitializeComponent(); // Create a repository to point to your map data. SimpleRepository repository = new SimpleRepository(@"\PATH\TO\MAP\DATA_acgc.gbfs"); Repository.CurrentThreadRepository = repository; // Carry out some housekeeping tasks to configure our form. mapMain.DragBehavior = DragBehavior.Hand; QueryBox.BackColor = Color.White; ResultsBox.Text = instructionText; mapMain.Center = new LatLon(33.879692, -118.029161); mapMain.Zoom = 50; mapMain.UiEnable = true; mapMain.Renderer = renderList; // Add a new custom search source to our Location Search. LocationSearch.AddSearchSource(new TelogisCustomSource()); }
Update the buttonReset_Click method that was added automatically when you double-clicked the events for the UI controls from the Design view. This code is executed when the 'Reset' button (buttonReset) is pressed, and will:
private void buttonReset_Click(object sender, EventArgs e) { renderList.Clear(); // Clear the results and query text fields. ResultsBox.Text = instructionText; QueryBox.Clear(); // Set the POI Type back to 'None'. poiTypes.Text = "None"; // Reset the map center location and zoom levels. mapMain.Center = new LatLon(33.879692, -118.029161); mapMain.Zoom = 50; // Reset the map to its initial state when the Reset button is clicked. mapMain.Invalidate(); }
Then update the mapMain_MouseClick method. This code is executed when the mouse is double-clicked on a map location. It will:
void mapMain_MouseClick(object sender, MouseEventArgs e) { // Get the map location when the map is double-clicked and convert to a LatLon. LatLon mapLocation = mapMain.XYtoLatLon(e.X, e.Y); // Reverse geocode the address under the mouse double-click. Address address = GeoCoder.ReverseGeoCode(mapLocation); // If a valid address is found, display it in the query textbox. if (address != null) { QueryBox.Text = address.ToString(); } // Update the last click location, for our address BalloonPushPin. lastClickLocation = mapLocation; // Create and add the BalloonPushPin to the map. BalloonPushPin bpp = new BalloonPushPin(lastClickLocation); renderList.Add(bpp); mapMain.Renderer = bpp; mapMain.Invalidate(); mapMain.Focus(); }
Next, update the QueryBox_TextChanged method with the code below. This code is executed when the text content of the QueryBox field changes.
private void QueryBox_TextChanged(object sender, EventArgs e) { // Check whether the text in the query text field has changed. If it // has, clear the last click location and clear the map of pins. lastClickLocation = LatLon.Empty; renderList.Clear(); }
Next, paste the following code into the buttonSearch_Click method. This code is executed when the 'Search' button is clicked, contains the Location Search operations and will:
private void buttonSearch_Click(object sender, EventArgs e) { // Get the POI type selected in the drop-down list String poiType = poiTypes.Text; // Get the address or query string. String queryString = QueryBox.Text; // Is there text in the query box? if (queryString != "") { // Is there a POI type selected? If not then we will use the query string as it is. The // query string may already contain a query consisting of the POI name and a string like "near" // or "in". if (poiType != "None"){ // Combine the POI type and the address, adding 'near' to the string. queryString = poiType + " near " + queryString; } // Construct the Location Search arguments var args = new LocationSearchArgs { Query = queryString, // Specifying the country speeds the search process. When not supplied, // all countries will be searched. Countries = new Country[] { Country.USA }, // Set a location hint for the center of the map. This is // used to bias the results toward the area currently visible on the map. LocationHint = mapMain.Center }; // Supply the Location Search search args. var search = new LocationSearch(args); // Then retrieve the results. LocationSearchResult output = search.Search(); // Begin constructing the text that will be shown in the results field. StringBuilder locationResults = new StringBuilder("\r\nResults for Location Search '"); locationResults.AppendFormat(queryString + "' \r\n\r\n"); // Check that there are some results returned. if (output.Suggestions.Length > 0) { // Clear the renderlist to prepare for result PushPins renderList.Clear(); // Add back the address used in the Location Search. if (lastClickLocation != LatLon.Empty) { BalloonPushPin bpp = new BalloonPushPin(lastClickLocation); renderList.Add(bpp); } // Set a counter to number the results, and to get the correct icons // to use for the PushPins. These will be used to number the results in order from // best to worst match. int counter = 1; int pincounter = Icons.Number1; // Create a new BoundingBox. We will add each result location to this, then // zoom the map to encompass the BoundingBox BoundingBox searchArea = new BoundingBox(); // Begin the loop through the Location Search results. for (int idx = 0; idx < output.Suggestions.Length; idx++) { // Append the result description. locationResults.AppendFormat(counter + ": " + output.Suggestions[idx].DisplayText); // Then, if the last clicked location and the suggested location aren't empty... if (lastClickLocation != LatLon.Empty && output.Suggestions[idx].Location != LatLon.Empty) { // Work out the distance between the result location and the clicked location, and convert it to miles. // Also getting rid of any unnecessary decimal points. decimal distShort = (decimal)(Math.Truncate((double)lastClickLocation.QuickDistanceTo(output.Suggestions[idx].Location, DistanceUnit.MILES) * 100.0) / 100.0); // Add the distance to the string. locationResults.AppendFormat(" (distance " + distShort + " miles)"); } // Add some newlines at the end of the result string. locationResults.AppendFormat("\r\n\r\n"); // Then add a simple PushPin at the result location. PushPin pin = new PushPin(output.Suggestions[idx].Location); // And expand the BoundingBox with the result location. searchArea.Add(output.Suggestions[idx].Location); // Then set the PushPin icon to a number from 1-20 (20 is the default number of results returned // from a Location Search operation). pin.Icon = pincounter; // Add the pin to the renderlist and increment the two counters by one. renderList.Add(pin); counter++; pincounter++; // Loop back to the top and repeat for the next result, if any. } // Convert the results StringBuilder text into a string, then add this to the results field. ResultsBox.Text = locationResults.ToString(); mapMain.Renderer = renderList; searchArea.Inflate(0.001); // Zoom the map to the results BoundingBox. mapMain.ZoomToBoundingBox(searchArea, 100); mapMain.Invalidate(); // Change focus to the map, so the user can pan and zoom. mapMain.Focus(); } else { // If the Location Search completed without any results found, display this in the results field. ResultsBox.Text = "No results found"; }; } else { // Display an alert message if the search button is clicked without providing text in the query field. MessageBox.Show("Double-click a location on the map, or enter an address/location. Alternatively enter your own search query (for example 'hotels in los angeles'). Then click the Search button."); } }
Next, paste the following TelogisCustomSource class. This code will define a custom search source that will be used by the Location Search engine in conjunction with built-in sources to modify search results. Location Search can be used without any custom search sources, but you can use them to provide new results or adjust existing results according to your own requirements. This custom source was added in the constructor for Form1 above.
public class TelogisCustomSource : LocationSearchSource { // Override the CreateSearchOperation method, which creates the operation to perform // the searching with this search source. public override LocationSearchOperation CreateSearchOperation(LocationSearchArgs args) { return new TelogisSearchOperation(args); } // Override the HandledResultType for this location search with an // IndividualPointOfInterest result type. It describes the type // of results that this source returns (in this case, specifically // named points of interest), which the Location Search engine uses // to determine whether the search source is relevant for a particular search. public override LocationSearch.ResultType HandledResultTypes { get { return LocationSearch.ResultType.IndividualPointOfInterest; } } // Override the PriorityForSearch method with a priority that is higher than // the DefaultPoiPriority, so that this search source is used first. public override int PriorityForSearch(LocationSearchArgs args) { return LocationSearchSource.DefaultPoiPriority + 1; } // Override the GetIgnoredTypesForMatchingSearches method with an // IndividualPointOfInterest result type. When this source finds // results, the Location Search engine will ignore other search // sources with a lower priority that find IndividualPointOfInterest results. public override LocationSearch.ResultType GetIgnoredTypesForMatchingSearches(LocationSearchArgs args) { return LocationSearch.ResultType.IndividualPointOfInterest; } // Override the name of this search source. public override string Name { get { return string.Format(@"TelogisCustomSource"); } } }
Finally paste the following TelogisSearchOperation class. This code will return a specific example location (i.e. the location of the Telogis headquarters) when the search query contains a prefix of "Telogis", for example "Telo".
public class TelogisSearchOperation : LocationSearchOperation { // The arguments to use for this location search. private LocationSearchArgs _args; // Constructor for this LocationSearchOperation public TelogisSearchOperation(LocationSearchArgs args) { _args = args; } // Override the FindResults method with a specific example. public override LocationSearchResult FindResults() { LocationSearchSuggestion[] suggestions = new LocationSearchSuggestion[0]; string lowerCaseQuery = _args.Query.Trim().ToLower(); if ("telogis".StartsWith(lowerCaseQuery)) { suggestions = new LocationSearchSuggestion[] { new LocationSearchSuggestion { ResultType = LocationSearch.ResultType.IndividualPointOfInterest, Location = new LatLon(33.583906, -117.731399), Confidence = 0.6 + 0.4 * lowerCaseQuery.Length / "telogis".Length, StreetNumber = 20, StreetName = "Enterprise", Regions = new string[] {"Aliso Viejo", "Orange", "California"}, PostCode = "92656", Country = Country.USA, Name = "Telogis Headquarters", DisplayText = "Telogis Headquarters, 20 Enterprise, Aliso Viejo, California 92656" } }; } return new LocationSearchResult() { Suggestions = suggestions, Errors = new string[0], Status = SearchResult.SearchCompleted }; } // Override the TryCancel method, immediately returning true to indicate cancellation // was successful, because this operation takes a negligible amount of time to run. public override bool TryCancel() { return true; } }
The complete code for this example project is included below:
using System; using System.Drawing; using System.Text; using System.Windows.Forms; using Telogis.GeoBase; using Telogis.GeoBase.ImageUtils; using Telogis.GeoBase.Geocoding; using Telogis.GeoBase.Repositories; namespace LocationSearchMap { public partial class Form1 : Form { // Create rendererlist RendererList renderList = new RendererList(); private LatLon lastClickLocation = LatLon.Empty; private const string instructionText = "Select a POI type and either enter an address/location or double-click a location on the map. Alternatively enter a full search query. Then click the Search button."; public Form1() { InitializeComponent(); // Create a repository to point to your map data. SimpleRepository repository = new SimpleRepository(@"\PATH\TO\MAP\DATA_acgc.gbfs"); Repository.CurrentThreadRepository = repository; // Carry out some housekeeping tasks to configure our form. mapMain.DragBehavior = DragBehavior.Hand; QueryBox.BackColor = Color.White; ResultsBox.Text = instructionText; mapMain.Center = new LatLon(33.879692, -118.029161); mapMain.Zoom = 50; mapMain.UiEnable = true; mapMain.Renderer = renderList; // Add a new custom search source to our Location Search. LocationSearch.AddSearchSource(new TelogisCustomSource()); } private void buttonReset_Click(object sender, EventArgs e) { renderList.Clear(); // Clear the results and query text fields. ResultsBox.Text = instructionText; QueryBox.Clear(); // Set the POI Type back to 'None'. poiTypes.Text = "None"; // Reset the map center location and zoom levels. mapMain.Center = new LatLon(33.879692, -118.029161); mapMain.Zoom = 50; // Reset the map to its initial state when the Reset button is clicked. mapMain.Invalidate(); } void mapMain_MouseClick(object sender, MouseEventArgs e) { // Get the map location when the map is double-clicked and convert to a LatLon. LatLon mapLocation = mapMain.XYtoLatLon(e.X, e.Y); // Reverse geocode the address under the mouse double-click. Address address = GeoCoder.ReverseGeoCode(mapLocation); // If a valid address is found, display it in the query textbox. if (address != null) { QueryBox.Text = address.ToString(); } // Update the last click location, for our address BalloonPushPin. lastClickLocation = mapLocation; // Create and add the BalloonPushPin to the map. BalloonPushPin bpp = new BalloonPushPin(lastClickLocation); renderList.Add(bpp); mapMain.Renderer = bpp; mapMain.Invalidate(); mapMain.Focus(); } private void QueryBox_TextChanged(object sender, EventArgs e) { // Check whether the text in the query text field has changed. If it // has, clear the last click location and clear the map of pins. lastClickLocation = LatLon.Empty; renderList.Clear(); } private void buttonSearch_Click(object sender, EventArgs e) { // Get the POI type selected in the drop-down list String poiType = poiTypes.Text; // Get the address or query string. String queryString = QueryBox.Text; // Is there text in the query box? if (queryString != "") { // Is there a POI type selected? If not then we will use the query string as it is. The // query string may already contain a query consisting of the POI name and a string like "near" // or "in". if (poiType != "None"){ // Combine the POI type and the address, adding 'near' to the string. queryString = poiType + " near " + queryString; } // Construct the Location Search arguments var args = new LocationSearchArgs { Query = queryString, // Specifying the country speeds the search process. When not supplied, // all countries will be searched. Countries = new Country[] { Country.USA }, // Set a location hint for the center of the map. This is // used to bias the results toward the area currently visible on the map. LocationHint = mapMain.Center }; // Supply the Location Search search args. var search = new LocationSearch(args); // Then retrieve the results. LocationSearchResult output = search.Search(); // Begin constructing the text that will be shown in the results field. StringBuilder locationResults = new StringBuilder("\r\nResults for Location Search '"); locationResults.AppendFormat(queryString + "' \r\n\r\n"); // Check that there are some results returned. if (output.Suggestions.Length > 0) { // Clear the renderlist to prepare for result PushPins renderList.Clear(); // Add back the address used in the Location Search. if (lastClickLocation != LatLon.Empty) { BalloonPushPin bpp = new BalloonPushPin(lastClickLocation); renderList.Add(bpp); } // Set a counter to number the results, and to get the correct icons // to use for the PushPins. These will be used to number the results in order from // best to worst match. int counter = 1; int pincounter = 208; // Create a new BoundingBox. We will add each result location to this, then // zoom the map to encompass the BoundingBox BoundingBox searchArea = new BoundingBox(); // Begin the loop through the Location Search results. for (int idx = 0; idx < output.Suggestions.Length; idx++) { // Append the result description. locationResults.AppendFormat(counter + ": " + output.Suggestions[idx].DisplayText); // Then, if the last clicked location and the suggested location aren't empty... if (lastClickLocation != LatLon.Empty && output.Suggestions[idx].Location != LatLon.Empty) { // Work out the distance between the result location and the clicked location, and convert it to miles. // Also getting rid of any unnecessary decimal points. decimal distShort = (decimal)(Math.Truncate((double)lastClickLocation.QuickDistanceTo(output.Suggestions[idx].Location, DistanceUnit.MILES) * 100.0) / 100.0); // Add the distance to the string. locationResults.AppendFormat(" (distance " + distShort + " miles)"); } // Add some newlines at the end of the result string. locationResults.AppendFormat("\r\n\r\n"); // Then add a simple PushPin at the result location. PushPin pin = new PushPin(output.Suggestions[idx].Location); // And expand the BoundingBox with the result location. searchArea.Add(output.Suggestions[idx].Location); // Then set the PushPin icon to a number from 1-20 (20 is the default number of results returned // from a Location Search operation). pin.Icon = pincounter; // Add the pin to the renderlist and increment the two counters by one. renderList.Add(pin); counter++; pincounter++; // Loop back to the top and repeat for the next result, if any. } // Convert the results StringBuilder text into a string, then add this to the results field. ResultsBox.Text = locationResults.ToString(); mapMain.Renderer = renderList; searchArea.Inflate(0.001); // Zoom the map to the results BoundingBox. mapMain.ZoomToBoundingBox(searchArea, 100); mapMain.Invalidate(); // Change focus to the map, so the user can pan and zoom. mapMain.Focus(); } else { // If the Location Search completed without any results found, display this in the results field. ResultsBox.Text = "No results found"; }; } else { // Display an alert message if the search button is clicked without providing text in the query field. MessageBox.Show("Double-click a location on the map, or enter an address/location. Alternatively enter your own search query (for example 'hotels in los angeles'). Then click the Search button."); } } public class TelogisCustomSource : LocationSearchSource { // Override the CreateSearchOperation method, which creates the operation to perform // the searching with this search source. public override LocationSearchOperation CreateSearchOperation(LocationSearchArgs args) { return new TelogisSearchOperation(args); } // Override the HandledResultType for this location search with an // IndividualPointOfInterest result type. It describes the type // of results that this source returns (in this case, specifically // named points of interest), which the Location Search engine uses // to determine whether the search source is relevant for a particular search. public override LocationSearch.ResultType HandledResultTypes { get { return LocationSearch.ResultType.IndividualPointOfInterest; } } // Override the PriorityForSearch method with a priority that is higher than // the DefaultPoiPriority, so that this search source is used first. public override int PriorityForSearch(LocationSearchArgs args) { return LocationSearchSource.DefaultPoiPriority + 1; } // Override the GetIgnoredTypesForMatchingSearches method with an // IndividualPointOfInterest result type. When this source finds // results, the Location Search engine will ignore other search // sources with a lower priority that find IndividualPointOfInterest results. public override LocationSearch.ResultType GetIgnoredTypesForMatchingSearches(LocationSearchArgs args) { return LocationSearch.ResultType.IndividualPointOfInterest; } // Override the name of this search source. public override string Name { get { return string.Format(@"TelogisCustomSource"); } } } public class TelogisSearchOperation : LocationSearchOperation { // The arguments to use for this location search. private LocationSearchArgs _args; // Constructor for this LocationSearchOperation public TelogisSearchOperation(LocationSearchArgs args) { _args = args; } // Override the FindResults method with a specific example. public override LocationSearchResult FindResults() { LocationSearchSuggestion[] suggestions = new LocationSearchSuggestion[0]; string lowerCaseQuery = _args.Query.Trim().ToLower(); if ("telogis".StartsWith(lowerCaseQuery)) { suggestions = new LocationSearchSuggestion[] { new LocationSearchSuggestion { ResultType = LocationSearch.ResultType.IndividualPointOfInterest, Location = new LatLon(33.583906, -117.731399), Confidence = 0.6 + 0.4 * lowerCaseQuery.Length / "telogis".Length, StreetNumber = 20, StreetName = "Enterprise", Regions = new string[] {"Aliso Viejo", "Orange", "California"}, PostCode = "92656", Country = Country.USA, Name = "Telogis Headquarters", DisplayText = "Telogis Headquarters, 20 Enterprise, Aliso Viejo, California 92656" } }; } return new LocationSearchResult() { Suggestions = suggestions, Errors = new string[0], Status = SearchResult.SearchCompleted }; } // Override the TryCancel method, immediately returning true to indicate cancellation // was successful, because this operation takes a negligible amount of time to run. public override bool TryCancel() { return true; } } } }