devxlogo

Create a Map Client with Web Services, Part II

Create a Map Client with Web Services, Part II

Part 1 of this series started a simple project to show how you can use Adobe Flash Builder 4 to rapidly develop a SOAP-powered map application called TerraClient. Using the new data service capabilities available in Flash Builder, you bound to MSR Map’s web service and generated a rudimentary search form to utilize it. Now, you’re going to dig deeper and use multiple asynchronous SOAP requests to display the tiled map imagery.

By the end of this article, you’ll have a rudimentary map client and-more importantly-a solid understanding of how you can use web services with Flash Builder. Figure 1 shows what the finished product looks like.

Figure 1. Palo Alto, California, as viewed inTerraClient.

As with part 1, you will need Flash Builder version 4 build 2 or later to follow along with the coding steps in this article. You will also need the project source code from where we left off at the conclusion of part 1. If you don’t have it, download terraclient_part1.fxp and perform the following steps to import the project into Flash Builder:

    1. Launch Flash Builder, and then click File > Import.
    2. Select Flash Builder Project as the Import Source, and then click Next.
    3. Select Import Project, and then browse to the location of terraclient_part1.fxp.
    4. Edit the Extract new project(s) to path, and remove the trailing _part1 from the project folder name. When you’re done, the project path should end with erraclient instead of erraclient_part1.
    5. Click Finish.

Before you start coding again, let’s take a look at a new tool in Flash Builder that is extremely useful when using SOAP services.

Step 1: Use the Network Monitor

It is practically a law of nature that when troubleshooting web services you’ll need to see the raw Hypertext Transfer Protocol (HTTP) request and response messages at some point. Adobe has introduced the new Network Monitor tool inside Flash Builder so you can see exactly what’s going back and forth between your project and the server.

If it isn’t already visible, click Window > Network Monitor. At the top right of the window is the toolbar shown in Figure 2. Use these buttons to enable or disable the monitor, pause monitoring, clear the window, and save the monitoring results to an Extensible Markup Language (XML) file.

Figure 2. The Network Monitor window.

To see the monitor in action, perform the following steps:
    1. Click Enable Monitor.
    2. Run the TerraClient application in Debug mode by clicking Run > Debug TerraClient.
    3. When the TerraClient page loads in your browser, enter a search term in the form, and then click Get Placelist.
As the requests and responses go between TerraClient and the MSR Maps web service, you’ll be able to view them in Network Monitor. You can switch between a raw view, a tree view (shown in Figure 2), and a hexadecimal notation view of the messages.

You won’t be specifically using the Network Monitor from here on out. However, if you get stuck and need to see exactly what’s going back and forth from the web service, the tool will certainly come in handy. The MSR Maps web services can be slow at times, so you can also use the monitor to see how long each response takes.

Step 2: Finish the Infrastructure

Before jumping into the code, you must identify all of the web service calls needed to retrieve map data for one of the search results. Figure 3 is a Unified Modeling Language (UML) sequence diagram that visualizes what will happen.

Figure 3. UML sequence of the web service calls made to MSR Maps TerraService

As shown in Figure 3, you’ve already handled the initial GetPlaceList call and the response of PlaceFacts. When you select one of those PlaceFacts, you have several more calls to make to assemble the map tiles related to the selection.

First, use the LonLatPt coordinates of the PlaceFacts to make a GetTileMetaFromLonLatPt request. The TileMeta response gives you the necessary data to then make a series of GetTile calls. The map display will be composed of nine tiles, so make nine GetTile requests. The response from the TerraService to each call will be an image transmitted as a byte array. The final GetTheme call will provide information on the type and source of the imagery.

Add new web service operations

In part 1, step 2, you connected to the TerraService Web Services Description Language (WSDL) file and bound to one of the web service operations called GetPlaceList. Now, add the operations shown in Figure 3 by performing the following steps:

1. On the Data/Services tab, right-click the TerraService2 node, then click Properties.
2. In the Web Service Properties window, select the GetTileMetaFromLonLatPt, GetTile, and GetTheme web service operations.
3. Click Finish.
See also  Custom Java Web Development - The Heartbeat of Modern Web Development

Flash Builder creates objects to represent the new operations and request/response object data types. You can take a look at the results in the Project Explorer (look in /src/server) or on the Data/Services tab.Now that the new service objects are available, it is time to start using them to assemble the actual map in TerraClient.

Complete the SearchForm class

The SearchForm class in src/client/SearchForm.mxml displays the search result place names, but (so far) selecting one of the results doesn’t do anything yet. You’ll make it so that selecting a result invokes a handler function set by another class.Declare a new property inside SearchForm’s script block to hold the handler, and add a placeList_clickHandler function used to invoke it:

/** Handler for when a PlaceFacts object is selected. */[Bindable]public var select:Function;/** * Invoke select handler when a PlaceFacts object is selected. *  * @param event  Click event from placeList.  */protected function placeList_clickHandler(event:MouseEvent):void{	if(select!=null)	{		select.call(null, placeList.selectedItem);	}}

To wire the handler to search result selection, add click="placeList_clickHandler(event)" to the search results List object. When you're done, it should look like this:

				

This wraps up the SearchForm. Now comes the heart of TerraClient, where the map images are retrieved and displayed.

Step 3: Get the Big Picture

Thus far, the PlaceMap class in src/client/PlaceMap.mxml has been untouched. It is a subclass of the Group class and will hold nine map tile Image objects loaded from the web service. Note the 3×3 grid specified in the TileLayout definition:

                                                         

Note: A caveat before we continue: Putting service call code directly in SearchForm and PlaceMap has been done for the sake of keeping the TerraClient project simple. In a real project, I highly recommend that you follow good Model-View-Controller (MVC) practices, which would likely involve moving all of the service-related code to non-visual controller classes and keeping the view classes as "dumb" as possible.

Take what you learned from the generated code in Part 1 and create some of your own variations in PlaceMap.mxml to get the work done. For starters, add a declaration for the TerraService2 proxy in the Declarations section:

 

You may recall that when you put SearchForm.mxml together, the generated search form also put a CallResponder instance in the declarations. You'll be using CallResponder instances in PlaceMap as well, but you'll create them programmatically as needed within Script block functions, instead.There are a number of bound properties to add to PlaceMap. These properties will be referenced in various places, so the simplest way forward is just to add them all now. Put these imports and properties in your Script block:

The inline comments for each property are self-explanatory, but the theme and scale properties require some extra explanation. MSR Maps uses the term theme to refer to the imagery type available in a given location. The term scale refers to the pixel-to-meter ratio of the images themselves. For example, a Scale16m image has a resolution in which each pixel covers 16 m2 on the ground. The TerraService documentation iterates the valid values for each property, most of which are used in /src/client/ImageOptions.mxml.PlaceMap needs to be initialized with empty Image objects before any service calls are made. Add this function to the Script block below the newly added properties:

/** * Initialize the map grid with new Image objects. * Set the remaining counter to keep track of which * ones have been loaded. */protected function initMapGrid():void{	if(this.numElements<9)	{		for(var i:int=0;i<10;i++)		{			var img:Image = new Image();			img.width=200;			img.height=200;			this.addElement(img);		}	}	remaining = 9;}

The empty Images are pre-sized to take up the same space as the map tile bitmaps that will be loaded. This behavior allows the tiles to be loaded nonsequentially into the correct position on the screen. With that taken care of, the real work begins.

If you take a look back at Figure 3, you'll see that you need to take the user's selection and request tile metadata for that location. You recently added a select handler function on SearchForm that would be invoked; here is where that function will be declared. (You'll assign it to the SearchForm in the TerraClient class later.) Add this to the Script block in PlaceMap.mxml:

/** * Show the specified place by building a grid of  * 9 map tile images centered on the PlaceFacts location. *  * @param place   The PlaceFacts object used build the map. */public function showPlace(place:PlaceFacts):void{   if(place!=null)   {      getTileMeta(place.Center);   }}

As you can see, the function simply uses the selected PlaceFacts' centerpoint LonLatPt to invoke another function called getTileMeta(). Add the code for that function and the two event handler functions it will use:

/** * Request the TileMeta object for a given location. *  * @param point   The location used to center and build the map. */protected function getTileMeta(point:LonLatPt):void{   status = "Getting MetaData...";   var callback:CallResponder = new CallResponder();   callback.addEventListener(ResultEvent.RESULT, tileMetaResult);   callback.addEventListener(FaultEvent.FAULT, tileMetaError);   callback.token = terraService2.GetTileMetaFromLonLatPt(point, theme, scale);}/** * Handle an error from a getTileMeta() request. *  * @param event   The error from the service. */protected function tileMetaError(event:FaultEvent):void{	status = "Sorry, imagery not available with these options.";	trace(event);}protected function tileMetaResult(event:ResultEvent):void{   // TODO}

When you examine the getTileMeta() function, you'll see that you're using a CallResponder to invoke the service proxy and handle the asynchronous response. The CallResponder is listening for a ResultEvent or a FaultEvent from the web service proxy. If the web service responds as expected, the proxy will generate a ResultEvent, which will invoke your tileMetaResult() function. A FaultEvent will invoke the tileMetaError(), instead.

The last line in getTileMeta()containing the callback.token assignment is where the GetTileMetaFromLonLatPt operation on the service is actually called. It is important to note that this is a non-blocking call; the getTileMeta() function will exit without waiting for a response. When the service finally replies with a good response or an error, the CallResponder's event listeners will invoke the corresponding handlers.

Requesting the map tiles and theme

Now, fill in the tileMetaResult() handler. Referring back to the sequence diagram in Figure 3, this function uses the TileMeta of the selected search result to coordinate calls to the web service's GetTile and GetTheme operations. Enter the following code:

/** * Handle the result of a getTileMeta() request. * Generate 9 map tile requests using the TileMeta. *  * @param event   The result from the service. */protected function tileMetaResult(event:ResultEvent):void{	// Sanity check the result	var tileMeta:TileMeta = event.result as TileMeta;		if(!tileMeta.TileExists)	{		status = "Sorry, imagery not available with these options.";		return;	}		// Update the currentTile and request the imagery theme info	currentTile = tileMeta;	getTheme(tileMeta.Id.Theme); // TODO		// Initialize the grid with blank images.	initMapGrid(); 		// Use the index to determine where each tile needs to be	// placed in the grid.	var index:int = 0;		// Use the TileMeta center point and derive the surrounding	// tile IDs with simple offsets.  Initiate a request for	// each of the 9 tiles.	for (var y:int = (tileMeta.Id.Y+1); y >= (tileMeta.Id.Y-1); y--) 	{		for (var x:int = (tileMeta.Id.X-1); x <= (tileMeta.Id.X+1); x++) 		{				// Derive the tile id			var tid:TileId = new TileId();									tid.X = x;			tid.Y = y;				tid.Scene = tileMeta.Id.Scene;			tid.Theme = tileMeta.Id.Theme;			tid.Scale = tileMeta.Id.Scale;						// Request the tile for the tile id			getTile(tid, index); // TODO			index++;		}					}}

Take a look at the nested for loops in the second half of tileMetaResult(). MSR Maps uses a planar grid (x,y) system for the tiles, which makes it easy to start with a center TileMeta object and use offsets to determine its neighbors. The for loops are a simple way to derive those offsets, which are ±1 of the center's x and y properties.

As marked by the TODO comments, you still need to write functions for getTheme() and getTile(). Start with getTheme():

/**  * Request the copyright/description information  * for the current imagery type (theme).  *   * @param themeType  The type code used by TerraService.  */ protected function getTheme(themeType:int):void {			    if(currentTheme == null || currentTheme.Theme != themeType)    {       var callback:CallResponder = new CallResponder();       callback.addEventListener(ResultEvent.RESULT,           function(event:ResultEvent):void          {             currentTheme = event.result as ThemeInfo;             status = currentTheme.Description;          });       callback.addEventListener(FaultEvent.FAULT,           function(event:FaultEvent):void          {             status = event.fault.faultString;          });       callback.token = terraService2.GetTheme(themeType);    } }

The basic mechanics of getTheme() are the same as getTileMeta(), but the CallResponder uses closures rather than declared functions to handle the ResultEvent or FaultEvent. If you're new to Adobe ActionScript coding and not familiar with the closures language feature, it is worth your time to do some research and get to know them. The topic of when to use closures is way out of scope for this article, but getTheme() is one example of how you can use them.

You'll use a combination of closures and declared functions to implement getTile(). Because you're doing nine asynchronous calls to the GetTile web service operation, they won't necessarily come back in the same order you requested them. As you add the code for getTile(), closures are a perfect way to keep each response associated with the map tile position you're trying to fill:

/**  * Request a map tile based on a TileId, providing  * the index of where the tile should be placed.  *   * @param tid   The TileId specifying the tile image needed.  * @param index Index of the image in the grid to load.  */ protected function getTile(tid:TileId, index:int):void {			    status = "Requesting Tile " + (index+1) + " of 9 ...";    var callback:CallResponder = new CallResponder();    callback.addEventListener(ResultEvent.RESULT,        function(event:ResultEvent):void       {          tileResult(event, index); // TODO       });    callback.addEventListener(FaultEvent.FAULT,        function(event:FaultEvent):void       {          tileError(event, index); // TODO       });    callback.token = terraService2.GetTile(tid); } 

So, even though getTile() will be called in sequence by the loop in tileMetaResult(), the responses coming back from the web service may be out of order. Your tileResult() and tileError() handlers (marked TODO in the above) therefore need to have the corresponding map index passed to them by the closures. This lets you load the tile images in whatever order they arrive.

Displaying the images

The payload of the ResultEvent coming back from the GetTile service operation is a ByteArray of bitmap image data. Adobe Flex provides a Loader object that you can use to inject that bitmap data into an Image object. Loader operations are non-blocking, so you need to use an event listener with it, as well. This map feels like overkill for a small map tile, but asynchronous loading is necessary when you're dealing with large images or low bandwidth. Here is the tileResult() function that uses the Loader to display the map images:

/** * Handle the result of a getTile() request, loading the * resulting bytes into an image.  The index indicates * which image in the grid should be used.  Decrement * the 'remaining' counter as each image is loaded. *  * @param event   The result from the service. * @param index   Index of the image in the grid to load. */protected function tileResult(event:ResultEvent, index:int):void{	status = "Processing Tile " + (index+1) + " of 9 ...";	var bytes:ByteArray = event.result as ByteArray;		// Get the empty image object matching the index position.	var tileImage:Image = this.getChildAt(index) as Image;		// Use a loader to asynchronously build a bitmap from the byte array	var loader:Loader = new Loader();			loader.contentLoaderInfo.addEventListener(Event.COMPLETE, 		function(event:Event):void		{				// Load the bitmap data into the image object			tileImage.source = Bitmap(loader.content);							// If the last image tile is done, show the theme			remaining--;			if(remaining<1 && currentTheme!=null) 			{				status = currentTheme.Description;			}		});	loader.loadBytes(bytes);}	You also need a simple tileError() function if the GetTile web service operation fails for some reason:/** * Handle an error from a getTile() request. *  * @param event   The error from the service. * @param index   Index of the image in the grid. */protected function tileError(event:FaultEvent, index:int):void{	var tileImage:Image = this.getChildAt(index) as Image;	tileImage.toolTip = "Error";	tileImage.errorString = event.fault.faultString;}

You're essentially done with PlaceMap and very nearly done with TerraClient. Before you close this file, add a reload function that you can use to force all the map images to be reloaded from the web service:

/** * Reload the map based on the currentTile and * the theme/scale settings. *  * @param An event used to trigger the reload.  Ignored. */public function reload(event:Event):void{	if(currentTile!=null)	{		getTileMeta(currentTile.Center);	}}

Step 4: Tie it Together, Try it Out

Now that the search result selection handler is in place on SearchForm and the PlaceMap class completed, you must tie them together in the main TerraClient class. Open /src/client/TerraClient.mxml to put the finishing touches on the project.

First, put an instance of PlaceMap into the TerraClient class. When you add the instance, also bind the theme and scale properties to the selected values from the ImageOptions instance. At the bottom of the file, replace the comment below it with the following code:

Next, locate the SearchForm instance already in place. Assign its new select handler property to the PlaceMap's showplace() function:

Now, whenever you click a search result, the PlaceFacts object selected will be passed to the PlaceMap class, kicking off the series of web service requests needed to display the map tiles.

Finally, locate the ImageOptions instance. Assign the text property to the status property on the PlaceMap, and assign the change handler property to the reload() function on PlaceMap:

The first addition displays the PlaceMap's status messages above the map tiles by ImageOptions. The second addition means that any change to the drop-down menus on ImageOptions for scale or theme will automatically reload the map tiles on PlaceMap.

And that's the last step in completing the project. Now, try it out. Launch the application by clicking Run > Run TerraClient. To see some examples of the different imagery types and scales available, here are some sample searches you can try:

* "San Diego Bay, California" / Aerial Photograph / 8 meters (shown in Figure 4)
* "Pentagon AHP" / Hi-Res (Limited) / 8 meters
* "Crater Lake, Oregon" / Topographic Map / 32 meters

Figure 4. San Diego Bay as viewed in TerraClient

Wrapping Up

You can still make a number of improvements to TerraClient, ranging from clickable maps to more robust state control, but the basics are in place. This project was intended to demonstrate how binding to web services with Flash Builder's data service tools can be a tremendous time saver. Form generation can be quite valuable as well, whether as an end product or as a way to see how the pieces can all fit together.

If you want to double-check your work or simply download the finished product, you can get the completed source code of TerraClient here: terraclient_part2.fxp.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist