Share |

Home > Archive > Developers/reference > Recipes & mashups > Mashup with Google Maps

Create a Mashup Between SpicyNodes and Google Maps

A mashup is a web page that combines two or more existing sets of data or functions to create a new service. In this tutorial, we’re going to have some fun utilizing SpicyNodes’ API together with Google Maps to create unique functionality. This tutorial demonstrates how SpicyNodes can talk to another system, and vice versa.

View demo

What We're Going to Build

In this mashup, there are two parts. On top, we’re going to generate a nodemap of cities via SpicyNodes. On the bottom, there is a Google maps container. When we traverse our nodemap, the map automatically loads and pans to the city currently selected on our nodemap. You can also add a city to the nodemap through a simple form placed below the map. That’s it. Let’s get started.

What You'll Need to Know

This tutorial is for the typical web master who has already reviewed SpicyNodes’ documentation and is well versed in how it works and how it interfaces with JavaScript. It’s also helpful to have had experience working with the Google Maps JavaScript API.

The NodeMap's XML

The nodemap XML will follow the typical XML syntax for SpicyNodes nodemaps, with a few extra attributes:

<?xml version="1.0" encoding="UTF-8"?>
<node label="Choose a city..." >
    <node label="Paris" lat="48.862004" lng="2.352619" zoom="8" >
        <node label="Reims" lat="49.2539" lng="4.0242" zoom="15" > </node>
        <node label="Rouen" lat="49.431" lng="1.1032" zoom="15" > </node>
    </node>
    <node label="New York" lat="40.700422" lng="-74.006653" zoom="8" loc="JavaScript:getXML();" >
        <node label="New Haven" lat="41.30" lng="-72.95" zoom="10" ><![CDATA[New Haven’s description text...]]></node>
    </node>
    <node label="Beijing" lat="39.897094" lng="116.400146" zoom="8" > </node>
    <node label="Warsaw" lat="52.24052" lng="21.02354" zoom="8" > </node>
    <node label="Buenos Aires" lat="-34.612019" lng="-58.372335" zoom="8" >
        <node label="Buenos Aires Airport" lat="-34.555" lng="-58.405" zoom="13" ><![CDATA[Aeroparque Jorge Newbery]]> </node>
        <node label="Buenos Aires - La Plata" lat="-34.97" lng="-57.945" zoom="12" ><![CDATA[El barrio La Plata]]> </node>
    </node>
    <node label="Oslo" lat="59.94331" lng="10.741971" zoom="8" > </node>
</node>

In the nodemap’s XML, note how we define the cities’ latitude and longitude values, along with the level of zoom in the map. We’ll make use of all three attributes shortly.

Setting Up the HTML

We need to make sure our HTML framework is in place for our mashup. Let’s tackle them in order.

The first order of business is to place the SpicyNodes viewer (a Flash object) on the page. We’re going to use the SpicyNodes engine to render the nodemap. But rather than store the nodemap XML on the SpicyNodes server, we will make our own XML file, nodemap/maps.xml. Our embedding code looks like this:

<object id="spicynodesViewer" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab"
height="400" width="950">

<param name="movie" value="http://media.spicynodes.org/display.swf" />
<param name="scalemode" value="showall" />
<param name="allowFullScreen" value="true" />
<param name="quality" value="high" />
<param name="allowScriptAccess" value="always" />
<param name="FlashVars"
value="snconnector=http://localhost/api/SNresources/snconnector.swf&nodemap=http://localhost/api/nodemap/maps.xml" />

<!– additional attribute name="spicynodesViewer" identifies the SpicyNodes Flash movie –>
<embed name="spicynodesViewer" height="400"
width="950"
scalemode="showall"
allowFullScreen="true"
pluginspage="http://www.macromedia.com/go/getflashplayer"
src="http://media.spicynodes.org/display.swf"
type="application/x-shockwave-flash"
quality="high"
allowScriptAccess="always"
flashvars="snconnector=http://localhost/api/SNresources/snconnector.swf&nodemap=http://localhost/api/nodemap/maps.xml" />
</object>

Notice how we’re passing the location of the SNConnector file and the XML for the maps as Flash variables to the engine. Make sure to remember the id we’ve assigned to the embedded movie. We’ll be using this extensively throughout our code.

Second, we’ll need to include the JavaScript file required for Google Maps. We’ll load it directly from Google’s servers.

<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<script type="text/JavaScript" src="http://maps.google.com/maps/api/js?sensor=false"></script>

Next, we need to include our custom files that will interface to our nodemap, and then manipulate it as needed. We’ll use two files here: GetMovie.js and Maps.js. We’ll look at both later.

<script language="JavaScript" src="js/GetMovie.js"></script>
<script language="JavaScript" src="js/Maps.js"></script>

With the JavaScript included, we will add code to our HTML <body> tag to initialize some JavaScript objects. We’ll get into the details of the role of these objects later. Right now, it’s enough to note that they should not be initialized before the whole page loads (particularly the SpicyNodes Flash movie). To guarantee all other elements are loaded before their initialization, we’ll set the HTML <body> tag as follows:

<body onLoad="initializeMaps(); initializeMessages('spicynodesViewer’);">

The first call in onLoad is initializeMaps() which will initialize the Google map, and the second one will initialize communication between SpicyNodes (identified as ‘spicynodesViewer’) and the server side JavaScript.

Also, create a DIV element for the Google Maps script.

<div id="map_canvas" style="width:950; height:400"></div>

Finally, we need some rudimentary markup to let the user add a city to the nodemap.

<form name="htmlForm">
<select name="cityCombo" >
<option value="">Select City</option>
</select>
  Add new location to current node
<input type="text" name="locationName" size="20" value="Location name..."/>
  Description
<input type="text" name="locationDescription" size="30" value=""/>
  latitude
<input type="text" name="latitudeValue" size="2" value="0"/>
  Longitude
<input type="text" name="longitudeValue" size="2" value="0"/>
  Zoom
<input type="text" name="zoom" size="2" value="8"/>
<input type="button" name="appendNew" value="Add"  />
<br/>
<input type="button" value="Get Current Nodemap as XML" /><br/>
 
<br/>
<textarea cols="50" rows="4" name="xmlOutput"></textarea>
</form>

We have a select element to switch between the loaded cities, and in order to let the user add a city, we have text boxes to hold the values for the name, description, latitude and longitude of the city to be added.

Finally, we have a simple button that lets the user capture the XML used to create the current nodemap. You might not have a button like this in your mashup, it’s here as an illustration of the capability.

Acquiring the Movie with JavaScript

We first need to acquire the movie to be used by our JavaScript. This code will go in GetMovie.js — We’ll look at the full code first, and then drill down to the important parts:

var spicynodesMovie;
var currentSelected;

function getFlashMovie(movieName) {
  var isIE = navigator.appName.indexOf("Microsoft") != -1;
  return (isIE) ? window[movieName] : document[movieName];
}

function initializeMessages(movieName) {
    spicynodesMovie = getFlashMovie(movieName);
}

First, we create a variable to hold the SpicyNodes flash movie reference we’re going to acquire.

Next, we create a simple function that checks which browser the user is currently using, and then chooses a method to acquire the flash movie. Right now, it’s all boilerplate.

The next function, initializeMessages, is were our SpicyNodes movie reference is defined by calling the function we previously created. This is called by our HTML code once the page is loaded (remember <body onLoad="..."> ? ).

With this completed, we can finally move on to the meat of our functionality. All of the code from this point forward should be placed under Maps.js. You’re free to place this whereever you want. It’s easiest, though, to keep the different functionalities separate so as to avoid clutter. Also, the first file — GetMovie.js — is a helpful template for times when JavaScript interacts with SpicyNodes, so you can use it for your future mashups and experiments.

Initializing Google Maps

Our other JavaScript file is Maps.js. First up, we’ll need to create a number of variables that’ll be used throughout the code.

var map;
var citiesList;
var markersList = [];

With that done, we can move on to initializing Google Maps. The following snippet will take care of it.

function initializeMaps() {
    var initialLatlng = new google.maps.LatLng(-34.397, 145.644);
    var myOptions = {
      zoom: 8,
      center: initialLatlng,
      mapTypeId: google.maps.MapTypeId.HYBRID
    };

// Geocoder used to know current location
geocoder = new google.maps.Geocoder();

// map is a global variable
map = new google.maps.Map(document.getElementById("map_canvas"),
     myOptions);

// listen to current location, to update fields below google map
google.maps.event.addListener(map, ‘dragend’, function() {
updateCurrentLocation(map.getCenter());
});
}

This initializeMaps() function is called from our HTML after the loading of <body> contents is completed, and it defines the options we need to pass to Google Maps. Next, it’s just a matter of acquiring the container we created for this functionality (map_canvas) and passing along the options we need. The geocoder is used to get the current map location, which can be used later as the content for a new node.

Handling navigation events

Now we create onSNAnimationStart(), a function that is called every time an animation starts in our nodemap. We hook into this event to make sure our currently selected node is kept updated on the client side. If the node argument is defined, then we ask SpicyNodes about the structure of whole nodemap that currently exists in memory. We’ll do it by calling the getTreeObject method. Later, based on returned information, we will populate the city selection combobox.

function onSNAnimationStart(node)
{
    // update the currentSelected node
    currentSelected = node;

    if(node) {
        // start
        spicynodesMovie.sn_call('getTreeObject,true,true’);
    }
}

Handling Callbacks

We’ll need a way to handle the data SpicyNodes sends back to us after we’ve made a request. The method name and a value is returned to us. Today, we’re only using two callbacks: getTreeObject and getTreeXML. So, we’ll just check which is being returned and perform the actions we need.

function callBackFromSN(methodName, returningValue)
{
    switch(methodName.toLowerCase())
    {
        case "gettreeobject": {
            updateCityCombobox(returningValue);
            goToCity(currentSelected.id);
            break;
            }
        case "gettreexml": {
            document.htmlForm.xmlOutput.value = returningValue;
            break;
            }
    }
}

We’re naming our callback method callBackFromSN, as required by the SpicyNodes engine. As we previously mentioned, the name of the method and the returned value is passed in.

We just created a very simple switch block to check which method is returned. If it’s gettreeobjext, we update the select element holding the list of cities and change the map to show the currently selected city.

If we attempted to acquire the XML of our current nodemap, we would merely update the value of the relevant text area with the value returned to us by SpicyNodes.

Updating the ComboBox

The combobox will be updated whenever we navigate to a new node. This covers layout initialization and adding a new city. The method responsible for it is called by the callback handler we just wrote and looks like this:

function updateCityCombobox(tree)
{
    citiesList = getIndexesList(tree);
    var optionsLen = document.htmlForm.cityCombo.options.length;

    // remove all combobox items
    for(m = document.htmlForm.cityCombo.options.length - 1;
                m > 0 ; m– ) {
        document.htmlForm.cityCombo.options[m] = null;
    }

    var listLen = citiesList.length;

    // populate combobox with new items, ommiting the
    // home node (i = 1 instead of i = 0)
    for(var i = 1; i < listLen; i++) {
        document.htmlForm.cityCombo.options[i] =
                new Option(citiesList[i].data.id + ": " +
                citiesList[i].label, citiesList[i].data.id);
    }
}
function getIndexesList(tree) {
      var list = [];
      getIndexesListRec(tree, list);

      return list;
}
function getIndexesListRec(node, list) {
      list.push({label:node.label, data:node});

      var children = node.children;

      for(var k in children) {
          getIndexesListRec(children[k],list);
      }
}

The tree argument is an object representing the entire, currently displayed SpicyNodes tree. In fact, it is the root node, and its children property is an array of its children — also nodes that can have their own children.

First up, notice the getIndexesList method that we use to get the citiesList we need. getIndexesList() is a helper function that recursively collects a list of items passing from the root to its children to its grandchildren and so on. These items will later be used to populate the combobox.

Next, we make a note of the current length of the selected list so we can iterate through it. Initially, we loop through and empty it so we can repopulate it with the new information.

Then, we loop through the select item again, but this time, we create an option with the name of the city and then add it back to the list. We omit the first items since it is the tree root labeled "Choose a city...", not a city itself.

Changing Cities

Once the combobox is populated, we can proceed to the second method used to handle getTreeObject() method in callBackFromSN(), which is goToCity().

This is the most important part of our mashup, so pay attention. We’ll list out the method itself and then explain it bit by bit.

function goToCity(nodeId)
{
    var node;

    var len = citiesList.length;

    // select the city data
    for(var k = 1; k < len; k++) {
        if(nodeId == citiesList[k].data.id) {
            node = citiesList[k].data;
            break;
        }
    }

    var lat = node.lat;
    var lng = node.lng;

    if(!lat || !lng) {
        alert("goToCity() no latitude or longitude defined for node: "
                + node.label);

        changeCurrentNode(node);
        return;
    }

    // get a new Google Maps latitude-longitude object
    var newLatLng = new google.maps.LatLng( Number(node.lat),
                Number(node.lng) );

    // center map above selected city
    map.panTo(newLatLng);
    map.setZoom( Number(node.zoom) );

    // place the new marker
    placeNewMarker(newLatLng, node.label);

    // if not yet selected, select the node in SpicyNodes layout
    // (this is needed when a city is selected from combobox)
    if(currentSelected.id != node.id) {
        spicynodesMovie.sn_call("nodeId,"+ node.id);
    }
}

This may look complicated, but it’s not. We first loop through the existing cities list, searching for the node with id number equal to the one passed as argument. Each city item holds a node as its data propery — it was part of the combobox.

Once the node is found, we access its latitude and longitude data (it not defined, just change focus and return), and create a Google Maps LatLng object.

Using the LatLng object and zoom value held in node’s data, we can center the map over the selected city — map.panTo(), map.SetZoom() and Google Maps does all of the hard work for us.

The LatLng object and node’s label are needed to place a new marker in the map.

Now that the map is fully updated, if the newly selected city isn’t currently selected, we call the nodeId() SpicyNodes method, and change the focus to the id number of the node related to this city.

Writing a Helper to Place a Marker

/* Places a new marker.
        The new marker, when clicked pop an info window. */
function placeNewMarker(latLng, cityName)
{
    var newInfoWindow = new google.maps.InfoWindow({
            content: "You have just clicked the '" +
                cityName + "’ marker..."
    });

    var newMarker = new google.maps.Marker({
        position: latLng,
        map: map,
        title: cityName + " marker."
    });

    // define the function triggered after clicking a marker
    google.maps.event.addListener(newMarker, ‘click’,
        function() {
           newInfoWindow.open(map, newMarker);
         });
}

These two functions together help us place our markers on the Google map. If it looks a little alien, don’t be alarmed. It will become clear in a few minutes.

We’ll first create a quick window through Google Maps to inform the user that the node the user requested has been clicked.

Remember how this method is passed — the latitude, longitude and the name of the city? We’ll be making use of it in the next part of the function.

We start by creating a new object called newMarker (part of Google Maps) that provides the position, the actual map that the marker points to, and a short title since Google Maps requires this information to place a marker.

Finally, we subscribe the newMarker object as Google Maps ‘click’ event listener with a short handler function that opens the window we first created.

Placing the marker is straightforward and we mostly manipulate predefined Google Maps objects, but if any details are still not clear it’s helpful to read the Google Maps reference.

Now that we’re done changing cities with SpicyNodes and the combobox, we’ll move on to describing two methods, one for displaying the nodemap as raw XML and a second one for appending new nodes to the existing layout.

Acquiring the Node Map's XML

Acquiring the nodemap’s XML content is simple. We just need to make a simple call to SpicyNodes and capture the value returned. The method looks like this:

function getXML()
{
    flashMovie.sn_call('getTreeXML,true,true’);
}

We just called the method getTreeXML, which returns the complete XML used to construct the current nodemap. The callback, which captures the returned data, was explained in "Handling Callbacks" above.

Adding a City

Now we’ll code in the functionality to add a city to both Google Maps and our nodemap. This is a two-step process. First, we’ll need to update parts of the HTML to call a method, and then we’ll write the method itself.

First the HTML:

<input type="button" name="appendNew" value="Add"
        onClick="appendNode(document.htmlForm.locationName.value,
        document.htmlForm.locationDescription.value,
        document.htmlForm.latitudeValue.value,
        document.htmlForm.longitudeValue.value,
        document.htmlForm.zoom.value);" />

We’ve just made sure that a method, appendNode, is called everytime the button is clicked. The values are all dynamically acquired from the form itself. No issues here.

Now, the method appendNode() itself:

function appendNode(locationName, description, latitude, longitude, zoom)
{

   var node;
   node = "<node label=’" + locationName + "’ lat=’" +
        latitude + "’ lng=’" + longitude + "’ zoom=’" + zoom + "’  >"
   if(description.length > 0) {
       node += "<![CDATA[" + description + "]]>"
   }
   node += "</node>";

   /* append a new child to currently selected node */
   flashMovie.sn_call('appendNode,’ + currentSelected.id+ "," + node);
}

As you can see, our method encompasses a number of parameters. These map directly to the input boxes we created in the HTML. We first concatenate them into a string to be used in our XML. If the syntax seems a little alien to you, please refer to our XML syntax guide.

Once our string has been constructed, we call the SpicyNodes method to append a node to the current nodemap: appendNode. We also pass in the id of the current node, which becomes the parent of the newly appended node and finally the long string that we constructed earlier.

Samples

View all of the above, plus additional code to tie it all together, in a live demo or download a zip archive with sample files. The files are:

  • sample-SN_and_maps.htm — HTML file that loads the two JavaScript files, the sample SpicyNodes viewer, and a Google Maps viewer.
    • GetMovie.js — Sets up communication from JavaScript in the web page to SpicyNodes
    • Maps.js — JavaScript code for the mashup
    • maps.xml — Nodemap XML file
    • snconnector.swf — Needs to be included to allow the SpicyNodes viewer to access the XML file hosted on your server

To use the sample files, upload them to a web site (they will not work locally), and edit sample-SN_and_maps.htm, changing the four occurances of "http://localhost/api" to the path on your server. In the case of our demo, that is "http://www.spicynodes.org/demo/Mashup-SN_and_maps".

Wrapping Up

And that’s it. Load up your page and navigate through the mashup you created completely from scratch, utilizing the APIs of both SpicyNodes and Google Maps. Congratulations! You’re done! Also, take a look at a simpler mashup that mixes SpicyNodes with Google Search.

Share |