24 February 2006
Intermediate
Maps appear to be all the rage on the web these days. Google and Yahoo! seem to battle each other for features on an almost daily basis. But online maps have one shortcoming—you can't take them with you. This is where Adobe AIR comes in. Using your existing web development skills and Adobe AIR, you can take an HTML-based web applications to the desktop and integrate it with standard desktop functionality, including local file IO, drag-and-drop and clipboard support, and more. The following video and this article demonstrate how you can integrate web-based and desktop technologies by way of an Adobe AIR sample app that brings Yahoo! Maps to the desktop (see Figure 1).
Watch Kevin Hoyt demo and explain his sample app, MapCache on Adobe AIR.
Note: This idea of taking maps to the desktop isn't new. The inspiration for this article comes from Christian Cantrell's MapCache Adobe AIR application, built using Flex. Note also that this article uses the Yahoo! Maps API, as Google Maps expect to be delivered into a specific website domain. Since an Adobe AIR application runs on the desktop, there is no domain, and Google Maps simple doesn't work.
The first step in getting a Yahoo! Maps application running in Adobe AIR is to build an HTML page that uses the API. You can build this first part in the browser using your favorite development and debugging tools. The basic Yahoo! Map is assembled by providing a DIV for the map, including the API script block, and then instantiating a map instance. There's a number of additional methods you can call to further customize the look and feel of the map.
<script src="http://api.maps.yahoo.com/ajaxymap?v=3.4&appid=YOUR_MAP_ID" type="text/javascript"></script>
<script type="text/javascript">
function doLoad()
{
map = new YMap( document.getElementById( 'map' ) );
map.addTypeControl();
map.addPanControl();
map.addZoomShort();
map.drawZoomAndCenter( HOME_GEO, 8 );
}
</script>
...
<div id="mapholder" class="box">
<div id="map"></div>
</div>
Although at this point you're still authoring for the browser, don't forget to provide user interface controls for the various operations the application will provide when on the desktop. For this application you'll want to have buttons for save, copy, and drag. These buttons will invoke saving an image of the map to disk, placing a copy of an image of the map on the clipboard, and providing a means to drag an image of the map to the desktop (or other application).
Although Adobe AIR provides a full browser implementation via WebKit, there's a distinct difference between what JavaScript should be allowed in the browser, and what should be allowed on the desktop. Keeping in mind that Adobe AIR provides native file IO, you can imagine something like eval( air.File.resolvePath( 'reallyimportant.sys' ).deleteFile() ) doing irreparable damage to an operating system. In order to address this type of scenario, Adobe AIR provides a number of additional security features that go above and beyond what the browser offers.
Although this article isn't going to address security within Adobe AIR at length, it's important to know that there are two security sandboxes: The application sandbox and the non-application sandbox. The non-application sandbox operates just like the browser, and doesn't give access the Adobe AIR APIs. The application sandbox has full access to the Adobe AIR APIs, but places certain restrictions on the JavaScript that can be executed. Some of these restrictions directly impact the Yahoo! Maps API.
In order for this application to function in Adobe AIR under the security model, the maps user interface (UI) will need to run in the non-application sandbox. This manifests itself as a distinct HTML page and related assets. This map UI will be hosted in an IFRAME that sits in another HTML page, which is essentially the application root. The respective pieces of native integration you'll need to provide also reside in the root HTML page.
<html>
<head>
<title>MapCache</title>
<link href="root.css" type="text/css" rel="stylesheet" />
<script src="lib/air/AIRAliases.js" type="text/javascript"></script>
<script src="lib/root.js" type="text/javascript"></script>
</head>
<body onLoad="doLoad();">
<iframe
id="viewport"
src="ui.html"
sandboxRoot="http://SomeRemoteDomain.com/"
documentRoot="app:/"
/>
</body>
</html>
Through a security feature called the bridge, you can allow the assets in the non-application sandbox to call functions in the application sandbox. The functions that can be accessed through the bridge are generally defined when the root HTML document finishes loading, although this isn't a rule. Per the previously described actions, you'll want a function for saving, copying to the clipboard, and dragging outside of the application. You can stub these in for now, and using a load event handler, create and expose the functions to the non-application sandbox.
function doLoad()
{
var
exposed = new Object();
exposed.doSaveClick = doSaveClick;
exposed.doCopyClick = doCopyClick;
exposed.doDragMove = doDragMove;
document.getElementById( 'viewport' ).contentWindow.parentSandboxBridge = exposed;
file = air.File.desktopDirectory.resolvePath( DEFAULT_NAME );
file.addEventListener( air.Event.SELECT, doSaveSelect );
}
At this point, even with no further functionality, it is now possible to run this application using Adobe AIR. In order for the runtime to know what assets to use, you'll need to create an application descriptor file. The application descriptor file is an XML file that tells the runtime what the main HTML file is, what the name of the application is, the initial size of the application window, and more.
<?xml version="1.0" encoding="utf-8" ?>
<application xmlns="http://ns.adobe.com/air/application/1.0">
<id>com.adobe.hoyt.MapCache</id>
<name>MapCache</name>
<version>1.0</version>
<filename>MapCache</filename>
<description>This is a port of Christian Cantrell's MapSnap/MapCache application from Flex to HTML. Various UI tweaks and bahvior modifications have been made to avoid confusion.</description>
<initialWindow>
<title>MapCache</title>
<content>root.html</content>
<systemChrome>standard</systemChrome>
<transparent>false</transparent>
<visible>true</visible>
<width>640</width>
<height>480</height>
<minimizable>true</minimizable>
<maximizable>false</maximizable>
<minSize>320 240</minSize>
<maxSize>800 600</maxSize>
</initialWindow>
<icon>
<image16x16>icons/AIRApp_16.png</image16x16>
<image32x32>icons/AIRApp_32.png</image32x32>
<image48x48>icons/AIRApp_48.png</image48x48>
<image128x128>icons/AIRApp_128.png</image128x128>
</icon>
</application>
Once you have this file created, you can run the application using the various tools provided with the Adobe AIR SDK. The ideal testing tool is ADL, which allows you to launch and test your application at any point during development. ADL is a command line executable that takes a single argument—the path to the application descriptor file. Your exact command line will look different depending on your system configuration, but here's a sample to get you pointed in the right direction:
./adl ../../Desktop/MapCache/application.xml
The first part of capturing an image of the map to save it to disk, is in knowing what portion of the screen the map occupies. To make quick work of this task, I like to use the Prototype JavaScript framework. The Prototype Example class contains the getDimensions() method that returns an array with two elements, which contain the width and height. The Position class contains a cumulativeOffset() method that returns the top-left corner position of the specified element as it relates to the entire HTML page.
function getMapRect()
{
var dim = Element.getDimensions( $( 'mapholder' ) );
var pos = Position.cumulativeOffset( $( 'mapholder' ) );
var rect = new Object();
rect.x = pos[0];
rect.y = pos[1];
rect.width = dim.width;
rect.height = dim.height;
return rect;
}
This snippet goes alongside the maps UI HTML page, and is executed in the non-application sandbox. Don't forget to include the appropriate SCRIPT tag! You'll need to handle the click event on the Save button in the non-application sandbox as well. The bridge that maps the application sandbox functions to the non-application sandbox was configured earlier. Now all you need to do is call the appropriate application sandbox function to save the image.
function doSaveClick()
{
parentSandboxBridge.doSaveClick( getMapRect() );
}
Back in the application sandbox, you'll need to capture the pixel data that represents the map. This may seem like a daunting task considering that nothing of the sort exists in JavaScript today. The good news is that when you're working with Adobe AIR, everything that is core to Adobe Flash Player is also available to you through JavaScript. Capturing pixel data is something that's been around in Flash Player for a little while. To use it, you "draw" screen content into a BitmapData object. You can crop at the same time by the information you obtained previously through the Prototype framework.
var bmp = new air.BitmapData( map_rect.width - 2, map_rect.height - 2 );
var matrix = new air.Matrix();
matrix.translate( 0 - ( map_rect.x + 1 ), 0 - ( map_rect.y + 1 ));
bmp.draw( window.htmlLoader, matrix );
Okay, you have the pixel data, now you need to encode it into a usable image format. What? Did I mention that although Adobe AIR can access the core parts of Adobe Flash Player, the player itself doesn't include image encoding utilities? As fortune would have it, however, enterprising community developers have developed just such a library for Flash. This project is called the ActionScript 3 Core Library, and is hosted on Google Code.
To use this library, you open the SWC file, which is in the ZIP format, and extract the main SWF file called library.swf. You can then include this SWF file in your application HTML using a SCRIPT tag. Yup, you just point the tag at the SWF file, and give it a type of application/x-shockwave-flash. At this point, any public functions in the SWF file are accessible via JavaScript, and this includes the one-liner necessary to encode the pixels to PNG.
<script src="lib/core/library.swf" type="application/x-shockwave-flash"></script>
...
png = runtime.com.adobe.images.PNGEncoder.encode( bmp );
Adobe AIR provides low-level file IO features as part of the core API. To save this image data to disk, you'll need to specify where on the disk it should go. This is accomplished with the File class. You can hard-code this, or prompt the user with a Save dialog box and let them choose. When you know where it's going to go, you use the FileStream class to write the actual bytes of the image to disk.
var file = air.File.applicationStorageDirectory.resolvePath( 'map.png' );
stream.open( file, air.FileMode.WRITE );
stream.writeBytes( png, 0, 0 );
stream.close();
The process of encoding the image is something you'll use repeatedly, so you might choose to encapsulate the functionality. Also keep in mind since image encoding and file IO are Adobe AIR features, that this needs to reside in the application sandbox. At first the difference between the two can be confusing, but with a little practice, I think you'll find that it provides a clean separation between your web content and your Adobe AIR business logic.
Copying the image data to the clipboard is very similar to saving the image to disk. The main difference here is that you don't really want to prompt the user for where to save the data. You still need something to put on the clipboard, however, so you might consider saving the image data to the application's storage directory as a placeholder. The Clipboard class provides access to the native OS clipboard through the setData() and getData() methods.
Putting data on the clipboard may seem pretty easy at first, but it isn't without its nuances. Keep in mind that the clipboard may hold numerous data types. This might be some text, a URL, some bitmap data, or a list of files. These different types of data are exposed as constants on the ClipboardFormats class. You also don't know where the user is going to place this data. To maximize usability, consider exposing clipboard data in more than one format such that it has the best opportunity of being reused.
file = air.File.applicationStorageDirectory.resolvePath( DEFAULT_NAME );
encodeImage();
air.Clipboard.generalClipboard.setData(
air.ClipboardFormats.FILE_LIST_FORMAT, new Array( file ) );
Although you might be inclined to think of dragging and dropping as a feature unto itself, it's really little more than a visual clipboard access. To that end, drag-and-drop operations in Adobe AIR utilize many of the clipboard classes. Before you get to that, however, you need to pick up the drag-and-drop operation from the user interface. As it turns out, the Yahoo! Maps map gets repositioned via drag-and-drop, so you'll need another button (or other means) to capture the native drag-and-drop.
There are about as many ways to handle drag-and-drop in JavaScript as there are JavaScript frameworks. Personally, I'm inclined to be a bit old-fashioned about it, and manage the event listeners manually. You might think that this is going to introduce browser compatibility problems. Remember that you're working in Adobe AIR now, which is only WebKit on all platforms. If it works in Safari, you have a pretty solid chance that it'll work in Adobe AIR, too.
function doDragDown()
{
this.src = 'images/' + this.id + '-down.gif';
this.addEventListener( 'mousemove', doDragMove );
}
function doDragMove( e )
{
// Kill events and reset UI
e.stopPropagation();
this.removeEventListener( 'mousemove', doDragMove );
this.src = 'images/' + this.id + '-up.gif';
// Manage image persistance and native drag and drop operation
parentSandboxBridge.doDragMove( getMapRect() );
}
function doDragOut()
{
this.src = 'images/' + this.id + '-up.gif';
this.removeEventListener( 'mousemove', doDragMove );
}
function doDragOver()
{
this.src = 'images/' + this.id + '-over.gif';
}
function doDragUp()
{
this.src = 'images/' + this.id + '-over.gif';
this.removeEventListener( 'mousemove', doDragMove );
}
You can decide how you want to implement drag-and-drop support in JavaScript, but once you've caught the event, you'll want to call back into the application sandbox and initiate it at a native level. On the application sandbox side, you set the data to be dragged on the clipboard first. You should already be familiar with this from the previous exercise. Then, using the Adobe AIR DragManager class, you can start the native operation.
// Put data on clipboard and start native drag operation
clipboard.setData( air.ClipboardFormats.FILE_LIST_FORMAT, new Array( file ) );
air.NativeDragManager.doDrag( window.htmlLoader, clipboard, scale,
new air.Point( 0 - ( scale.width / 2 ), 0 - ( scale.height / 2 ) ) );
}
The NativeDragManager.doDrag() method takes a number of arguments. Mostly it wants to know what started this operation, and what data is going to get passed (the clipboard). Note that the clipboard reference isn't to the general OS clipboard, as it was previously. In this case, you need to establish your own instance of the Clipboard class to use.
One of the real gems of native drag-and-drop support, is that you can specify how that data is visualized during the operation. By default this will just be whatever the OS provides. With a little manipulation on the pixel data of the map, however, you can scale the whole thing down into a thumbnail, and use that as the hint. Again, BitmapData, now paired with some advanced Matrix functionality, makes this process very easy.
// Get the cropped pixels from the display list
crop = new air.BitmapData( map_rect.width - 2, map_rect.height - 2 );
// Get bitmap of map only
matrix = new air.Matrix();
matrix.translate( 0 - ( map_rect.x + 1 ), 0 - ( map_rect.y + 1 ));
crop.draw( window.htmlLoader, matrix );
// Generate thumbnail
ratio = 160 / crop.width;
matrix.scale( ratio, ratio );
scale = new air.BitmapData( crop.width * ratio, crop.height * ratio );
scale.draw( window.htmlLoader, matrix );
This article has walked you through putting a Yahoo! Maps display on the desktop with Adobe AIR. Along the way you extended onto the desktop, providing local file IO, native clipboard support, and native drag-and-drop support. Still, this article has only scratched the surface of these APIs. At the very least, you owe it to yourself to explore the security model and deferred clipboard handling further. Using your existing web development skills, however, you can now easily build and deploy desktop applications.
For more information about Adobe AIR, visit the product pages. For inspiration, check out the sample applications in the Adobe AIR Developer Center for HTML and Ajax, as well as the showcase apps.
For more details on the new security model in Adobe AIR, also check out Lucas Adamski's Introducing the Adobe AIR security model and the Adobe AIR HTML security FAQ.
| 04/11/2012 | Surround sound 5.1 with Air 3.2 on desktop app? |
|---|---|
| 12/12/2011 | Live Streaming H.264 Video on iOS using AIR |
| 04/17/2012 | HTMLLoader - Google Maps? |
| 04/12/2012 | Tabindex in forms on mobile? |