Login | Register   
Twitter
RSS Feed
Download our iPhone app
TODAY'S HEADLINES  |   ARTICLE ARCHIVE  |   FORUMS  |   TIP BANK
Browse DevX
Sign up for e-mail newsletters from DevX


advertisement
 

Create a Map Client with Web Services, Part II : Page 2

This project demonstrates how binding to web services with Flash Builder's data service tools can be a tremendous time saver.


advertisement
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 <!-- Map Display --> <!-- TODO --> comment below it with the following code:

<!-- Map Display --> <client:PlaceMap id="placeMap" theme="{imgOpts.theme}" scale="{imgOpts.scale}" />

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

<client:SearchForm select="{placeMap.showPlace}" cornerRadius="4">

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:

<client:ImageOptions id="imgOpts" text="{placeMap.status}" change="{placeMap.reload}" />

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.


Comment and Contribute

 

 

 

 

 


(Maximum characters: 1200). You have 1200 characters left.

 

 

Sitemap