FlexPicasa on Google App Engine

This entry is part of 3 in the series Flex on GAE

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.

Series Navigation
0saves
If you enjoyed this post, please consider leaving a comment or subscribing to the RSS feed to have future articles delivered to your feed reader.
Share
This entry was posted in programming and tagged , . Bookmark the permalink.

One Response to FlexPicasa on Google App Engine

  1. Pingback: Video | Enjolt.com | Innovate for Success

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">