Now that Google's Picasa Web Albums (PWA) allows you to map your photos, you can use it as a source for some interesting feeds within GME. To demonstrate this, I created a simple mashup at http://levsplaces.googlemashups.com/—a small viewer for geo-tagged photos in a PWA album. You can go ahead and look at the source code or read on to see how it was built, step-by-step.
I started with an album containing geo-tagged photos of the various places I've visited in the past couple of years: http://picasaweb.google.com/levikcom/Places.
My album is public, and PWA shows a small link to the album's RSS feed in the lower-right corner of the album view. The URL of the link is:
http://picasaweb.google.com/data/feed/base/user/levikcom/albumid/5083728396306002961?kind=photo&alt=rss&hl=en_US
To better use this feed with GME, I want it in the Atom format instead of RSS. I can do this by changing the alt=rss part of the URL to alt=atom:
http://picasaweb.google.com/data/feed/base/user/levikcom/albumid/5083728396306002961?kind=photo&alt=atom&hl=en_US
I can now create a very simple mashup to list my photos:
<gm:page title="Places" authenticate="false"> <gm:list id="list" data="http://picasaweb.google.com/data/feed/base/user/levikcom/albumid/5083728396306002961?kind=photo&alt=atom&hl=en_US"/> </gm:page>
This isn't a very interesting mashup (yet), but it does show a listing of all photos in my album. To make this mashup a bit more interactive, I will add a map and "connect" it to the list using event handlers. Notice that instead of using the full URL of the feed again for the map, I simply use the ${list} reference in its data attribute. This way, if the URL ever changes, I will only have to change one data source instead of many.
<gm:page title="Places" authenticate="false">
<gm:list id="list" data="http://picasaweb.google.com/data/feed/base/user/levikcom/albumid/5083728396306002961?kind=photo&alt=atom&hl=en_US">
<gm:handleEvent src="map" event="select"/>
</gm:list>
<gm:map id="map" data="${list}">
<gm:handleEvent src="list" event="select"/>
</gm:map>
</gm:page>
Since PWA provides us with a well-formed Google Data API (or "GData") feed, there is no need to explicitly specify the ref attribute for the map—the geo information is delivered in the default location.
Now, time to add some formatting and override the list template to show a thumbnail of the image:
<gm:page title="Places" authenticate="false">
<table width="100%">
<tr>
<td width="300" valign="top">
<gm:list id="list" template="photoList" height="300" data="http://picasaweb.google.com/data/feed/base/user/levikcom/albumid/5083728396306002961?kind=photo&alt=atom&hl=en_US">
<gm:handleEvent src="map" event="select"/>
</gm:list>
</td>
<td>
<gm:map id="map" height="300" data="${list}">
<gm:handleEvent src="list" event="select"/>
</gm:map>
</td>
</tr>
</table>
<gm:template id="photoList">
<div repeat="true" style="padding: 5px">
<gm:image ref="media:group/media:thumbnail/@url" height="54" width="54" mode="fill" style="float: left"/>
<gm:text ref="atom:title"/>
<br style="clear: both"/>
</div>
</gm:template>
</gm:page>
Notice that I am using HTML, CSS, and custom GME markup together. I've used the gm:image control to show the thumbnails, explicitly specifying both the width and height, and setting the mode attribute to 'fill' so that I get nice, even squares. I also added style="float: left" to have the text flow around the image. As with any float, I added a clearing <br/> tag underneath the flowing text. Finally, adding an explicit height value to the list will make it scroll if it should grow taller.
Let's add a larger photo to the mashup; I want a preview of the selected image to show up on the right side. For this, I will add another column to my layout table and put a gm:item module in it.
<gm:page title="Places" authenticate="false">
<table width="100%">
<tr>
<td width="300" valign="top">
<gm:list id="list" template="photoList" height="300" data="http://picasaweb.google.com/data/feed/base/user/levikcom/albumid/5083728396306002961?kind=photo&alt=atom&hl=en_US">
<gm:handleEvent src="map" event="select"/>
</gm:list>
</td>
<td>
<gm:map id="map" height="300" data="${list}">
<gm:handleEvent src="list" event="select"/>
</gm:map>
</td>
<td width="300">
<gm:item template="photo">
<gm:handleEvent src="list" event="select"/>
</gm:item>
</td>
</tr>
</table>
<gm:template id="photoList">
<div repeat="true" style="padding: 5px">
<gm:image ref="media:group/media:thumbnail/@url" height="54" width="54" mode="fill" style="float: left"/>
<gm:text ref="atom:title"/>
<br style="clear: both"/>
</div>
</gm:template>
<gm:template id="photo">
<gm:image ref="media:group/media:thumbnail[@width='288']/@url"/>
<gm:image ref="media:group/media:thumbnail[@height='288']/@url"/>
</gm:template>
</gm:page>
Perhaps you noticed that I had to resort to a bit of trickery here: the PWA feed provides me with thumbnails of two sizes. I use the first, small thumbnail in the list display, but want the bigger thumbnail for my item preview. Unfortunately, there is no way (yet, we're working on it!) to select the second thumbnail link directly, so I use two gm:image controls in my template, one looking for a thumbnail 288 pixels wide, the other for 288 pixels tall (288 pixels is the size of the medium thumbnail provided by PWA). This duplication allows me to account for both horizontally and vertically oriented photos.
My gm:item doesn't have a data attribute, and instead gets populated via select events from the list module. Clicking a pin on the map still updates it, since the event gets propagated by the list which listens to the map.
Time to add some custom scripting. I would like the map to zoom in whenever I select a picture, and zoom back out to show all picture locations when I de-select it. To do this, I can add a custom event handler that is called when a map selection is made as well as a custom JavaScript function to perform the required work:
<gm:map id="map" height="300" data="${list}">
<gm:handleEvent src="list" event="select"/>
<gm:handleEvent event="select" execute="onMapSelect(event.entry)"/>
</gm:map>
...
<script type="text/javascript">
function onMapSelect(entry) {
if (entry) {
var latLng = new GPath("georss:where/gml:Point/gml:pos").getValue(entry); // Get the entry's lat-long value
if (latLng) {
var map = google.mashups.getObjectById("map").getMap();
map.setZoom(14); // Zoom in
latLng = latLng.split(" "); // Split out the space-separated latitude and longitude values
map.setCenter(new GLatLng(latLng[0], latLng[1])); // Re-center the map
}
} else {
google.mashups.getObjectById("map").showAllMarkers(); // If no entry selected, zoom out
}
}
</script>
Notice how I use the event.entry variable in the event handler to pass the selected entry into the onMapSelect() function. This variable will be null for a de-select event, a fact I rely on to know whether I should zoom in or out. When zooming in, I am using the getMap() method of the map module to get access to the GMap2 object of the Google Maps API. However, after zooming, I need to re-center the map manually; I use GPath to obtain the lat-long value from the entry and move the map by calling setCenter(). Since I only want to zoom out as far as I need to see all the map pins, I use the map module's showAllMarkers() method, rather than relying purely on the Maps API functionality with something like setZoom(1).
I'm pretty happy with my mashup at this point, but there is one small problem. Google doesn't seem to have very detailed map data for one of the places I've visited (Buenos Aires), and another (Black Rock City, Nevada) just doesn't have all that many roads to make for an interesting map. However, we do have pretty detailed satellite imagery of these places, so I will probably get better pictures if I set my map to display a 'Hybrid' view.
To do this, I can add an onload handler to my mashup, which will call a function once everything is properly bootstrapped and loaded:
<gm:page title="Places" authenticate="false" onload="switchMap()">
...
<script type="text/javascript">
function switchMap() {
var map = google.mashups.getObjectById("map").getMap();
map.setMapType(map.getMapTypes()[2]); // Hybrid view is the third type supported by the Maps API
}
...
</script>
</gm:page>
The onload attribute of gm:page can be used similar to HTML's <body> tag's onload attribute—to trigger JavaScript that will execute once the mashup has been loaded. This is pretty useful in performing the type of initialization that requires the GME framework and components to be fully loaded first.
Now I'm satisfied with my mashup. I was able to create a functional viewer for the places I've visited with under 60 lines of code, including layout and custom scripting. I can go ahead and publish it on googlemashups.com and start planning for my next adventure.