Autocomplete Geocoder Tutorial |
The AutocompleteGeocoder class takes partial address strings, and generates a list of suggestions of full addresses. This can be used to create an interactive address search, where suggested addresses are displayed in near real-time as users type, potentially allowing the correct address to be selected before the user has keyed the full address string.
In this tutorial, we will create a Windows Forms application with an address search box, and a list of address suggestions that updates as the search string changes. These suggestions can be selected with the mouse, and the corresponding location will then be displayed on a map.
Note |
---|
The AutocompleteGeocoder requires specific GBFS files:
|
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:
Efficient use of the AutocompleteGeocoder requires your application to make use of an asynchronous, thread-based structure. This tutorial assumes basic familiarity with threading in .NET, and the approach has been structured to be as simple as possible, obviating the need for locks or cross-thread invocation. If you are unfamiliar with using threading in .NET, we would recommend reading a primer such as Threading in C# by Joseph Albahari, which the author has made freely available online.
Create a new Visual Studio Project containing a Windows Forms Application, targeting .NET Framework 4.0 or higher. Name your project AutocompleteGeocoderExample. Add a reference to geobase.net.dll, then add the following controls to your form:
A suggested layout is in the screenshot below. Note that to achieve this layout you will need to set the AutoSize property of labelStatus to false, then set the control's Size, TextAlign and Border properties appropriately.
Using the events view on the Properties panel, double click to create handler stubs for the following events:
Switch to the code view for your form, and add the following using directives to the top of the project form:
using System; using System.ComponentModel; using System.Drawing; using System.Threading; using System.Windows.Forms; using Telogis.GeoBase; using Telogis.GeoBase.Geocoding; using Telogis.GeoBase.Repositories; using Telogis.GeoBase.Addresses;
First, we will set up a BackgroundWorker, which lets us send items of work to be processed on a background thread. Add the following properties to the Form1 class:
private BackgroundWorker geocodeWorker = new BackgroundWorker(); private AutocompleteGeocoderArgs nextSearch = null; private const int SEARCH_TIMEOUT_SECS = 1;
This constructs our BackgroundWorker, geocodeWorker, and sets up a property and a constant that will be required shortly.
Next, we need to create a repository that points to the appropriate map data file containing Autocomplete Geocoding data.
Additionally, we need to set up the DoWork and RunWorkerCompleted handlers for geocodeWorker. DoWork is the engine of our application, receiving items of work to process asynchronously on a background thread. When this work is completed, the RunWorkerCompleted handler is triggered on the UI thread, allowing UI components to be updated with the new information.
In the Form1 constructor, after the call to InitializeComponent(), add the following lines:
// Set the map data to point at data containing ACGC data. SimpleRepository repository = new SimpleRepository(@"[Path to the GBFS data file]"); Repository.Default = repository; // Set up the BackgroundWorker handlers. geocodeWorker.DoWork += new DoWorkEventHandler(geocodeWorker_DoWork); geocodeWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(geocodeWorker_RunWorkerCompleted); // Ensure the UI has the correct focus on startup. textBoxSearch.Focus();
Next, we will define the DoWork handler - a stub for this may have been created automatically in the previous step; if not, then create it using the code sample below. This handler receives an AutocompleteGeocoderArgs object passed in as an argument, runs this through the AutocompleteGeocoder to generate suggestions, and provides an AutocompleteGeocoderResult to be passed to the RunWorkerCompleted handler.
void geocodeWorker_DoWork(object sender, DoWorkEventArgs e) { // Set the Autocomplete Geocoder arguments and get the results of the Autocomplete Geocoding operation. AutocompleteGeocoderArgs args = (AutocompleteGeocoderArgs)e.Argument; AutocompleteGeocoderResult result = AutocompleteGeocoder.Geocode(args); e.Result = result; }
Now we need a way of feeding searches into this background engine. We will do this by detecting changes to the text in textBoxSearch, and constructing AutocompleteGeocoderArgs objects to pass to geocodeWorker for processing. Update your textBoxSearch_TextChanged handler to contain the following:
private void textBoxSearch_TextChanged(object sender, EventArgs e) { // Get the text from the search box. string searchTerm = (sender as TextBox).Text; // Build an AutocompleteGeocoder argument object to pass to the background thread. AutocompleteGeocoderArgs args = new AutocompleteGeocoderArgs { Query = searchTerm, Countries = new Country[] { Country.USA }, LocationHint = this.mapMain.Center, Timeout = TimeSpan.FromSeconds(SEARCH_TIMEOUT_SECS) }; if (!geocodeWorker.IsBusy && nextSearch == null) { // If background worker isn't running at present, and there is nothing // scheduled to run next, run straight away. geocodeWorker.RunWorkerAsync(args); } else { // Otherwise, set to run next. This will replace // anything previously scheduled to run next. nextSearch = args; } }
Our geocodeWorker can only handle one request at a time, so we are checking if it is busy before telling it to action our AutocompleteGeocoderArgs object. If so, we will instead store this object in the nextSearch property, to be picked up when the current search has completed.
So, we can now feed requests into geocodeWorker, and we have defined the engine that will process them. Next, we need to pick up the results produced by the this engine, and display them in our list box. To do this, we need to define the geocodeWorker_RunWorkerCompleted handler - a stub for this may have been created automatically in an earlier step; if not, then create it using the code sample below.
void geocodeWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { // Clear the ListBox of any old suggestions listBoxResults.Items.Clear(); // Get the result of the Autocomplete Geocoding operation. AutocompleteGeocoderResult result = (AutocompleteGeocoderResult)e.Result; // Update the status label text, and color - green for Complete, red for anything else labelStatus.Text = result.Status.ToString(); labelStatus.BackColor = (result.Status == SearchResult.SearchCompleted) ? Color.LightGreen : Color.Tomato; // Add suggestions to ListBox listBoxResults.Items.AddRange(result.Suggestions); listBoxResults.SelectedIndex = -1; // If there's more work waiting, kick it off on the background thread if (!geocodeWorker.IsBusy && nextSearch != null) { geocodeWorker.RunWorkerAsync(nextSearch); nextSearch = null; } }
The Status property indicates how the search concluded, and indicates whether the output provided should be considered comprehensive. Values are taken from the SearchResult enumeration. Those which are relevant for the AutocompleteGeocoder are:
So, we can now enter search strings in our TextBox, and see suggestions appear in our ListBox. The final step is to let us select suggestions from the ListBox and see them displayed on the map. We will use BalloonPushPin objects for this, and adjust the map zoom and center to appropriately display each type of suggestion. Populate your listBoxResults_SelectedIndexChanged handler with the following code:
private void listBoxResults_SelectedIndexChanged(object sender, EventArgs e) { // Get the selected search result. AutocompleteGeocoderSuggestion suggest = (AutocompleteGeocoderSuggestion)listBoxResults.SelectedItem; // Results will usually have a location, although some region results only have a BoundingBox. We will // place our pin at Location if available, BoundingBox centroid if not. LatLon loc = suggest.Location != LatLon.Empty ? suggest.Location : suggest.BoundingBox.Center; // Check what type of result this is. string resultType; if (suggest.ResultType == AutocompleteGeocoder.ResultType.Region) { resultType = "Region Result"; } else if (suggest.ResultType == AutocompleteGeocoder.ResultType.PostCode) { resultType = "Postcode Result"; } else if (suggest.ResultType == AutocompleteGeocoder.ResultType.Street) { resultType = "Street Result"; } else { resultType = "Unknown Result"; } // Place a marker on the map containing the location's details. BalloonPushPin bpp = new BalloonPushPin(resultType, loc); bpp.Information = string.Join("\n", AddressFormatter.Default.GetLines(suggest.Address, string.Empty)); mapMain.Renderer = bpp; // Zoom to the BoundingBox provided by the region or to the street level of the location. if (suggest.BoundingBox != null) { mapMain.ZoomToBoundingBox(suggest.BoundingBox, 20); } else { mapMain.Center = suggest.Location; mapMain.Zoom = ZoomLevel.StreetLevel; } mapMain.Invalidate(); }
Run your application. Start typing an address from the regions available in your USA data files. You should see the ListBox populate with suggestions, and the Label text/color change to indicate search status. Click on a suggestion to see it displayed on the map.
using System; using System.ComponentModel; using System.Drawing; using System.Threading; using System.Windows.Forms; using Telogis.GeoBase; using Telogis.GeoBase.Geocoding; using Telogis.GeoBase.Repositories; using Telogis.GeoBase.Addresses; namespace AutocompleteGeocoderExample { public partial class Form1 : Form { private BackgroundWorker geocodeWorker = new BackgroundWorker(); private AutocompleteGeocoderArgs nextSearch = null; private const int SEARCH_TIMEOUT_SECS = 2; public Form1() { InitializeComponent(); // Set the map data to point at data containing ACGC data. SimpleRepository repository = new SimpleRepository(@"C:\Program Files (x86)\Telogis\GeoBase\bin\HERE_USAWestUSA_15Q4_30x151208-1141_CaIAZ.gbfs"); Repository.Default = repository; // Set up the BackgroundWorker handlers. geocodeWorker.DoWork += new DoWorkEventHandler(geocodeWorker_DoWork); geocodeWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(geocodeWorker_RunWorkerCompleted); // Ensure the UI has the correct focus on startup. textBoxSearch.Focus(); } void geocodeWorker_DoWork(object sender, DoWorkEventArgs e) { // Set the Autocomplete Geocoder arguments and get the results of the Autocomplete Geocoding operation. AutocompleteGeocoderArgs args = (AutocompleteGeocoderArgs)e.Argument; AutocompleteGeocoderResult result = AutocompleteGeocoder.Geocode(args); e.Result = result; } void geocodeWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { // Clear the ListBox of any old suggestions listBoxResults.Items.Clear(); // Get the result of the Autocomplete Geocoding operation. AutocompleteGeocoderResult result = (AutocompleteGeocoderResult)e.Result; // Update the status label text, and color - green for Complete, red for anything else labelStatus.Text = result.Status.ToString(); labelStatus.BackColor = (result.Status == SearchResult.SearchCompleted) ? Color.LightGreen : Color.Tomato; // Add suggestions to ListBox listBoxResults.Items.AddRange(result.Suggestions); listBoxResults.SelectedIndex = -1; // If there's more work waiting, kick it off on the background thread if (!geocodeWorker.IsBusy && nextSearch != null) { geocodeWorker.RunWorkerAsync(nextSearch); nextSearch = null; } } private void listBoxResults_SelectedIndexChanged(object sender, EventArgs e) { // Get the selected search result. AutocompleteGeocoderSuggestion suggest = (AutocompleteGeocoderSuggestion)listBoxResults.SelectedItem; // Results will usually have a location, although some region results only have a BoundingBox. We will // place our pin at Location if available, BoundingBox centroid if not. LatLon loc = suggest.Location != LatLon.Empty ? suggest.Location : suggest.BoundingBox.Center; // Check what type of result this is. string resultType; if (suggest.ResultType == AutocompleteGeocoder.ResultType.Region) { resultType = "Region Result"; } else if (suggest.ResultType == AutocompleteGeocoder.ResultType.PostCode) { resultType = "Postcode Result"; } else if (suggest.ResultType == AutocompleteGeocoder.ResultType.Street) { resultType = "Street Result"; } else { resultType = "Unknown Result"; } // Place a marker on the map containing the location's details. BalloonPushPin bpp = new BalloonPushPin(resultType, loc); bpp.Information = string.Join("\n", AddressFormatter.Default.GetLines(suggest.Address, string.Empty)); mapMain.Renderer = bpp; // Zoom to the BoundingBox provided by the region or to the street level of the location. if (suggest.BoundingBox != null) { mapMain.ZoomToBoundingBox(suggest.BoundingBox, 20); } else { mapMain.Center = suggest.Location; mapMain.Zoom = ZoomLevel.StreetLevel; } mapMain.Invalidate(); } private void textBoxSearch_TextChanged(object sender, EventArgs e) { // Get the text from the search box. string searchTerm = (sender as TextBox).Text; // Build an AutocompleteGeocoder argument object to pass to the background thread. AutocompleteGeocoderArgs args = new AutocompleteGeocoderArgs { Query = searchTerm, Countries = new Country[] { Country.USA }, LocationHint = this.mapMain.Center, Timeout = TimeSpan.FromSeconds(SEARCH_TIMEOUT_SECS) }; if (!geocodeWorker.IsBusy && nextSearch == null) { // If background worker isn't running at present, and there is nothing // scheduled to run next, run straight away. geocodeWorker.RunWorkerAsync(args); } else { // Otherwise, set to run next. This will replace // anything previously scheduled to run next. nextSearch = args; } } } }