2 Requesting image information
Request in Figure 1c is the superclass of PicasaRequest and PicasaLocationRequest and serves as template for them. In Java I would have made it abstract. It seems that this is not posssible in Actionscript 3. However, you can define an interface in Actionscript, which I should have done. The refactoring is left as an exercise for the reader
. The onIOError, getFocusBack and onKeyDown methods are implemented in the same way for both normal and geographical search. setStyle is empty and needs to be overridden. In Java this class would have been declared abstract. The instance variables of Request have internal access, which as far as I know is equivalent to protected in Java.
Listing 3: Request
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | package net.ivanidris.flexpicasa { import flash.events.Event; import flash.events.IOErrorEvent; import flash.events.KeyboardEvent; import flash.net.URLLoader; import flash.net.URLRequest; import flash.net.navigateToURL; import mx.core.UITextField; import mx.managers.PopUpManager; import net.ivanidris.flex.Bookmarks; import net.ivanidris.flex.Counter; public class Request { internal var app; internal var counter:Counter; internal var loader:URLLoader; internal var mediaNamespace:Namespace = new Namespace('http://search.yahoo.com/mrss/'); internal var entries:PhotoEntryCollection = new PhotoEntryCollection(); internal var MAX:uint = 10; internal var thePanel; internal var thumbnails; internal var progressBar; public function Request() { } public function onIOError(event:IOErrorEvent):void { trace("ioErrorHandler: " + event); } public function getFocusBack(e:Event):void { setStyle(counter.get()); this.thumbnails.setFocus(); } public function onKeyDown(e:KeyboardEvent):void { if(e.target is UITextField) { return; } var c:String = String.fromCharCode(e.charCode); if(c == 'l' ) { this.progressBar.visible = true; counter.increment(); setStyle(counter.get()); } if(c == 'h' ) { this.progressBar.visible = true; counter.decrement(); setStyle(counter.get()); } if(c == 'j' ) { this.thePanel.verticalScrollPosition += 20; } if(c == 'k' ) { this.thePanel.verticalScrollPosition -= 20; } if(c == 'r' ) { var resizeForm:ResizeForm = ResizeForm(PopUpManager.createPopUp(this.app, ResizeForm, false)); resizeForm.url.text = this.entries.getMediumThumbnailUrl(counter.get()); } if(c == 'o' ) { navigateToURL(new URLRequest( this.entries.getContentUrl( counter.get() ))); } if(c == 'f' ) { var flipForm:FlipForm = FlipForm(PopUpManager.createPopUp(this.app, FlipForm, false)); flipForm.url.text = this.entries.getMediumThumbnailUrl(counter.get()); } if(c == 'b' ) { var bookmarksWindow:Bookmarks = Bookmarks(PopUpManager.createPopUp(this.app, Bookmarks, false)); bookmarksWindow.url.text = this.entries.getContentUrl(counter.get()); bookmarksWindow.t.text = this.entries.getContentUrl(counter.get()); } } public function setStyle(i:uint):void { // to be overridden } } } |
Request stores relevant image info in the PhotoEntryCollection class of Listing 4. PhotoEntryCollection is a wrapper of ArrayCollection, which contains PhotoEntry objects. A PhotoEntry is defined to have three URL properties – the URL of the smallest thumbnail available, the URL of a medium sized thumbnail and the URL of the image itself.
Listing 4: PhotoEntryCollection
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 | package net.ivanidris.flexpicasa { import mx.collections.ArrayCollection; public class PhotoEntryCollection { private var entries = new ArrayCollection(); public function PhotoEntryCollection() { } public function add(entry:PhotoEntry):void { this.entries.addItem(entry); } public function getThumbnailUrl(i:uint):String { if(i >= len()) { return ""; } return this.entries.getItemAt(i).thumbnailUrl; } public function getMediumThumbnailUrl(i:uint):String { if(i >= len()) { return ""; } return this.entries.getItemAt(i).mediumThumbnailUrl; } public function getContentUrl(i:uint):String { if(i >= len()) { return ""; } return this.entries.getItemAt(i).contentUrl; } public function toString():String { var s:String = ""; for(var i:uint; i < this.entries.length; i++) { s += getThumbnailUrl(i); s += 'n'; } return s; } public function clear():void { this.entries.clear(); } public function len():uint { return this.entries.length; } } } |
2.1 Requesting Picasa photos by community search
Picasa allows you to search for photos in private albums, or in public albums also known as community search. FlexPicasa implements community search. By specifying a query and performing a GET request using the appropriate URL, a response will be returned in XML format. More accurately put, the response is an ATOM feed. Fortunately, parsing XML with E4X is child’s play in Actionscript. feed.py in Listing 5 performs the actual community search request.
Listing 5: feed.py
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 | from google.appengine.api import urlfetch from google.appengine.ext import webapp from google.appengine.ext.webapp.util import run_wsgi_app import cgi import urllib class FeedPage(webapp.RequestHandler): def get(self): q = urllib.quote_plus( self.request.get('q') ) url = "http://picasaweb.google.com/data/feed/api/all?max-results=10&q=" + q result = urlfetch.fetch(url) if result.status_code == 200: self.response.headers['Content-Type'] = 'text/xml' self.response.out.write(result.content) application = webapp.WSGIApplication( [('/feed', FeedPage)], debug=True) def main(): run_wsgi_app(application) if __name__ == "__main__": main() |
feed.py is nothing more than a proxy. It is mapped to the /feed URL pattern. All the action occurs in the get method of FeedPage. As you probably have guessed this method processes HTTP GET requests. The response is of XML content type, without any filtering, because it is much easier to do the XML processing with E4X than with any Python XML API, at least the ones that I know. This proxy is used by PicasaRequest in Listing 6.
Listing 6: PicasaRequest.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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | package net.ivanidris.flexpicasa { import flash.events.Event; import flash.events.IOErrorEvent; import flash.events.KeyboardEvent; import flash.net.URLLoader; import flash.net.URLRequest; import mx.controls.Image; import net.ivanidris.flex.Counter; public class PicasaRequest extends Request { private namespace atomns = "http://www.w3.org/2005/Atom"; use namespace atomns; public function PicasaRequest(app, counter:Counter):void { this.app = app; this.counter = counter; this.thePanel = this.app.searchPanel; this.thumbnails = this.app.thumbnails; this.progressBar = this.app.progress; } public function initGrid():void { this.app.thumbnails.setFocus(); for(var i:uint = 0; i < MAX; i++) { var image:Image = new Image(); this.app.thumbnails.addChild(image); } } public function getImages():void { try { this.progressBar.visible = true; var url:String = '/feed?q=' + encodeURI(this.app.q.text); var request:URLRequest = new URLRequest(url); loader = new URLLoader(request); loader.addEventListener(IOErrorEvent.IO_ERROR, onIOError); loader.addEventListener(Event.COMPLETE, onServerResponse); } catch(e:Error) { trace(e.message + e.getStackTrace()); } } public function onServerResponse(e:Event):void { try { this.entries.clear(); var feed:XML = new XML(loader.data); for each (var entry in feed..entry){ var photoEntry:PhotoEntry = new PhotoEntry(); photoEntry.contentUrl = entry.mediaNamespace::group.mediaNamespace::content.@url; photoEntry.thumbnailUrl = entry.mediaNamespace::group.mediaNamespace::thumbnail[0].@url; this.entries.add(photoEntry); = entry.mediaNamespace::group.mediaNamespace::thumbnail[1].@url; } for(var i:uint = 0; i < MAX; i++) { getThumbnail(i).source = this.entries.getThumbnailUrl(i); } setStyle(0); this.counter.setMax(this.entries.len()); } catch(err:Error) { trace(err.message + 'n' + loader.data); } } private function getThumbnail(index:uint):Object { return this.app.thumbnails.getChildAt(index); } public override function setStyle(index:uint):void { var item = getThumbnail(index); item.source = this.entries.getMediumThumbnailUrl(index); var prev:int = counter.prev(); if(prev != -1) { getThumbnail(prev).source = this.entries.getThumbnailUrl(prev); } var next:int = counter.next(); if(next != -1) { getThumbnail(next).source = this.entries.getThumbnailUrl(next); } item.drawFocus(true); this.app.bigimage.source = this.entries.getContentUrl(index); } } } |
Line 14 declares the ATOM feed namespace for the community search feed response. Line 15 is a shortcut that makes it unnecessary to fully qualify references to this namespace, similar to import statements. The getImages function sends the request to feed.py. When the request is complete, onServerResponse is executed. This method does all the fun XML parsing, with E4X. mediaNamespace is the namespace inherited from Request. As you can see in lines 58 – 60, if you do not use the namespace, you need to use the notation with ::. For each image three thumbnails of ascending size are given in the XML response, hence the thumbnail array. From these thumbnails I store the URLs of the smallest thumbnail[0] and medium thumbnail. setStyle should have been called draw or something similar, because that is what this function does, updating the user interface. The current image is shown in a large box. The corresponding thumbnail is shown in medium size, so that it sticks out in comparison to the other thumbnails, which are shown in a smaller size.
2.2 Requesting Picasa photos with added geographical information
The l query parameter allows to search for image data in a specified location. For example, you can set l to Amsterdam. I created a proxy Python script, which seems a bit of a overkill, since as you can see in Listing 7 it is almost the same as feed.py.
Listing 7: geofeed.py
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 | from google.appengine.api import urlfetch from google.appengine.ext import webapp from google.appengine.ext.webapp.util import run_wsgi_app import cgi import urllib class GeoFeedPage(webapp.RequestHandler): def get(self): l = urllib.quote_plus( self.request.get('l') ) url = "http://picasaweb.google.com/data/feed/api/all?max-results=10&l=" + l result = urlfetch.fetch(url) if result.status_code == 200: self.response.headers['Content-Type'] = 'text/xml' self.response.out.write(result.content) application = webapp.WSGIApplication( [('/geofeed', GeoFeedPage)], debug=True) def main(): run_wsgi_app(application) if __name__ == "__main__": main() |
This proxy is used by PicasaLocationRequest.as in Listing 8. This class is a subclass of Request and is fortunately completely different from PicasaRequest.as, so I didn’t overdo the copy pasting
. In the first lines you can already find significant differences – imports for Google Maps classes and special namespace definitions. Although, the ATOM namespace should have been placed in the Request class. Another free refactoring tip
.
Listing 8: PicasaLocationRequest.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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | package net.ivanidris.flexpicasa { import com.google.maps.LatLng; import com.google.maps.LatLngBounds; import com.google.maps.overlays.Marker; import com.google.maps.overlays.MarkerOptions; import flash.events.Event; import flash.events.IOErrorEvent; import flash.net.URLLoader; import flash.net.URLRequest; import mx.controls.Image; import net.ivanidris.flex.Counter; public class PicasaLocationRequest extends Request { private namespace atomns = "http://www.w3.org/2005/Atom"; use namespace atomns; private var georss:Namespace = new Namespace('http://www.georss.org/georss'); private var gml:Namespace = new Namespace('http://www.opengis.net/gml'); private var markers:Array = new Array(); public function PicasaLocationRequest(app, counter:Counter):void { this.app = app; this.counter = counter; this.thePanel = this.app.geosearchPanel; this.thumbnails = this.app.geothumbnails; this.progressBar = this.app.geoprogress; } public function initGrid():void { this.app.geothumbnails.setFocus(); for(var i:uint = 0; i < 10; i++) { var image:Image = new Image(); this.app.geothumbnails.addChild(image); } } public function getImages():void { try { this.progressBar.visible = true; var url:String = '/geofeed?l=' + encodeURI(this.app.l.text); var request:URLRequest = new URLRequest(url); loader = new URLLoader(request); loader.addEventListener(IOErrorEvent.IO_ERROR, onIOError); loader.addEventListener(Event.COMPLETE, onServerResponse); } catch(e:Error) { trace(e.message + e.getStackTrace()); } } public function onServerResponse(e:Event):void { try { this.entries.clear(); var feed:XML = new XML(loader.data); var bounds:LatLngBounds = new LatLngBounds(); for each (var entry in feed..entry){ var photoEntry:PhotoEntry = new PhotoEntry(); photoEntry.contentUrl = entry.mediaNamespace::group.mediaNamespace::content.@url; photoEntry.thumbnailUrl = entry.mediaNamespace::group.mediaNamespace::thumbnail[0].@url; photoEntry.mediumThumbnailUrl = entry.mediaNamespace::group.mediaNamespace::thumbnail[1].@url; this.entries.add(photoEntry); var latlngArray:Array = entry.georss::where.gml::Point.gml::pos.split(" "); var latlng:LatLng = new LatLng(latlngArray[0], latlngArray[1]); bounds.extend(latlng); var marker:Marker = new Marker(latlng, defaultMarkerOptions()); this.markers.push(marker); this.app.map.addOverlay(marker); } var zoomLevel : Number = this.app.map.getBoundsZoomLevel( bounds); if(this.app.map.getZoom() != zoomLevel) { this.app.map.setZoom(zoomLevel); } this.app.map.panTo(bounds.getCenter()); for(var i:uint = 0; i < MAX; i++) { getThumbnail(i).source = this.entries.getThumbnailUrl(i); } setStyle(0); this.counter.setMax(this.entries.len()); } catch(err:Error) { trace(err.message); } } public function defaultMarkerOptions():MarkerOptions { var options:MarkerOptions = MarkerOptions.getDefaultOptions(); options.radius = 4; return options; } private function getThumbnail(index:uint):Object { return this.app.geothumbnails.getChildAt(index); } public override function setStyle(index:uint):void { var item = getThumbnail(index); item.source = this.entries.getMediumThumbnailUrl(index); this.markers[index].setOptions(new MarkerOptions({ fillStyle: { color: 0x223344, alpha: 0.8 },radius: 7})); var prev:int = counter.prev(); if(prev != -1) { getThumbnail(prev).source = this.entries.getThumbnailUrl(prev); this.markers[prev].setOptions(defaultMarkerOptions()); } var next:int = counter.next(); if(next != -1) { getThumbnail(next).source = this.entries.getThumbnailUrl(next); this.markers[next].setOptions(defaultMarkerOptions()); } item.drawFocus(true); this.app.geobigimage.source = this.entries.getContentUrl(index); } } } |
E4X is used here as well to parse the XML data. URLs of the original image, small and medium thumbnail are again stored in PhotoEntry object. For each image the corresponding coordinates, that is latitude and longitude are stored in LatLng objects. A LatLngBounds representing a rectangular box is iteratively extended with the extend method until all the image locations fit inside. Also Markers are created and displayed on the map for each point. The LatLngBounds is then used to set the appropriate zoom level for the map. After that the new center of the map is determined and the map readjusted. The setStyle function also shows the thumbnail of the current image in slightly larger size. In addition, the marker on the map corresponding to the current image gets a different color and larger size.
More From ivanidris
ivanidris Recommends
- Seven Characteristics of Stepper Motors | Solder In The Veins (Solder In The Veins)

Pingback: Video | Enjolt.com | Innovate for Success