Thursday, November 01, 2007

Centering an AIR application window on startup

Ah. Centered window. Mmmmmmm....

This was a bit tricky. I'll take you through it in several steps. Please note that I am running Flex Builder 3 (whatever build was available from labs.adobe.com on 10/30/07), so if you are using a later build Adobe may have fixed this "feature".

Step 1: Referencing the user's screen resolution


You'll need to include a little sum'm sum'm:
import flash.system.Capabilities;


This will allow you to reference the user's native screen dimensions, thus:
Capabilities.screenResolutionX
Capabilities.screenResolutionY

Step 2: Referencing the application's screen position


For this we use the NativeWindow class. For detailed info, see the Flex 3 documentation.
stage.nativeWindow.x
stage.nativeWindow.y

Step 3: Creation (ostensibly) complete


Here's where the real trick is. If you're like me and you wanted to have this "centering" deal happen when the app starts, you probably thought, "I'll just put it in a creationComplete handler and everything will swim." Then you probably got this error:

Cannot access a property or method of a null object reference.

I hit my head against the AS3 wall for a while and brandished my googling skills for a while before I came across Raghu's blog that explained all about why this error crops up. I'll let you read his words for full explanation and just put the fix here.

You need an include:
import mx.managers.SystemManager

You also need to use systemManager in your reference to the stage:
systemManager.stage.nativeWindow.x = ...

So the final code in your creationComplete handler might look like this:
systemManager.stage.nativeWindow.x = Capabilities.screenResolutionX/2-(yourApplicationWidth/2);
systemManager.stage.nativeWindow.y = Capabilities.screenResolutionY/2-(yourApplicationHeight/2);


Hope that makes sense and works for you.

Monday, April 02, 2007

Walkthrough

This tutorial will walk you through creating a simple Apollo application using the extension for Flex Builder. The application will pull data about a user's geotagged photos from Flickr and save the data in a .kml file that can be read by Google Earth. (If you're new to ActionScript 3, Flex, or Apollo and this sounds complicated, don't worry. You'll be surprized at how simple it actually is.) If you want to download the sample application and project archive, they are available here:

Sample application: right-click and select Save As
(requires the Apollo runtime, available here)

Project archive (.zip)

TOC

Installation
Creating the Project
Laying out the user interface
Setting up the HTTPService
Searching by user name
Retrieving photo data
Converting to kml
Saving the File
Testing the application
Deployment

Installation

If you don't have Apollo or Flex Builder, you can download them here:

Adobe Flex Builder 2

Apollo extensions for Flex Builder


Creating the Project

After installing Flex Builder and the Apollo extensions, go to File > New > Apollo Project. The new project dialog will open. Leave Basic selected and click Next. Give the project a name. I decided to call mine FlickEarth. At this point you can click Finish, or click Next to specify main project source folder, etc. I went with the defaults.

When the project creation is complete, you should see a new Apollo project folder icon in the Navigator pane that contains a bin folder and two files: WhateverYouNamedYourApp.mxml, and WhateverYouNamedYourApp-app.xml. The mxml file is pretty standard - just a stub:

<?xml version="1.0" encoding="utf-8"?>
<mx:ApolloApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">

</mx:ApolloApplication>



We'll come back to this in a minute when we start coding the application. The first thing we need to do is set up the -app.xml file. Open it and find the properties tag. You can set the name, description, publisher, and copyright info here, if you want to. This information will be displayed during the install dialog when your finished application is being deployed. Lynda.com has some video tutorials on further customization of this file, including how to appropriately set up an application icon. When you've finished, save and switch back to the mxml file.

Laying out the user interface

The mxml code is pretty simple. We'll start the layout by setting the dimensions of the ApolloApplication component:
<mx:ApolloApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="400" height="144" showEffect="fadeIn" horizontalScrollPolicy="off" verticalScrollPolicy="off">

Next, we'll layout the controls:
<mx:Label text="FlickEarth" verticalCenter="0" horizontalCenter="0" id="splash_lbl"/>
<mx:Form defaultButton="{search_btn}" width="398" y="0" height="88">
<mx:FormItem label="Flickr User:">
<mx:TextInput id="userName_txt" width="230"/>
</mx:FormItem>
<mx:FormItem>
<mx:Button label="Search" id="search_btn" click="findUser()" visible="false"/>
</mx:FormItem>
</mx:Form>
<mx:Button label="Legal" cornerRadius="0" click="Alert.show('This product uses the Flickr API but is not endorsed or certified by Flickr.');" fontSize="9" width="39" height="20" right="10" bottom="10"/>
<mx:Text x="10" y="76" text="" id="status_txt" width="331" height="50"/>
</mx:Canvas>


I've put in findUser() for the click event handler of search_btn. We'll write that function in a minute. You might have noticed that search_btn's visible property is set to false. This is because I originally had no Form component enclosing the input textbox and button. I wanted the user to be able to just hit enter when the textbox had focus and have the Search button's click event be dispatched. Using a form, though, allows you to specify a default_button in the Form component which will essentially dispatch the specified control's click event when the enter/return key is pressed while any of the FormItem controls contained in the form has focus. The default_button property is simply set to the desired component's id.

Setting up the HTTPService

Now that we have our visual components layed out, we can start pulling data from Flickr. The first thing we need is a way to call the web services exposed by the Flickr API. To do this, we add an HTTPService component. Note that this is a non-visual component and must be added in the Code view in Flex Builder.
<mx:HTTPService id="srv" useProxy="false" result="resultHandler(event)" fault="faultHandler(event)" resultFormat="e4x"/>

Now we need to add a Script component where we will put all our ActionScript code:
<mx:Script>
<![CDATA[
import mx.controls.Alert;

private var resultXML:XMLList;

private function resultHandler(event:Event):void {
//store data in xml list
resultXML = new XMLList(srv.lastResult);
}

private function faultHandler(event:Event):void {
//hide the album and display error message
Alert.show("Error contacting flickr.com");
}

]]>
</mx:Script>

In our AS code block, we've added functions to handle the HTTPService's result and fault events. When the service's send method is called, one of these two events will be dispatched, depending on the result of send operation. The resultHandler function stores the data to a local XML object for further use. The faultHandler function simply displays an Alert. This is sort of a catch-all error message, since the fault event could be caused by several things (for example flickr not responding, or no active internet connection).

So our HTTPService is set up and ready to use. Let's now add the code to envoke the service's send method. First, let's set up a few preliminaries. Add the following to your Script block:
import mx.rpc.events.ResultEvent;

private var originalUserName:String;
private var parsedUserName:String;
private var api_key:String = "Here you should paste in your own Flickr API key"; //You can get it by going to http://www.flickr.com/services/api/
private var queryString:String;
private var user_id:String;

Next, add a function to call the HTTPService. This is a bit abstracted for modularity. You need to compose the querystring and pass it in. Documentation for the Flickr API can be found here.
private function callService(queryString:String):void {
//set the url and call the send method
srv.url = "http://api.flickr.com/services/rest/?" + queryString;
srv.send();
}

Searching by user name

Finally, we add the findUser() function mentioned earlier, and a helper function.
private function parseUserName(name:String):String {
//replace all spaces with plus signs and return edited string
return name.replace(/ /g, '+');
}

private function findUser():void {
originalUserName = userName_txt.text;
parsedUserName = parseUserName(userName_txt.text);
status_txt.text = "Searching flickr.com for " + originalUserName;
queryString = "api_key=" + api_key + "&method=flickr.people.findByUsername&username=" + parsedUserName;
srv.addEventListener(ResultEvent.RESULT, foundUser);
callService(queryString);
userName_txt.setFocus(); //Save the user a keystroke/mouseclick
}

The Flickr API is pretty simple and straightforward. The URL is contained in the callService function. All calls to the API are composed of a querystring appended to the URL. The querystring will contain the method being called, the (necessary) API key, and any additional parameters.

If the Flickr user name has spaces in it, the API explorer on Flickr's site substitutes the + character for spaces. I found that it works just as well if we don't follow suit, but for good form and in case they later require it, I add the parser function to do the substitution. For those unfamiliar with regular expressions, the first parameter passed to the 'replace' function is a regex composed of the pattern being matched (/ /) - in this case a single space and a flag (g) which indicates that all instances of the pattern found should be treated. Leaving off the flag will result in only the first instance being treated.

You probably noticed that a foundUser function is mentioned in the addEventListener parameters. Let's write that:
private function foundUser(event:Event):void {
if (resultXML.@stat == "ok") {
user_id = resultXML.user.@id;
srv.removeEventListener(ResultEvent.RESULT, foundUser);
status_txt.text = originalUserName + " found. Searching for geotagged photos...";
getPhotos();
}
else {
//Our Princess is in another castle
status_txt.text = originalUserName + " not found.";
}
}

You can probably begin to see a pattern here. After the user enters a name and hits enter/return, we call the service. If it fails, we error out. If it succeeds, we move on to the next step. If that fails, we error out, or move on if it succeeds. We'll continue this way through to the end of the process. At this point we hopefully have the user_id of the Flickr user specified by our application's user.

Since the HTTPService relies on an HTTP request, chances are that the results will not be returned before our application is ready for them. Using the event listeners allows us to wait for the results and continue execution upon receiving them. Since we are using the same HTTPService object each time, we need to remove the old event listener and add the new one so that the appropriate handler function is called each time.

Retrieving photo data

Next step is to write the getPhotos() function:
private function getPhotos():void {
//get photos: flickr.people.getPublicPhotos
queryString = "api_key=" + api_key + "&method=flickr.people.getPublicPhotos&user_id=" + user_id + "&extras=geo&per_page=500";
srv.addEventListener(ResultEvent.RESULT, findTaggedPhotos);
callService(queryString);
}

We're setting the optional per_page parameter here to get the maximum possible photos. We could write the program to go through page by page, appending each time, but since this is just a simple example, I'll leave that as the proverbial exercise for the reader. We've also set the optional extras parameter so that the photo data will come back with latitude and longitude we can use to create our .kml file with later on. extras takes a comma delimited list, but we're just requesting the geo data for this application.

So we've requested the user's photo data, let's add the function that handles the results:
private function findTaggedPhotos(event:Event):void {
//clean up
srv.removeEventListener(ResultEvent.RESULT, findTaggedPhotos);
if (resultXML.@stat == "ok") {
//for each photo if lat/long data is included, numTaggedPhotos++
//else remove node from resultXML
for (var index:Number = 0; index < resultXML.photos.photo.length(); index++) {
if (resultXML.photos.photo[index].@latitude != 0) {
numTaggedPhotos++;
status_txt.text = originalUserName + " found. Searching for geotagged photos... " + numTaggedPhotos;
}
else {
delete resultXML.photos.photo[index];
index--;
}
}

//now we have a list of only the geotagged photos from this user...
//let the user know how many items were in the list and that we're now converting the data to kml format
if (numTaggedPhotos > 0) {
status_txt.text = numTaggedPhotos + " geotagged photos found for " + originalUserName + ". Converting to .kml";
convertToKML();
}
else {
status_txt.text = "No geotagged photos found for " + originalUserName;
}
}
else {
Alert.show("Flickr error code: " + resultXML.@stat + " Module: findTaggedPhotos");
}
}


Now that we ostensibly have the user's photo data things start getting interesting. On receiving valid photo data, we go through and remove all photo nodes that haven't been geotagged. There is the possibility that the user has tagged photos exactly on the Prime Meridian. To get around this, we could use Flickr's getWithGeoData method. Again, this is left as an exercise to the reader (read: yeah, neither of us wants to go to the trouble).

You may notice that we are changing our loop counter variable inside the loop when we delete. This is because deleting an XML node means that the next node immediately has the index the deleted node had. Since the counter variable will increment on the next loop iteration, we need to adjust for that.

Converting to KML

We're now down to a bit of data manipulation. This part is boring, but necessary. If you want to skip it and go on to the next section which covers file i/o, feel free. Here, we take the geo data and build the contents of the .kml file.

First, we need an XML object to store the kml data in (note that this is placed outside the Script block):
<mx:XMLListCollection id="kml"/>



And now a few more small items of business to run the show:
private var numTaggedPhotos:Number = 0;
private var xmlHeader:String = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
private var kmlWrapper:String = "<Folder> <name>FlickEarth</name> <visibility>0</visibility> <description>Output from FlickEarth: a sample Apollo app.</description><styleUrl>#noDrivingDirections</styleUrl>";


And finally the conversion function:
private function convertToKML():void {
var add:XML;
for (var index:Number = 0; index < resultXML.photos.photo.length(); index++) {
add = <Placemark />;

add.name = String(resultXML.photos.photo[index].@title);
add.description = "";
add.description = XML("<![CDATA[<img src = 'http://farm1.static.flickr.com/" + resultXML.photos.photo[index].@server + "/" + resultXML.photos.photo[index].@id + "_" + resultXML.photos.photo[index].@secret + "_m.jpg' /> ]" + "]>");
add.Point.coordinates = resultXML.photos.photo[index].@longitude + "," + resultXML.photos.photo[index].@latitude + "," + resultXML.photos.photo[index].@accuracy;

kml.addItem(add);
}
//save data out to the desktop
writeFile();
}

When written out to the file, this kml content can be handled by Google Earth. It does not currently work in Google Maps, since Maps doesn't support folders in the kml, although according to their help center, they plan on adding support for folders soon. If you view the raw XML data, you will notice that the CDATA block does not actually make it into the XML, and the HTML characters (<, >, &, etc.) get HTML encoded. This is not a problem for usability, only for readability. The XML object also appears to strip out the \ escape character. Again, not a problem, but if anyone figures out how to input HTML characters in an XML object, please feel free to comment. Now we're ready to take the final product and write it out to a file.

Saving the file

Let's add some objects to handle the file i/o:
public var saveFile:File;
public var stream:FileStream = null;


And then the writeFile() function:
private function writeFile():void {
//create the save file
saveFile = File.desktopDirectory.resolve( "flickr_photos.kml" );
stream = new FileStream();
//open the file and write the data out
stream.open( saveFile, FileMode.WRITE );
stream.writeMultiByte( xmlHeader + kmlWrapper + "" + kml.toXMLString() + "", File.systemCharset );
stream.close();

//clean up for the next go-round
saveFile = null;
stream = null;
status_txt.text = numTaggedPhotos + " geotagged photos found for " + originalUserName + ". .kml saved to Desktop.";
numTaggedPhotos = 0;
}


Note that using the desktopDirectory places the file on the user's desktop, regardless of which OS they are running. The Apollo runtime takes care of deciding what the actual path on the local volume is.

Testing

Now we're ready for testing the application. Save the file (if you haven't already). Go to Run > Run YourApplicationName. A window should open staged with a proscenium of the default system chrome. Enter a Flickr user name into the text input and hit enter/return. Assuming you have a working internet connection, and assuming that the user exists on Flickr, and assuming that the user has at least one geotagged photo, you should soon have a file named flickr_photos.kml sitting on your desktop. Open this with Google Earth, and you should see a placemark in the correct location for each of the user's geotagged photos. Clicking on a placemark should open up a pop-up in Earth that will pull the appropriate photo directly from Flickr, as per the HTML written in the .kml file. When you're done, close Earth and the running Apollo application.

Deploying

Now that we have the finished application, we are ready to deploy it, that is to export it as an installable .air file. Note that to install the .air file, users (including you) will need to have the Apollo runtime installed. The runtime can be downloaded here.

Go to File > Export. Expand the Apollo tree node and select Deployable AIR file. Click Next and select the project you wish to deploy (for me, FlickEarth). You will need to click on the -app.xml file. Click Next and specify the directory you want Flex Builder to create the .air file in. If the filename isn't already specified, give it one (for example MyApplication.air). Click Finish. Flex Builder will compile your project into an installable .air file which you can distribute to users. After installing the runtime, double click on the .air file to install it.

The last thing you'll want to be aware of is that if you're placing the .air file on a web server for distribution via download, you'll need to set the mime type on the server so that the user's browser knows how to handle it. Mike Chambers has a post on his blog that covers this quite nicely. If you're not sure how to set the mime type, contact your server admin.

Since Apollo is currently in its alpha release there are still a few undocumented features running around. I typically use OSX for my Flex development, and I noticed that while the .air file installs and runs fine (assuming that you leave the box checked which specifies running the application upon completion of the install) that it doesn't show up in my Applications folder. There may be other bugs that I haven't noticed yet in this particular example project, but overall Adobe seems to have done a pretty decent job with the first release of Apollo.

If you have any comments or questions, feel free to post them. I'll try to respond in a somewhat timely manner.

Labels:

Thursday, March 29, 2007

Hello, World

For my first Apollo (alpha 1) application, I've decided to write a small, simple app that will allow the user to search by username for geotagged images on flickr, convert the geo data to .kml, and save the kml file out to the local file system for use in Google Earth.

I chose to start with this application for several reasons. First, I've been playing around with the Flickr API a bit and wanted more experience. Second, it was a good excuse to spend time in Apollo and get used to that. Third, mashups and 2.0 seem to be what everybody won't shut up about, so I decided to go with the time-honored "If you can't beat em..."

I'll be posting a rundown of the development process later, including a link to download the sample application and source code.

Thursday, June 22, 2006

Fin

The Flex Developer Derby Showcase has been updated to include quite a few more entries, including the Terra Captus demo and a streaming multimedia management console some friends of mine were working on. Unfortunately, the Adobe folks are taking a bit longer than expected to announce the results. I'm on pins and needles to see how it turns out. My favorite so far (aside from my own entry): the barcode scanner.

Unfortunately, Flash Player 8.5 is required to view a few of the entries, including the two mentioned above. Since the public release of Player 9 beta Adobe is no longer allowing 8.5 to be downloaded.

In light of this I am working on migrating the demo to FlexBuilder 2 beta 3 so that it will run on Player 9. In moving to beta 3 there were a few things that broke, but if you'd like to check it out you can see the work-in-progress version of the demo here:

http://www.bugg.it/terrac/TerraCaptus/main.html

In other news, I'm currently working on an application that will render XML content using Flex 2 components. The XML file is parsed and components created and added to a canvas on the fly. So far it is working well with the exception of the Image control, which doesn't seem to want to load the external image file. Ah, the delights of being a code monkey.

More later.

Wednesday, May 31, 2006

Bomb's Away!

I just finished and submitted my entry for the Flex Developer Derby. You can check it out here:

http://www.bugg.it/terrac/TerraNovaAlpha.html

You will need Flash Player 8.5 installed to view it. (For some reason apps created in FlexBuilder 2 Beta 2 require 8.5 and those created in Beta 3 require 9.)

I think a little background info would be appropriate here:

When FlexBuilder 2 Beta 1 was released I was working with a Flash 8/Flash Media Server-based streaming video/audio application designed for use by college faculty to allow them record media which they could later embed in their online course pages. The group I was working with decided to migrate the application to Flex after seeing how much better it was suited to developing RIAs than Flash.

To be brief, during the time when we were working on the streaming video project I had the idea to do a separate project on my own. I thought that creating a multiplayer online game might be challenging and rewarding. Unfortunately, I had the idea only a short while before the contest deadline, so as of the time I submitted it the game was still in demo form.

The application has a three-layer structure (presentation, business logic, data access). The top two layers are essentially complete and will only need a bit of tweaking once the data access layer is finalized and connected to a MySQL back-end. For now, the data access object has a set of arrays that it is storing data in. Each instance of the application is independent in the demo version - no multi-player action until the DB and a few web services are finished.

The game controls are accessable in the Flex app, as well as a link to an external kml file that allows the player to view the current layout of the game in Google Earth.

The estimated release date will be sometime in August 2006. I'll probably do a limited beta test release to work out any bugs in the full version before then as well. Subscribe to this blog's feed to keep posted on updates and news. I'll also be discussing some of the ins and outs of FlexBuilder and ActionScript 3.

Monday, May 29, 2006

I'm Game

This is to announce the (possibly) first Flex 2-based MMO. The working title is "Terra Captus" - take over the earth. I'll be entering a demo of the game in the Flex Developer Derby which ends... tomorrow. Wow. Time flies; I'd better get back to coding.

Monday, May 01, 2006

Welcome

...to the Flex Developer blog.

I'll be posting my commentary on new things in the Flex world here, as well as code samples, tutorials, and news about applications I'm developing including a Flex-based game I have in the works. Feel free to comment and provide feedback to my posts, post snippets or discuss problems/solutions.

Wow. I'm really excited for Adobe's upcoming release of Flex 2.0 and ActionScript 3.0. It looks like they're really taking a serious punch at making all those Flash app programmer-types out there happy.

More than that, though; Flex 2 will likely make quite an impact in the RIA area. Microsoft and the open source community have both pushed .net and PHP which have been widely used and well supported. After playing around with the Beta version of Flex Builder 2 I can see that Adobe is not just aiming to make development easier, but really trying to cater to the needs of rich internet application developers.

The next version of ActionScript is no small step forward either. The online documentation keeps improving, and it is readily apparent that a lot of careful planning has gone into the class library. These classes look like a good foundation for OO application development.

Kudos to Adobe. I look forward to using Flex extensively in the future.