Requirements

Prerequisite knowledge
General familiarity with Flex and ActionScript 3.0
 
User level
Intermediate
Required products
Flash Builder (Download trial)
Sample files
googlemaps_api.zip (15 KB)
The Google Maps API for Flash is a recently launched API that offers Flex developers the opportunity to easily integrate highly interactive maps in their Flex applications, and to take advantage of Flex for fast vector graphics, animation, video, socket connections, and more. This article will cover the basics of creating a map using the Google Maps API for Flash, then move on to integrating the API-provided driving directions with Flex UI components. The Maps API classes used in the code are Map, Marker, Directions, and Polyline. The Flex UI components used in the application are HBox, HDividedBox, Label, TextInput, Button, DataGrid. This article also discusses the use of itemRenderers for custom display in the DataGrid, and the use of ArrayCollection for dynamically populating the DataGrid.
 

 
Creating the map

To begin, get a basic "Hello World" map compiling and running. Read through this article, follow the steps, and come back here when you've got the map running. At the end of those steps, you should have a map that looks like the one at this URL.
 

 
Creating the UI

Next, you'll add the Flex components needed so that the user can enter the to/from addresses, and then see the results displayed next to the map. Follow these steps:
 
  1. Add some additional layout containers within the existing Panel.
    Start by adding a VBox above the Map component. This VBox will soon contain the directions form and output elements, laid out one after another from top to bottom. Then surround both that VBox and the Map component with an HDividedBox. This will enable users easily to drag a divider to resize either the map or the directions info, depending on their needs. The MXML for that is shown below, and a screenshot of the result in Design mode is shown as Figure 1:
     
<mx:HDividedBox width="100%" height="100%"> <mx:VBox width="100%" height="100%"> </mx:VBox> <maps:Map id="mapContainer" width="100%" height="100%" mapevent_mapready="onMapReady(event)"/> </mx:HDividedBox>
Figure 1. Additional layout containers
Figure 1. Additional layout containers
 
  1. Add the directions form components.
    First add labels and text inputs for the users to enter the to and from addresses, and give them id attributes so you can reference them programmatically later. To make sure that a label is on the same line as its corresponding text input, surround the pairs with HBox components. To make testing the app easier, give the text inputs default values of "San Francisco CA" and "Mountain View CA". Finally, add a Button component that says "Get Directions". The MXML for all that is shown below, and a screenshot of the result in Design mode is shown as Figure 2:
     
<mx:HBox> <mx:Label text="From: " width="70"/> <mx:TextInput id="from" text="San Francisco, CA" width="100%"/> </mx:HBox> <mx:HBox> <mx:Label text="To: " width="70"/> <mx:TextInput id="to" text="Mountain View, CA" width="100%"/> </mx:HBox> <mx:Button label="Get Directions"/>
Figure 2. Add the directions form components
Figure 2. Add the directions form components
 
  1. Connect the button's click event with a function in the code.
    Add a click attribute to the component that calls a function called processForm, like this:
     
click="processForm(event);"
Then, inside the script section, define the processFrom function to just trace the value of the text attributes for the from and to components. For example, loading the app and just clicking the button without changing of the values would output "San Francisco CA Mountain View CA" to our Flash log. The code for that looks like this:
 
private function processForm(event:Event):void { trace(from.text + " " + to.text); }
  1. Add the directions output components.
    Start by adding an HRule below the Button component to visually separate the form from the output. Then add two text components with id attributes. These will be used to later display the summary and copyright information from the directions query. Finally, add a DataGrid component between the two text components. This will be used to later display the directions steps. Give the DataGrid an id attribute and set its sortableColumns property to false, since users won't want to sort the steps in any way besides sequential. By using a DataGrid instead of just a Text component with HTML inside, you'll be able to assign handlers to click events for individual steps and specify custom formatting for each column. The MXML for that is shown below, and a screenshot of the result in Design mode is shown as Figure 3:
     
<mx:Text id="directionsSummary" width="100%"/> <mx:DataGrid id="directionsGrid" dataProvider="{directionsSteps}" width="100%" height="100%" sortableColumns="false" /> <mx:Text id="directionsCopyright" width="100%"/>
Figure 3. Add the directions output components
Figure 3. Add the directions output components
 
  1. Connect the directions grid to a data provider.
    There are various ways of dynamically populating a DataGrid. For this example, you'll be using an ArrayCollection object, adding and clearing items from it as needed. Start by declaring and initializing a global variable called directionsSteps of type ArrayCollection. Mark that variable as [Bindable], so that the UI will automatically reflect any changes in the variable.
     
[Bindable] public var directionsSteps:ArrayCollection = new ArrayCollection();
Then add a dataProvider attribute to the existing DataGrid and set it equal to "{directionsSteps}". The curly braces notation denotes that the UI component is being connected to a variable.
 
dataProvider="{directionsSteps}"
At the end of the above five steps, you should have a map like this.
 
Nothing terribly exciting will happen yet, because you haven't added the directions logic into the code; but at least it looks good.
 

 
Loading the Directions query

In this section, you'll issue the directions query to the Google servers and load the polyline result to verify that it was successful. Follow these steps:
 
  1. Issue the directions query.
    In the processForm function that's called when the user presses the button, declare a Directions object (from the Maps API), and then add event listeners to it for both the success and failure events. To actually send off the query to the Google servers, create a string of the form "from: A to: B", where A and B are the values the user entered in the text inputs, and pass that into the load function of the Directions object. The code for that looks like:
     
var directions:Directions = new Directions(); directions.addEventListener(DirectionsEvent.DIRECTIONS_SUCCESS, onDirectionsSuccess); directions.addEventListener(DirectionsEvent.DIRECTIONS_FAILURE, onDirectionsFail); directions.load("from: " + from.text + " to: " + to.text);
  1. Handle a failed query.
    Not all directions queries are successful. Among other reasons, a query may fail due to the Google servers not recognizing the addresses sent in or not being able to compute the directions between two addresses (for example, "From: California To: Australia"). When a query does fail, a status code is sent indicating the type of failure. The full list of status codes and explanations is found in the reference. In the onDirectionsFail function, you'll show an alert to the user with the status code. In a production-level application, it would be better to show a more user-friendly message to the user, but I've chosen to show the status code here to make debugging easier. The code for that looks like this:
     
Alert.show("Status:" + event.directions.status);
  1. Handle a successful query.
    When a query is successful, it will send back information in the directions property of the event that you can use to create markers, polylines, or info windows. For quick visual proof that a query worked, add a polyline to the map that represents the entire query. In the onDirectionsSuccess function, start by calling clearOverlays on the map to get rid of the poly from the previous query, and then create the polyline from the directions object and add it to the map, like this:
     
map.clearOverlays(); var directions:Directions = event.directions; var directionsPolyline:IPolyline = directions.createPolyline(); map.addOverlay(directionsPolyline);
Now, you have a UI issue. The polyline has been created and added to the map, but unless the directions query was only about addresses in the starting viewport, the user will have to pan and zoom to actually see the complete polyline. It would be better if the map automatically zoomed to fit any arbitrary route. To accomplish that, you'll first find out the bounds of the polyline; then set the center of the map to the center of the polyline bounds; and then use the map.getBoundsZoomLevel helper function to find the optimal zoom level that fits the whole poly bounds. The code for that looks like this:
 
var directionsBounds:LatLngBounds = directionsPolyline.getLatLngBounds(); map.setCenter(directionsBounds.getCenter()); map.setZoom(map.getBoundsZoomLevel(directionsBounds));
At the end of the above three steps, you will have a map like the one at this URL.
 
When you press "Get Directions", the map will display a polyline of the route. Try different address queries to see the results (and try one that should fail, like "from:aggu baggu to:ishy bishy".
 

 
Adding directions steps and markers

In this section, you'll parse the directions object and extract information to create markers and steps.
 
  1. Add directions markers.
    Parsing the results of a driving directions query is actually a little trickier than you'd expect, because the actual query can be more complex than you'd think. The query can be the typical A-to-B ("from: here to: there") or it can actually contain multiple waypoints ("from: here via: other place to: there"). Because of the possibility of waypoints, a driving directions result contains one or more Route objects, and each route contains one or more Step objects. So, to create the "start" and the "end" markers, you actually must find the coordinates of the first step of the first route, and then the coordinates of the last step of the last route, and create markers from those coordinates. The code for that looks like this:
     
var startLatLng:LatLng = dir.getRoute(0).getStep(0).latLng; var endLatLng:LatLng = dir.getRoute(directions.numRoutes-1).endLatLng; map.addOverlay(new Marker(startLatLng)); map.addOverlay(new Marker(endLatLng));
  1. Add directions steps.
    Now that you've got the markers added, you just need to add the steps to the DataGrid. As you'll recall, when you set up the DataGrid, you bound it to an ArrayCollection variable. So, to update the DataGrid display, you just need to modify the contents of that ArrayCollection. Create a for loop to iterate through all of the routes, and then a nested for loop to iterate through the steps of each route, adding each step object as an item in the ArrayCollection. The DataGrid will default to assuming each key in an object is a column name, and then create columns for all the keys it finds (like descriptionHtml, distanceHtml, and so on). The code for that nested for loop looks like this:
     
for (var r:Number = 0 ; r < directions.numRoutes; r++ ) { var route:Route = directions.getRoute(r); for (var s:Number = 0 ; s < route.numSteps; s++ ) { var step:Step = route.getStep(s); directionsSteps.addItem(step); } }
Now that you have the steps displaying after a query, you need to clear them out between queries. You can do that by removing all the items from the ArrayCollection at the beginning of the onDirectionsSuccess function, like so:
 
directionsSteps.removeAll();
  1. Stylize the directions grid.
    The DataGrid defaults to displaying its data as plain text. Since the distance and description values sent in were actually HTML-formatted, the HTML tags are being displayed in the grid; you want to find a way to get the HTML rendered properly instead. To accomplish that, you can explicitly define each column in the DataGrid, and then provide something called an itemRenderer to provide special rendering for the distance and description columns. To define the columns in the DataGrid, create an array of DataGridColumn components as a child of the DataGrid component, set the dataField attributes to the Step object properties, the width attributes to appropriate values, and headerText to a nice-sounding column name. You can prevent some of the data in the step object (such as the latLng property) from being displayed by simply not creating a DataGridColumn for that data. The MXML for the columns looks like this:
     
<mx:columns> <mx:Array> <mx:DataGridColumn headerText="Description" dataField="descriptionHtml" itemRenderer="{HTMLRenderer}"/> <mx:DataGridColumn headerText="Distance" dataField="distanceHtml" width="120" itemRenderer="{HTMLRenderer}"/> </mx:Array> </mx:columns>
Now for the itemRenderers. There are various types of itemRenderers, depending on the level of flexibility and reusability desired. Since you want the description and distance columns to both be rendered the same way (as HTML text), create a reusable inline itemRenderer that can be referenced by both of those columns. To do that, start by adding a generic Component as a child of Application (not of Panel—only visual children are allowed there), and give it an id of "HTMLRenderer" so that you can reference it from the DataGrid. Inside that component, create a Text component with 100% width. An itemRenderer can either access all of the data for the row in the DataGrid by referencing the data variable (for example, data.Description), or it can access just the data for that column, by referencing the listData variable. Since you want to just take whatever the column value is and render it as HTML, set the htmlText attribute of the Text component to {listData.label}. The MXML for the component looks like this:
 
<mx:Component id="HTMLRenderer"> <mx:Text width="100%" htmlText="{listData.label}"/> </mx:Component>
To connect the two columns to this itemRenderer, just set the itemRenderer attribute of the DatGridColumn to {HTMLRenderer}, like this:
 
<mx:DataGridColumn dataField="Description" itemRenderer="{HTMLRenderer}"/> <mx:DataGridColumn dataField="Distance" width="120" itemRenderer="{HTMLRenderer}"/>
  1. Add click events to the grid.
    Now, show the user the location of each step on the map by popping up an info window when they click on it in the grid. To start off, add an itemClick attribute to the DataGrid in the MXML, and set it to call the onGridClick function, like this:
itemClick="onGridClick(event)"
Next, define the onGridClick function. Inside that function, you'll use various properties of the Step object for the selected DataGrid row, and use them to pop up an infowindow at the step's coordinate and with the step description inside it. The full code for the function looks like this:
 
privatefunction onGridClick(event:Event):void { var latLng:LatLng = directionsGrid.selectedItem.latLng; var opts:InfoWindowOptions = new InfoWindowOptions(); opts.contentHTML = directionsGrid.selectedItem.descriptionHtml; map.openInfoWindow(latLng, opts); }
At the end of those 4 steps, you should have the full directions app functionality working. When you submit a query, you should see the markers and polyline displayed on the map, the steps nicely rendered as HTML in the DataGrid, and then infowindows pop up on the map when you click on a row, as in the map at this URL.
 

 
Where to go from here

Now that you've got this working, there are various ways you can enhance it:
 
  • Give your users more customization options for directions queries, like avoiding highways or walking instead of driving. You can do this by adding additional Flex UI components (such as ComboBox, CheckBox) and passing different values into DirectionsOptions.
  • Let your users specify more than two locations. You could do this by giving the user a TextInput that adds additional rows to a DataGrid, and then passing in the array of addresses to the loadFromWaypoints method.
  • Integrate directions into an existing map with markers on it by letting users click a marker and specify that they want directions to/from that marker (as is done in http://maps.google.com). You could do this by putting a TextInput and Button into the infowindow, and then triggering the appearance of the pre-filled directions panel once the button is submitted.