Walhthrough/Tutorial 2: How to make the Google Maps - Google Trends - Google Zeitgeist mashup.
The mash itself can be found here;
www.mashupstation.com/station/users/admin/dapptest.html∞
The secret sauce behind this mashup is actually dappit.com. Yes, we're riding that particular rocking-horse pretty hard around here, but that's just because it cuts development time _a lot_.
The reason for that is that
dappit ∞ makes it easy to select parts of one or several pages, and then call upon dappit to scrape those pages "a la minute", so you get the info that's there right now, be it news, music listings or what have you.
But the real timesaver is that dappit let you read that info in a variety of formats. You can get the list of things yuo just scraped from a graphically frustrating web-page as a RSS-feed, as XML, YAML, raw HTTP, or (as in our case) JSON (Among other formats).
The reason we like JSON is that the data is returned as a hierarchical
JavaScript object. So instead of accessing the data - nornmally returned as XML from most web-based services - as data[0].childNodes[5].childNodes[2].Url or something, we can use a named notation more like; data.Result.Items[index].Url.
In the Trends-Maps-Zeitgeist (TMZ) mashup I use only two dapps (one for Trends and one for Zeitgeist), since Gogle maps doesn't need to a cross-domain proxy. The reason for that is that even though a script on a page can only connect to the server the page was loaded from, you can always load and execute an external script and load external pictures.
This is used in google maps so that the script itself is loaded, given local parameters (such as long/lat, et.c) and then loads pictures from google servers and formats them in its own way with js. No cross-domain
XhttpRequest is ever made that violates the security principles of js execution.
Anyway, onward to the code. We begin in the (doh) beginning;
<html>
<head>
<title>Dappit powered google trends - Zeitgeist mashup</title>
<meta content="">
<script src="../../scripts/prototype.js"></script>
<script src="../../scripts/connect.js"></script>
<script src="http://maps.google.com/maps?file=api&v=2&key=ABQIAAAAAB2mHkgTU7lUQcCzRfHqAhQgAWOc_nvETqnLKspyTsVPVvLVfxT7jK-Eq1l-gyQIDzIvYbmCdz8ERQ"
type="text/javascript"></script>
We use prototype as always for the .each and $ functions which are now much copied into new and
up-and-coming libraries∞.
Then we have the ubiqitous connect.js which handles the verification and cross-domain proxying of the communication. And latsly there's the google maps call. Note that the "developer key" for Maps is always tied to the url of the site from where the apge is loaded, so it really can't be used anywhere else. Therefore it makes no sense to use mashupstations developer key protectiong - and it wouldn't work either :)
Note that if you want to use google Maps on a mashup you make here on the station, you have to grab the key as well. My treat :)
Next, some intializations;
//---------------------------------------------
var map;
var query = "wii,psp,nintendo ds,ps3"; // The search query (comma-separated search items)
var geocoder = new GClientGeocoder(); // Add points for top cities
var allmarkers = new Array(); //So that we know which markers to rease from map between searches
var region = ""; // non-null if specific region is selected
//---------------------------------------------
The map will hold the Google Maps js object, to be used when adding events and things below. The query is the default search string which will be sent to Trends when loading the page, and the next three variables are specific to google maps. The Google Geocoder SUCKS by the way. I've been trying to find a viable geocoder which understand addresses outside USA, but haven't had much luck finding something that's easy to use.
The region variable will hold the gazillion country options (like SE - Sweden) so that Trend searches can be narrowed and happily ignored by google
geocoder.
The we init the Google Map;
function load()
{
if (GBrowserIsCompatible())
{
map = new GMap2(document.getElementById("map"));
map.addControl(new GSmallMapControl());
map.addControl(new GMapTypeControl());
map.setCenter(new GLatLng(15, -20), 2);
GEvent.addListener(map, "click", function(marker, point){
if (marker)
{
map.removeOverlay(marker);
}
else
{
map.addOverlay(new GMarker(point));
}
});
}
}
</script>
And the basic functrionality of this section (Happily ripped stright out of the strangely easy to follow google maps example pages (Strange because the rest of their API's are a pretty motley crew, with no generic interfaces shared))
is to add navigation controls and the ability to add or remove markers when clicking on the map.
The we skip some css stuff and the nicely color-schemed google ad and jump right into the initialization of the above function;
The we have some div containing the map and lists of things, as well as the horribly large option lists for countries seletion, which will be ommited here for boredom reasons;
<H1>The Google Maps - Google Zeitgeist - Google Trends Mashup</H1>
<hr/>
<br/><b>Dappit result for current Google trends</b><br/><br/>
<div id="map" class="map"></div>
<select id=regionselect name=geo onchange='selectorsChanged()' >
<option value='all' selected>All Regions</option>
..
..
</select>
<form action="javascript:doit();">
<input id="query" type="text" size="40"> </input>
<input type="submit" value="Go"/>
</form>
<B>Google Trends Top Cities:</B><BR><div id="foo2" class="citylist"> </div>
<br/>
<B>Zeitgeist monthly most popular:</B><BR>(Click to add search terms to query field)<br/><div id="foo" class="zeitlist" > </div>
<br/><br/><br/>
<div id="baz" class="chart"> </div>
<div id="bar"> </div>
<div id="linklist" class="linklist"> </div>
Beside the small script handling the option selection, there's not much to say here, except for the truly nonstandard way of naming the divs. Who wrote this stuff anyway?!?! :)
Then it's time to dive down into code again. Mmmm..
<script>
...
//showDebug();
$('regionselect').selectedIndex = 0;
function selectorsChanged()
{
// Get the selected region option
region = $('regionselect').options[$('regionselect').selectedIndex].value;
}
...
Omitted here are the debug functions and the removeChildrenFromNode function which is the same as in previous example. Left is the method which takes care of country selection, which gets added to the dapp call (as a dapp argument :-P You didn't know that did you :) ).
function showAddress(address, text)
{
// Fix Google BD geocode
if (address.indexOf("United Kingdom") > 0)
{
address = address.replace("United Kingdom", "England");
}
geocoder.getLatLng(address, function(point)
{
if (!point)
{
alert(address + " not found");
}
else
{
//map.setCenter(point, 13);
var marker = new GMarker(point);
map.addOverlay(marker);
allmarkers.push(marker);
GEvent.addListener(marker, "mouseover", function() {
marker.openInfoWindowHtml(text);
});
}
});
}
function removeAllMarkers()
{
allmarkers.each(function(m)
{
map.removeOverlay(m);
});
}
ShowAddress gets called once for every top city we receive from the Google Trends dapp scraper, and the other function pretty obviously removes all markers from the map and is called when a new search is made.
Now we come to our first json callback function, named zcb because it handles the scraping data from the Zeitgeist dapp;
function zcb(data) //Callback function
{
if (!data) return;
debug("zcb called with "+data);
removeChildrenFromNode($('foo'));
var f = data.fields;
debug("zcb fields are "+f);
var items = f.zeitgeist_items;
var j = 1;
var zlist = document.createElement('ul');
debug("zcb got "+items.length+" items");
items.each(function(i)
{
var d = document.createElement('li');
d.innerHTML = "<i>"+j+". "+i.value+"</i>";
debug("zcb created list item "+i.value);
d.onclick = function()
{
$('query').value = $('query').value + i.value + ",";
}
j++;
zlist.appendChild(d);
});
$('foo').appendChild(zlist);
}
As you see, it begins by removing everything from the 'foo' div, and then iterates over teh data.fields.zeitgesit_items array which has been assembled by dapp as part of the normal json behaviour. Inside each 'field' we then read the 'value' property which holds the string we are looking for. The array i ordered, so we number our items accordingly and build a nice HTML list.
After that comes the 'main' json callback, which handles Trends data;
function cb(data) //Callback function
{
if (!data) return;
// Make sure we erase any previous content, otherwise it will look pretty silly.
removeChildrenFromNode($('baz'));
removeChildrenFromNode($('bar'));
removeChildrenFromNode($('foo2'));
removeChildrenFromNode($('linklist'));
//$('foo').innerHTML = "cb called with;"+data;
//------------------------- Query --------------------------
var query = data.fields.Trend_query[0].value;
var label = document.createElement('div');
label.innerHTML = "<B>Query: </B>"+query;
$('baz').appendChild(label);
//------------------------- Chart --------------------------
var imgsrc = data.fields.Generic_google_trends_chart[0].src;
var img = document.createElement('img');
img.src = imgsrc;
$('baz').appendChild(img);
//------------------------- City List ----------------------
var citidiv = document.createElement('div');
var citilist = document.createElement('ul');
citidiv.appendChild(citilist);
var cities = data.fields.Top_cities;
var i = 1;
removeAllMarkers();
cities.each(function(c)
{
if (c.value != "" && c.value != "1.")
{
var li = document.createElement('li');
var sub = "";
if (c.value.indexOf('.') > 0)
{
sub = c.value.substring(c.value.indexOf('.')+1 , c.value.length);
}
else
{
sub = c.value;
}
li.innerHTML=""+i+". "+sub;
showAddress(sub, ""+i+". "+sub);
citilist.appendChild(li);
i++;
}
});
$('foo2').appendChild(citidiv);
//debugger;
//------------------------- Link List ----------------------
var llist = data.fields.Trend_keyword_search;
debug("Got "+llist.length+" keyword search items");
var odd = 0;
var linkdiv = document.createElement('div');
var linklist = document.createElement('ul');
var currentli = "";
llist.each(function(l)
{
if (odd == 0)
{
currentli = document.createElement('li');
currentli.innerHTML = '<img src="'+l.src+'"/>';
odd = 1;
}
else
{
currentli.innerHTML = currentli.innerHTML + " "+l.value;
debug("Adding list element "+l.value);
linklist.appendChild(currentli);
odd = 0;
}
});
linkdiv.appendChild(linklist);
$('linklist').appendChild(linkdiv);
}
This is the longest function in the mash and I hope that the sections I've commented it into makes it more readable. The key to understand how the data is parsed is of course to check out the dapp itself,
so that's what I recommend.∞
Noteworthy here are two thigns; 1) I have to massage the string describing each city, because I want to strip any preceding number and dot, like; '7. Los Angeles', 2) When making a daåpp it is not uncommon to get two columns interweaved, like number, name, number, name, et.c. into jsut one array. Therefore I ose the 'odd' varaible in the linklist parsing to grab just what I need.
Then there's tha main logic and finish;
//
//-------------------------------------------------------------------------------------
// Here be main logic
//-------------------------------------------------------------------------------------
//
$('query').value = query;
function response(data)
{
var x = data.responseText;
eval(x);
//$('foo').innerHTML = "Response called with;"+data.responseText;
callZeitgeist(); // When we get here, we've been called back from our google search. Make the zeitgeist search again.
}
function error(x)
{
$('foo').innerHTML = "Error:"+x;
}
function zresponse(data)
{
debug("zresponse called with "+data.responseText);
zcb(eval(data.responseText));
}
function zerror(x)
{
$('foo').innerHTML = "Zeitgeist Error:"+x.responseText;
}
function callZeitgeist()
{
debug("callZeitgeist called");
var zurl = 'http://www.dappit.com/transform.php?dappName=GoogleZeitgeistlisting&transformer=JSON&extraArg_callbackFunctionWrapper=zcb';
callMashupStation(zurl, zresponse, zerror);
debug("callZeitgeist survived call to mashupstation");
}
function doit()
{
query = escape($('query').value);
// Remove trailing comma, if any
if (query.charAt(query.length) == ",")
{
query = query.substring(0, query.length - 1);
}
if (!region)
{
url = 'http://www.dappit.com/transform.php?dappName=Genericgoogletrendschart&transformer=JSON&variableArg_0='+query+'&extraArg_callbackFunctionWrapper=cb';
}
else
{
url = 'http://www.dappit.com/transform.php?dappName=GenericgtrendsVersion2&transformer=JSON&variableArg_0='+query+'&variableArg_1='+region+'&variableArg_2=2006&extraArg_callbackFunctionWrapper=cb';
}
callMashupStation(url, response, error);
}
doit();
//-------------------------------------------------------------------------------------
</script>
</body>
</html>
The doi function is the start of everything. Again, this is a very good idea, so that you don't sprinkle your page with odd bits of js code here and there. Also, that makes it much easier to make a library of yuo functions and jsut import them as a script with the <script src="...> tag and separate code and presentation properly.
Now, this is a bit of a hack. When I begun doing this mash I didn't realize I might want to narrow down searches geographically, so when I eralized that I made a new dapp. That's why the doiy function checks to see if the user has made a region slection or not. I didn't know at the time that I only need to use the lower dapp and have 'all' as a region to scrap the old version. Being the old, lazy bastard I am, I just threw some logic on it instead of cleaning up. Just get over it, OK :)
When callMashupStation returns, it will call (as you can see at the back of hte 'url' variable) the cb callback function, but also the response function, since the ajax call returned properly. I eval the return expression, but I really don't have to. The json callback gets called anyway. Strange, eh?
Anyway, what I do next is to call the zeitgeist function. The reason I don't just call both the trends and teh zeitgeist dapps at the same time is that the page behaved less weird when I did it in an orderly fashion.
I think that the rest of the code speaks for itself. If you have any more question, please post them in our forums, so that others can be helped by our answers. Thanks!!
There are 33 comments on this page. [Display comments]