This week I saw a very exciting announcement that soon we can use Java on Google App Engine. It was the number 1 ticket in the Google App Engine request tracker, for which I voted as well. The third howto of four is about my FlexPicasa application, with a Flex client on Google App Engine using old fashioned Python. This howto being totally unrelated to the world famous
“How to dismantle an atomic bomb”, or Java, will cover the following new topics:
- Python Imaging Library (PIL) used by GAE Images API
- Google Maps API for Flash
- Flex Effects
- Custom tooltips
I decided that I should do something with images and geotagging. It seemed a good idea to use the related Google API’s from Google Maps and Picasa. I called the application I made FlexPicasa. It has two search modes – normal and location based. In location based search a map is displayed, using the geographical information found in the images metadata. I created an experimental user interface, where the following keybindings were defined – h/l scrolls through thumbnails of found images, j/k scrolls up and down, r resizes the image, f flips/rotates the image, b bookmark.
FlexPicasa demo video ( logo displayed from shareware product ).
Table of contents
- 1 Flex client
- 1.1 The core FlexPicasa application
- 2 Requesting image information
- 2.1 Requesting Picasa photos by community search
- 2.2 Requesting Picasa photos with added geographical information
- 3 Image manipulation
- 3.1 Resizing images
- 3.2 Flipping images
- 4 Odds and ends
- 4.1 The main Python script
- 4.2 Custom tooltip
- 5 Resources
- 6 Conclusion
1 Flex client
Below, I have shown with the tree command, the directory layout of a portion of my FlexPicasa project. On the right, you can see associations with related Python scripts on Google App Engine.
|-- FlexPicasa.as |-- FlexPicasa.mxml |-- FlipForm.mxml ---- photoHandler.py |-- ResizeForm.mxml ---- photoHandler.py |-- net | `-- ivanidris | `-- flexpicasa | |-- PhotoEntry.as | |-- PhotoEntryCollection.as | |-- PicasaLocationRequest.as ---- geofeed.py | |-- PicasaRequest.as ---- feed.py | `-- Request.as `-- picasa.css
FlexPicasa.mxml in Listing 1 declares the layout of user interface of the core application. FlexPicasa.as has the majority of the application logic of FlexPicasa. The net.ivanidris.flexpicasa package holds several Actionscript classes. The MXML files FlipForm.mxml and ResizeForm.mxml declare a number of form windows.
1.1 The core FlexPicasa application
Figure 1 and b exhibit the GUI of FlexPicasa and layout. The user can switch between normal search mode and location based search mode with a TabNavigator. One of the tabs as shown in Figure 1 b contains a panel called searchPanel for normal search, and the other tab contains a panel called geosearchPanel for geographical search. Each panel has three compartments. The first one holds a text box where the user can enter keywords or a location to search for. The second compartment holds a horizontal array of thumbnails. The third and last container displays the currently selected image in its original size.
The class diagram of Figure 1 c shows, that the FlexPicasa, Request, PicasaRequest, PicasaLocationRequest, PhotoEntryCollection and Counter classes are associated. PhotoEntryCollection is covered in 1.3.
Listing 1: FlexPicasa.mxml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | <?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:maps="com.google.maps.*" layout="absolute" viewSourceURL="srcview/index.html" creationComplete="initApp();"> <mx:Script source="FlexPicasa.as"/> <mx:Style source="picasa.css"/> <mx:Fade id="fadeOut" alphaFrom="1.0" alphaTo="0.0"/> <mx:Fade id="fadeIn" alphaFrom="0.0" alphaTo="1.0"/> <mx:Panel id="mainPanel" title="Flex Picasa. Right click to view source." width="100%" height="100%"> <maps:Map xmlns:maps="com.google.maps.*" key="ABQIAAAAZIsc68RyX-pQIow87QfZIRRaMQQVWkEGPGMMnHpN-jUWCtrSchTEpyAAX3DWTQE-OrrhGz6daBmOLg" showEffect="{fadeIn}" hideEffect="{fadeOut}" id="map" mapevent_mapready="onMapReady(event)" width="1" height="1" visible="false"/> <mx:Text id="helpText" fontSize="12"> <mx:text>h/l scroll through images, j/k scroll up and down, r resize, f flip/rotate image, b bookmark</mx:text> </mx:Text> <mx:TabNavigator id="tabNavigator" width="100%" height="100%" change="onTabChange();"> <mx:Panel id="searchPanel" label="Search" width="100%" height="100%"> <mx:HBox> <mx:TextInput id="q" enter="search();" toolTip=" " toolTipCreate="net.ivanidris.flex.CustomToolTipFactory.createPanelToolTip('Image Search', 'Fill in keywords for image search', event);"/> <mx:Button id="searchButton" label="Search" click="search();" toolTip=" " toolTipCreate="net.ivanidris.flex.CustomToolTipFactory.createPanelToolTip('Image Search', 'Image Search', event);" buttonMode="true"/> </mx:HBox> <mx:HBox id="thumbnails"/> <mx:VBox> <mx:ProgressBar id="progress" width="100%" source="bigimage" visible="false" complete="progress.visible = false;"/> <mx:Image id="bigimage" ioError="bigimage.errorString = event.text;"/> </mx:VBox> </mx:Panel> <mx:Panel id="geosearchPanel" label="Geosearch" width="100%" height="100%"> <mx:HBox> <mx:TextInput id="l" enter="geosearch();" text="europe" toolTip=" " toolTipCreate="net.ivanidris.flex.CustomToolTipFactory.createPanelToolTip('Image Search', 'Fill in location keywords for image search', event);"/> <mx:Button id="geosearchButton" label="Location Search" click="geosearch();" toolTip=" " toolTipCreate="net.ivanidris.flex.CustomToolTipFactory.createPanelToolTip('Location Search', 'Location based image search', event);" buttonMode="true"/> </mx:HBox> <mx:HBox id="geothumbnails"/> <mx:VBox> <mx:ProgressBar id="geoprogress" width="100%" source="geobigimage" visible="false" complete="geoprogress.visible = false;"/> <mx:Image id="geobigimage" ioError="geobigimage.errorString = event.text;"/> </mx:VBox> </mx:Panel> </mx:TabNavigator> </mx:Panel> </mx:Application> |
I would like to draw your attention to the Map tag with maps namespace. The main panel contains a map, which is initially invisible, because its visible property is set to false. The map also has width and height of 1 initially so that it does not take up any space. A key is also defined in the Map tag. You need to sign up for a key as it depends on your domain name. My key will not work for you!!! Two Fade effects are defined with opposite alphaFrom and alphaTo values. These effects are defined as the hideEffect and showEffect of the map. It all boils down to fading in and fading out of the map, when it is hidden and displayed. The Script tag includes FlexPicasa.as of Listing 2, that has the majority of the Actionscript logic. When the loading is done the function initApp in FlexPicasa.as is executed. This method initializes the application.
Listing 2: FlexPicasa.as
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | import com.google.maps.LatLng; import com.google.maps.MapEvent; import com.google.maps.MapType; import flash.events.Event; import flash.events.KeyboardEvent; import net.ivanidris.flex.Counter; import net.ivanidris.flexpicasa.PicasaLocationRequest; import net.ivanidris.flexpicasa.PicasaRequest; private var request; public function initApp():void { addEventListener(Event.ACTIVATE, getFocusBack); addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); } public function onKeyDown(e:KeyboardEvent):void { if(this.request) { this.request.onKeyDown(e); } } public function getFocusBack(e:Event):void { if(this.request) { this.request.getFocusBack(e); } } public function search():void { var counter:Counter = new Counter(0); this.request = new PicasaRequest(this, counter); this.request.initGrid(); this.request.getImages(); } public function geosearch():void { var counter:Counter = new Counter(0); this.request = new PicasaLocationRequest(this, counter); this.request.initGrid(); this.request.getImages(); } private function onMapReady(event:MapEvent):void { map.setCenter(new LatLng(37.4419, -122.1419), 8, MapType.NORMAL_MAP_TYPE); } private function onTabChange():void { if(tabNavigator.selectedIndex == 1) { map.visible = true; map.height = 150; map.width = 200; } else{ map.visible = false; map.height = 1; map.width = 1; } } |
At the top you can see imports for com.google.maps from the Google Maps API for Flash. In Listing 1 an event handler is declared for the “map ready” event. onMapReady initializes the map with the setCenter method of Map – the main class of the Google Maps Flash API. The first argument is a LatLng object, which holds the latitude and longitude coordinates of the center. The second argument is the zoom level of the map. The third argument is the type of the map. The onTabChange function is assigned to the change property of the TabNavigator in Listing 1. When the second tab is selected the map is displayed and the map dimensions are adjusted. Otherwise, the map is hidden and width and height are set to 1. The search and geosearch functions perform similar tasks, but use different subclasses of Request – PicasaRequest and PicasaLocationRequest respectively. More about these classes in the next section.