MapQuest Developer Blog

Archives for Marty Kube

  • MapNews - A Map Based News Browser - Part 3 - AOL National News RSS Feed

    "In theory, there is no difference between theory and practice. But, in practice, there is." - Jan L. A. van de Snepscheut

    In theory, the project to place RSS news feeds on a map could be very easy. GeoRSS is standard for encoding geographic locations in RSS feeds. RSS feeds that have this encoding are the perfect data source for this project. But, in practice, the feeds I've been looking at do not have GeoRSS information. So, I'm going to start by seeing what I can get done without GeoRSS.

    The source of news that I'm working with is the AOL national news RSS feed. The description element of each feed starts with a location. I extracted the location, geocoded each location with the MapQuest (MQ) geocoding service, and then placed point of interest (POI) markers on the map.

    In my last post, I laid out my design for MapNews. In this post, I present the working application and code I've written based on the MQ client Javascript tool kit. The following screen shot of MapNews shows POI markers for cities that have news stories in an example feed. I've clicked on the POI marker for Salt Lake City to expose links to news stories in the information window.

    The HTML and Javascript for MapNews are shown below. I used prototype for an AJAX fetch of the RSS feed and also for parsing the RSS XML. On line 7, I included prototype 1.6 since the MQ Javascript library version seemed to be missing the AJAX component (also set ipr=false on line 6). I installed the PHP version of the MQ proxy on my server and pointed the MQExec object to the proxy on lines 13-26. I installed the MQ API Javascript files on my server and referenced them on lines 8-10 (excluding mqcommon.js which is included in line 6).

    Lines 34-43 define the Feed class that represents each feed item and associated geocoding results. I fired off an AJAX request to fetch the RSS file on lines 46-54 and handled the return in processRss (lines 57-71). Lines 60-63 pull the item description and link straight from the RSS feed (the prototype function cleanWhitespace (line 126) comes in handy here).

    The function parseForCityAndState (called on line 64) uses a regex to find a city, and perhaps, a state in the description RSS element. The regex on line 114 will make more sense if you consider the following examples of the AOL RSS feed:

          <description>
            ST. LOUIS (AP) - Substandard care at a southern Illinois ...
          </description>
          <description>
            ALBANY, N.Y. (AP) - It's the perfect tax: Government  ...
          </description>
        

    I, basically, looked for text starting the description prior to the literal '(AP)', and if a comma was present, I inferred that the state was the text after the comma.

    With the city and state in hand, I call the MQ geocoding service (line 100-111). The geocoding service worked well; I had only 1 failed geocoding out of 35 items, many of which had only a city name. I picked the first element of the returned MQGeoAddress collection (line 109) and skipped items for which the geocoding failed (line 76).

    Finally, the geocoded items are converted to POI makers with associated descriptions and links (lines 88-97). I collected the items by location to have only one information window per location with multiple items (lines 77-84). This was accomplished by building a hash with keys of MQLatLng.toString() (which is a concatenation of longitude and latitude) and values of a list of Feed objects.

    I'm pretty happy with the application so far. There is one problem, however. I'm firing off many geocoding requests (one for each RSS item) from the client and the page load is too slow. I envision that the fix will be to use the batch geocoding method from the MQ API, or, to do the geocoding once per RSS feed on the server.

    The next steps for MapNews are to consume GeoRSS feeds and to adopt batch geocoding for feeds without GeoRSS encoding.

    001 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    002 <html>
    003  <head>
    004   <title>MapNews</title>
    005   <link rel="stylesheet" href="mapnews.css"
    type="text/css"/>
    006   <script src="http://btilelog.access.mapquest.com/tilelog/transaction?
        transaction=script&key=mjtd%7Clu6y290rn9%2C22%3Do5-0utn1
        &ipr=false&itk=true&v=5.2.0"
        type="text/javascript"></script>
    007   <script src='prototype-1.6.0.2.js'
        type='text/javascript'></script>
    008   <script src='mqutils.js'
        type='text/javascript'></script>
    009   <script src='mqobjects.js'
        type='text/javascript'></script>
    010   <script src='mqexec.js'
        type='text/javascript'></script>
    011   <script language='javascript'>
    012
    013    var g_proxyServerName = '';
    014    var g_proxyServerPort = '';
    015    var g_proxyServerPath = '/mq/JSReqHandler.php5'
    016
    017    var g_serverName = 'geocode.dev.mapquest.com';
    018    var g_serverPort = '80';
    019    var g_serverPath = 'mq';
    020
    021    var g_geoExec = new MQExec(
    022     g_serverName, g_serverPath, g_serverPort,
    023     g_proxyServerName, g_proxyServerPath, g_proxyServerPort
    024    );
    025
    026    var g_mqMap;
    027
    028    function startMap() {
    029     g_mqMap = new MQTileMap(document.getElementById('mapWindow'), 2,
    030       new MQLatLng(39.81,-98.56), "map");
    031     getRss();
    032    }
    033
    034    function Feed(){}
    035    Feed.prototype = {
    036     title: '',
    037     link: '',
    038     description: '',
    039     city: '',
    040     state: '',
    041     country: 'USA',
    042     geoAddress: null
    043    };
    044
    045
    046    function getRss() {
    047     new Ajax.Request('news_top_nat.xml', {
    048      method:'get',
    049      onSuccess: function(transport){
    050      processRss(transport.responseXML);
    051     },
    052     onFailure: function(){ alert('Something went wrong...') }
    053     });
    054    }
    055
    056
    057    function processRss(root) {
    058     var feeds = new Array();
    059     Element.select(root, 'item').each(function(item, i) {
    060      var feed = new Feed();
    061      feed.title = getChildsText(item, 'title');
    062      feed.link = getChildsText(item, 'link');
    063      feed.description = getChildsText(item, 'description');
    064     parseForCityState(feed);
    065
    066     geoCode(feed);
    067     feeds.push(feed);
    068
    069     });
    070     placeOnMap(feeds);
    071    }
    072
    073    function placeOnMap(feeds) {
    074     var locToFeed = new Hash();
    075     feeds.each(function(feed) {
    076     if(feed.geoAddress) {
    077       var thisKey = feed.geoAddress.getMQLatLng().toString();
    078     if(locToFeed.keys().indexOf(thisKey) > -1) {
    079       locToFeed.get(thisKey).push(feed);
    080     } else {
    081         var a = new Array();
    082         a.push(feed);
    083         locToFeed.set(thisKey, a);
    084       }
    085      }
    086     });
    087
    088    locToFeed.values().each(function(feeds) {
    089      var html = '';
    090      feeds.each(function(feed) {
    091       html += "<a href='#{link}'>#{title}</a>
              <br>".interpolate(feed);
    092      });
    093      var poi = new MQPoi(feeds[0].geoAddress.getMQLatLng());
    094      poi.setInfoTitleHTML(feeds[0].city);
    095     poi.setInfoContentHTML(html);
    096     g_mqMap.addPoi(poi);
    097     });
    098    }
    099
    100    function geoCode(feed) {
    101     if(feed.city != '') {
    102     var address = new MQAddress();
    103     address.setCity(feed.city);
    104     address.setState(feed.state);
    105     address.setCountry(feed.country);
    106
    107     var gaCollection = new MQLocationCollection("MQGeoAddress");
    108     g_geoExec.geocode(address, gaCollection);
    109     feed.geoAddress = gaCollection.get(0);
    110    }
    111    }
    112
    113    function parseForCityState(feed) {
    114     var match = feed.description.match(/^([^(]+)\(AP\)/);
    115     if(match) {
    116      var cityAndMaybeState = match[1].split(',');
    117      feed.city = cityAndMaybeState[0];
    118      if(cityAndMaybeState.length > 1) {
    119        feed.state = cityAndMaybeState[1];
    120      }
    121     }
    122    }
    123
    124    function getChildsText(item, whichChild) {
    125     var child = Element.select(item, whichChild)[0];
    126     return Element.cleanWhitespace(child).firstChild.nodeValue;
    127    }
    128   </script>
    129  </head>
    130
    131  <body onload="startMap();">
    132   <h1>MapNews</h1>
    133   <hr>
    134   <div id="mapWindow" style=""></div>
    135   <hr>
    136  </body>
    137 </html>
    
  • MapNews - A Map Based News Browser - Part 2 - Initial Design

    "If you don't know where you are going, any road will get you there." - Lewis Carroll

    Like many developers I often start a project by jumping in and writing code. For this project I'm going try something a little different and create a road map for my mapping project.

    My project is named MapNews. In my prior post, I covered the project concept: present a map with markers/information windows which show news headlines by location.

    Doing the design up front is a bit of a jump off of a cliff for me. I've had only a light reading of the MapQuest Platform documentation and am going to proceed to layout the design. This should be interesting; as the project unfolds, I'll be able to look back and see my misconceptions exposed.

    Without further ado, I present the following sketch of my design:

    One implicit choice I've made here is to use JavaScript on the client to access the MapQuest functionality. Other language choices and/or server side implementation are allowed as the MapQuest Platform has bindings for Java, .NET, CPP, and ActionScript.

    The server will hold RSS feeds arranged by topics such as National or Business news. A complete system would include a server side process to collect and cache RSS feeds. I'm focusing on the MapQuest API so I may not build out this component. Instead, I'll just stage a couple of RSS XML files on the server.

    I plan to fetch the RSS feeds with an AJAX component after the initial load of index.html to the browser. I'll also need some JavaScript to parse the RSS feeds and discover which items have associated locations and then geocode the locations via the MapQuest API. MapNews JavaScript will also have to invoke the MapQuest JavaScript API to create the map and place markers and information windows on the map.

    I've shown the processes executing on the client. However, the physical location of the JavaScript files is also important and is driven by browser security restrictions (scripts can only call back to the domain from which they are served).

    For basic maps the MapQuest (MQ) JavaScript files can be directly included from a MapQuest server. For more complex functionality such as searching and routing the MapQuest JavaScript files need to be located on your server. The MQ JavaScript files then communicate with MapQuest servers via a proxy located on your server. MapQuest provides proxy implementations in several languages (Java, PHP5, .Net, ROR, and Flash). I have PHP on my server so that's an easy choice.

    That is about all I have in my initial design. The next steps are to get the infrastructure set up. I plan to follow the instructions in the MapQuest Advantage Developer Guide to configure and install the proxy. Then, I am off to the fun parts of writing the RSS parsing code and using the MQ API to bring MapNews to life.

  • MapNews - A Map Based News Browser

    Part 1 - Concept and Prototype

    "You've got your chocolate in my peanut butter! You've got your peanut butter on my chocolate!" - Reese's Peanut Butter Cups commercial.

    Two guys in the Reese's commercial found out how much fun it is to combine your two favorite things. I, too, have discovered this phenomenon. For me, the combination is a mashup of maps and news. While this mixture might not be quite as good as a Reese's Peanut Butter Cup, it's pretty close in my book.

    I have always enjoyed the newspaper section where you can read about what is going on your state or other states. Following this concept, my idea is to display a map with markers and information windows on places for which news is available. The information window will show headlines and allow click-through to the underlying story.

    The source of news I plan to use is AOL RSS news feeds. The RSS feeds often have a place name associated with a news items. I'll extract and geocode the locations and then populate the map with markers for newsworthy places.

    This is my first time using the MapQuest APIs so I tried a few baby steps to get the development process moving. I created the following static prototype of MapNews:

    This prototype shows details for a particular zip code. I've decided that this isn't quite the right model as this is pulling in news for a selected location. It seems to me that a better model is a push model. The map should be zoomed out to the entire US to show which locations have associated news. You can then click on the locations that appeal to you and view the headlines.

    The other concept shown is selection of news channels (national, business, and sports). These should map directly to specific RSS feeds, so I'll keep that.

    The process for building this prototype was straightforward. I created an account at the MapQuest Technical Resource Center and obtained an API key and downloads of the Javascript API and documentation. I was able to produce this prototype after reading the first couple of pages in the Advantage API Javascript Developer Guide. I, therefore, won't cover those details here.

    I worked with the HTML page on my local file system without moving the page to a web server. This required configuring my account to allow blank referers. After making the account changes, it takes an hour or so for the settings to propagate to the MapQuest servers. Allowing blank referrers should be avoided as it allows anyone to obtain and use your API key. Correcting this is high on my To Do list as I build out MapNews.

    The next step is developing a design to identify the components and architecture I'll use to bring MapNews to life.

  • Customize Your Trail Maps Using MapQuest and KML

    I've always liked maps, but I've always been disappointed with the content. Sure, knowing road names and where to find a gas station is great. But I'm an outdoors kind of guy, and what I really want to see is cool biking and hiking routes. That's why I'm so excited about Keyhole Markup Language (KML) and the MapQuest Platform; the combination of the two lets users, as opposed to mapmakers, supply map content. Overlaying a map with KML data opens up many possibilities for uploading, sharing, and finding user-generated content. Couple this ability to share data with the vast number of people carrying GPS-enabled devices and you have a perfect storm for sharing off-road routes.

    In this article, I show how to read KML files and create map features by using the MapQuest JavaScript API. The example I'm presenting is a KML file that describes the route for the Appalachian Trail (AT) and the location of shelters hikers use for overnight stays. I've focused on my favorite part of the trail, which is the section between Front Royal, VA, and Harper's Ferry, West Virginia. The source code for the example is listed at the end of this article. The following screen shot shows my custom AT map in action:

    Figure 1. The AT from Front Royal, VA, to Harper's Ferry, WV, showing the location of the trail and overnight shelters

    The trail route is shown in red. The overnight shelters are shown as Point of Interest (POI) stars. Clicking a star opens an information window that lists more details about the shelter:

    Figure 2. Clicking the POI marker opens an information window that lists details about the shelter

    The last screen shot shows the map after zooming in and changing to an aerial image view:

    Figure 3. Aerial view of the David Lesser shelter

    This screen shot shows that the David Lesser shelter is located east of the trail. The screen shot gives just a suggestion of the excellent view from the shelter's porch looking out over the farmlands of Northern Virginia. Just in case you can't tell, I think the David Lesser shelter is the nicest one in this section of the AT.

    In the remainder of this article, I cover some background on KML and the KML files I used to prepare the maps shown here. The following section covers the code and the experiences, some good and some bad, that I had while developing my custom AT map.

    KML

    Wikipedia describes KML as an XML-based schema for expressing geographic annotations of online maps in two or three dimensions. KML is just one of many file formats for interchanging map annotations, which also includes formats for data recorded by GPS-enabled devices. The nice thing about KML is that it is widely supported and has been submitted to the Open Geospatial Consortium for standardization.

    A couple of examples of widespread KML use are 1) searching KML files on AOL if you are interested in a particular topic and 2) aggregation sites that syndicate user-generated content in KML format such as virtualglobetrotting.com, mapufacture.com, and bikely.com. If you have data in a particular format, you can use GPSBabel to convert to and from KML and other data file formats.

    A KML file typically contains elements that describe features, such as camera viewing angles and elevation, that are not relevant for the two-dimensional maps generated with the MapQuest Platform. My approach was to ignore these elements. The code in this article recognizes KML Placemark elements that have a child Point geometry or a LineString geometry. This is best explained by looking an extract from the KML file for the AT data:

    0172 <kml xmlns="http://earth.google.com/kml/2.1">
    0173 <Document>
    0174   <name>Appalachian Trail Centerline</name>
    0175   <description>Exported from at_centerline.shp 3/12/02 downloaded from
    0176     http://www.appalachiantrail.org using City of Portland's Export to
    	   KML 2.3.5
    0177     Go Hokies!
    0178   </description>
    0179   <LookAt>
    0180     <longitude>-78.92943061782194</longitude>
    0181     <latitude>37.87708055555556</latitude>
    0182     <altitude>0</altitude>
    0183     <range>1067501.314867445</range>
    0184     <tilt>32.73615635179154</tilt>
    0185     <heading>1.815747767697585</heading>
    0186   </LookAt>
    0187   <Folder>
    0188     <name>Features</name>
    0189
    0190 <!-- Routes -->
    0191
    0192 <Placemark>
    0193       <name>1778</name>
    0194       <styleUrl>#FEATURES_copy18</styleUrl>
    0195       <LineString>
    0196         <tessellate>1</tessellate>
    0197         <coordinates>
    0198 -78.0494085970235,38.9169067777347,0 -78.0494323854585,38.9169149040082,0 ...
    	 </coordinates>
    0199       </LineString>
    0200     </Placemark>
    
    ...
    
    1004 <!-- Shelters -- >
    1005
    1006     <Placemark >
    1007       <description>Notes:  Sheltered picnic table, fireplace, and privy;
    1008       spring
    1009
    1010 Fee:  No
    1011
    1012 Capacity:  6
    1013
    1014 Maintained By:  Potomac Appalachian Trail Club</description>
    1015       <name>David Lesser Shelter</name>
    1016       <View>
    1017         <longitude>-77.77954</longitude>
    1018         <latitude>39.22718</latitude>
    1019         <range>999.9999999999999</range>
    1020         <tilt>0</tilt>
    1021         <heading>0</heading>
    1022       </View>
    1023       <visibility>1</visibility>
    1024       <styleUrl>#cloned1</styleUrl>
    1025       <Point>
    1026         <coordinates>-77.77954,39.22718,607.9747496823218</coordinates>
    1027       </Point>
    1028       <styleURL>#cloned0</styleURL>
    1029     </Placemark>
    
    ...
    
    1182   </Folder>
    1183 </Document>
    1184 </kml>
    

    Lines 179-186 describe a viewing angle for a three-dimensional viewer. Lines 192-200 are a Placemark that describes a section of the trail route. The example code maps this Placemark to a line overlay on the map. The second Placemark, on lines 1006-1026, describes a shelter and associated three-dimensional location coordinates (on lines 1025-1027). I mapped these elements to a POI and used the longitude and latitude elements of the coordinates.

    There are a few features of KML to keep in mind when writing code to consume KML. The first is that the file can be quite large and KML files are often stored in a zipped file with a .kmz extension. The file for the AT trail route is close to 15 MB. Therefore, the code to traverse KML needs to be as efficient as possible. The other feature of KML worth noting is that container elements such as Folder can be nested. KML document object model (DOM) tree-traversal routines need to be ready for arbitrary nesting of elements.

    The AT trail data I used came in two files: one for shelter locations and one for the center line of the trail route. The trail route file was just too large to handle with client-side JavaScript. To construct a reasonable-size sample, I extracted features lying between Front Royal and Harper's Ferry using the Perl XML::XPath library and consolidated the extracted features into one shorter file. The following listing shows the Perl code I used to extract Placemark elements for my region of interest:

    #!/usr/bin/perl -w
    
    use strict;
    use XML::XPath;
    use XML::XPath::XMLParser;
    
    my $xp = XML::XPath->new(filename => 'doc.kml');
    my $placemarks = $xp->find('/kml/Document/Folder/Placemark');
    foreach my $placemark ($placemarks->get_nodelist) {
      my $coordinates = $xp->find('./LineString/coordinates', $placemark);
      foreach my $c ($coordinates->get_nodelist) {
        my $text_nodes = $xp->find('child::text()', $c);
        my $sv = '';
        foreach my $t ($text_nodes->get_nodelist) {
          $sv .=  $t->string_value;
        }
        $sv =~ s/^\s*//;
        my @coords = split /\s+/, $sv;
        my ($lng, $lat, @rest) = split ',', $coords[0];
    
        #harpersFerry - (39.32, -77.72) - frontRoyal - (38.90, -78.05);
        if( (38.90 <= $lat) and ($lat <= 39.32) and
            (-78.05 <= $lng) and ($lng <= -77.72)) {
          print XML::XPath::XMLParser::as_string($placemark) . "\n";
        }
      }
    }
    

    KML Import JavaScript Code

    I broke the task of reading the KML DOM document into two classes: TreeWalker (lines 28-61) and MapMediator (lines 64-159). The TreeWalker class is responsible for traversing the DOM tree and passing MapMediator interesting KML nodes. The MapMediator class is responsible for creating MapQuest objects on the map for the corresponding KML elements.

    As I mentioned earlier, KML files can be quite large, so I made a couple of optimizations for efficient KML DOM tree traversal. The processing model I use here is client-side JavaScript, so there will be limits to the reasonable file size; however, I aimed to accommodate large files.

    To support large files, the first decision I made was to make only one pass through the KML DOM. This choice is opposed to a pull type of model in which one queries the DOM for interesting elements. (Examples of pull processing might include calls such as document.getElemntByTagName() or JavaScript Prototype Framework methods like $('Placemark Point');.) Each pull query might involve traversing all or part of the tree multiple times, which a one-pass approach avoids.

    To support a one-pass model, I implemented a depth-first, document-order traversal that scans the KML DOM in the same order as I tend to read the XML (top to bottom). The algorithm is as follows: 1) visit the first node (and send the node to MapMediator), 2) push the node's children onto a stack in reverse document order, and 3) pop the stack and repeat from step 1. The main route for the traversal is:

    0050         while(this.stack.size() > 0) {
    0051           var node = this.stack.pop();
    0052           this.handler.handleElement(node);
    0053           if(this.handler.prune(node)) continue;
    0054           if(node.hasChildNodes()) {
    0055             for(var i = node.childNodes.length; i > 0; i--) {
    0056               this.pushIfElement(node.childNodes.item(i - 1));
    0057             }
    0058           }
    0059         }
    

    The second optimization I made was to pass only element nodes to MapMediator (see MapMediator pushIfElement() on Line 37). This approach avoids many method calls for text and attribute nodes. MapMediator can always directly query for these types of nodes when it receives a DOM element.

    The final optimization I made for traversing the tree involves the idea of pruning the tree traversal. When processing elements such as a KML Placemark, I noticed that there are many child elements that are not relevant for a map. It looked to be more efficient for MapMediator to query directly for the relevant children when passed a Placemark element. After TreeWalker allows MapMediator to visit an element, it calls the MapMediator.prune(element) (line 53). If MapMediator returns true, the tree traversal is pruned at that element.

    The time to traverse the 300 Placemarks in the AT shelter file was less than half a second. I wasn't able to traverse the full AT route file in a reasonable amount of time. That's fine, because processing a 15 MB XML file in the browser doesn't make much sense.

    The callback from TreeWalker to MapMediator is handleElement():

    0070       // callback from TreeWalker
    0071       handleElement: function (node) {
    0072         if (node.nodeType == Node.ELEMENT_NODE) {
    0073           if('Placemark'.toLowerCase() == node.nodeName.toLowerCase()) {
    0074             this.placemark(node);
    0075           }
    0076         }
    0077       },
    

    As I mentioned earlier, MapMediator creates MapQuest objects for Placemarks elements only. The code for creating MapQuest objects from KML elements is contained in the method placemark():

    
    0088       // Create map objects for place marks
    0089       placemark: function(node) {
    0090
    0091         // find interesting children of placemark
    0092         var name = this.getChildText(this.getFirstChild(node, 'name'));
    0093         var description = this.getChildText(this.getFirstChild(node,
    		 'description'));
    0094         var point = this.getFirstChild(node, 'Point');
    0095         var lineString = this.getFirstChild(node, 'lineString');
    0096         var rawCoordinates = this.getChildText(
    0097           this.getFirstChild(point ? point : lineString, 'coordinates')
    0098         );
    0099         var coordinates = this.parseCoordinates(rawCoordinates);
    0100
    0101         // Create a MQ POI for KML points and MQ line overlay for KML
    		 LineStrings
    0102         // KML is (lng,lat), MQ is (lat,lng)
    0103         if(point) {
    0104           var poi = new MQPoi(new MQLatLng(coordinates[1], coordinates[0]));
    0105           poi.setInfoTitleHTML(name ? name : '');
    0106           poi.setInfoContentHTML(description ? description : '');
    0107           this.map.addPoi(poi);
    0108         } else if(lineString) {
    0109           var points = new MQLatLngCollection();
    0110           for(var i = 0; i < coordinates.length; i = i + 3) {
    0111             points.add(new MQLatLng(coordinates[i + 1],coordinates[i]));
    0112           }
    0113           var lineOverlay = this.getLineOverlay();
    0114           lineOverlay.setShapePoints(points);
    0115           this.map.addOverlay(lineOverlay);
    0116         }
    0117       },
    

    Lines 92-99 pull the child elements of the Placemark element that will be used when creating the corresponding MapQuest object. For POIs, the name and description are used to populate the information window. In both cases, the coordinates are fetched for locating the MapQuest object. For a lineString element, the coordinates are an array of (longitude, latitude, and altitude) tuples. POI markers are generated for KML points in lines 104-107. Lines 109-115 add a line overlay for the AT route information.

    The balance of the MapMediator class is given over to utility routines. The method getLineOverlay() returns a template MQLineOverlay with styling information and no location information. The method parseCoordinates() converts the text contained within the KML coordinates elements into a flat list of floats; each set of three elements in the list corresponds to a KML location tuple.

    The Long Story

    When I started this project, I envisioned that I would load the KML file using an Asynchronous JavaScript and XML (AJAX) call after the map was created. This means that MapQuest would show the map first, then add in POI markers and line overlays after the map was rendered. I figured this would keep the user happy during a potentially long KML parsing cycle.

    I started with adding the POI markers for the shelters first. I used the Prototype AJAX class to fetch the KML file. After configuring my server's content-type header for KML files to ''application/vnd.google-earth.kml+xml" the AJAX parsing worked well. The main stumbling block I had was that Prototype's DOM traversal routines didn't seem to work if the XML DOM tree was not made part of the document DOM tree. I got past this problem by not using the Prototype routines and instead using the standard DOM API.

    Then, I included code to add the AT route as a series of MapQuest line overlays and found out that my AJAX fetching of the KML wouldn't work anymore. The reason for this problem is tied to MapQuest's use of Dojo for maps with overlays. Map creation happens during a callback from Dojo (see MQInitDojo() on line 165). Dojo is loaded by the MapQuest API, so calling my server for the KML file from Dojo event callbacks was not possible. Without using Dojo event callbacks, I couldn't be sure of when the map was created and initialized.

    One solution might be to fetch and parse the KML and store an intermediate representation in the browser before loading the map. Then, in the map creation callback, use the stored representation to populate the map. Another solution might involve loading Dojo from your server and possibly a proxy to call out for KML files in other domains.

    In the example code, I simply included the KML in a hidden <div> in the HTML document. This solution seemed to work well enough and is a reasonable approach, especially if you have fixed content that can be included via server-side templates. I did observe that some Prototype functions failed to match XML element names. Inspection in Firebug showed that the reported tag names were uppercase for some KML elements. This was strange; in any case, I resorted to String.toLowerCase() for tag name comparison.

    Conclusions

    I've demonstrated how to use the MapQuest JavaScript API to load basic map features from a KML file and create corresponding map objects. However, KML is a rich language and there are many more features that can be rendered on a MapQuest map. There are a few more features in KML for which consumption would be a straightforward extension of what I've presented here.

    I've covered the handling of basic KML elements for point and line annotations. There are several different geometries that could be handled with map overlays, such as linear rings and polygons. KML also has several elements for specifying style information, such as the color or weight of lines, and even the icons used for map elements.

    Even with the minimal amount of KML elements that I've processed, I'm very happy with the maps I was able to create. I look at the AT maps I created and have to say, "Those are my kind of maps." Have fun with the new API and enjoy your own custom map.

    Full Source Code Listing

    0001 <html>
    0002   <head>
    0003     <title>MapQuest KML Import</title>
    0004     <link rel="stylesheet" href="kml.css" type="text/css"/>
    0005     <script
    	src="http://btilelog.access.mapquest.com/tilelog/transaction?transaction
    	=script&key=mjtd%7Clu6y290rn9%2C22%3Do5-0utn1&ipr=false&itk=true&v=5.2.0"
    	 type="text/javascript"></script>
    0006     <script src='prototype-1.6.0.2.js' type='text/javascript'></script>
    0007     <script language="javascript">
    0008
    0009       function initMap()
    0010       {
    0011         var mapCenter = new MQLatLng(39.11, -77.89);
    0012         var map = new MQTileMap(document.getElementById('mapWindow'), 7,
    		 mapCenter, "map");
    0013
    0014         map.addControl(new MQLargeZoomControl(map));
    0015         var vc = new MQViewControl(map);
    0016         map.addControl(vc, new MQMapCornerPlacement(
    0017           MQMapCorner.BOTTOM_RIGHT, new MQSize(20,20)
    0018         ));
    0019
    0020         try {
    0021           new TreeWalker($('kml'),  new MapMediator(map)).depthFirstWalk();
    0022         } catch (e) {
    0023           alert(e);
    0024         }
    0025       }
    0026
    0027     // Class to traverse KML DOM
    0028     function TreeWalker(startNode, handler){
    0029       this.startNode = startNode;
    0030       this.handler = handler;
    0031     }
    0032
    0033     TreeWalker.prototype = {
    0034       stack: null,
    0035
    0036       // process only element nodes
    0037       pushIfElement: function(node) {
    0038         if(node.nodeType == Node.ELEMENT_NODE) this.stack.push(node);
    0039       },
    0040
    0041       // Depth first, document order traversal of DOM
    0042       depthFirstWalk: function() {
    0043         this.stack = [];
    0044         this.handler.handleElement(this.startNode);
    0045         if(this.startNode.hasChildNodes()) {
    0046           for(var ci = this.startNode.childNodes.length; ci > 0; ci--) {
    0047             this.pushIfElement(this.startNode.childNodes.item(ci - 1));
    0048           }
    0049         }
    0050         while(this.stack.size() > 0) {
    0051           var node = this.stack.pop();
    0052           this.handler.handleElement(node);
    0053           if(this.handler.prune(node)) continue;
    0054           if(node.hasChildNodes()) {
    0055             for(var i = node.childNodes.length; i > 0; i--) {
    0056               this.pushIfElement(node.childNodes.item(i - 1));
    0057             }
    0058           }
    0059         }
    0060       }
    0061     }
    0062
    0063     // Class to create MQ objects from KML elements
    0064     function MapMediator(map) {
    0065       this.map = map;
    0066     }
    0067
    0068     MapMediator.prototype = {
    0069
    0070       // callback from TreeWalker
    0071       handleElement: function (node) {
    0072         if (node.nodeType == Node.ELEMENT_NODE) {
    0073           if('Placemark'.toLowerCase() == node.nodeName.toLowerCase()) {
    0074             this.placemark(node);
    0075           }
    0076         }
    0077       },
    0078
    0079       // stop tree walk at placemarks
    0080       prune: function(node) {
    0081         var shouldPrune = false;
    0082         if('Placemark' == node.nodeName) {
    0083           shouldPrune = true;
    0084         }
    0085         return shouldPrune;
    0086       },
    0087
    0088       // Create map objects for place marks
    0089       placemark: function(node) {
    0090
    0091         // find interesting children of placemark
    0092         var name = this.getChildText(this.getFirstChild(node, 'name'));
    0093         var description = this.getChildText(this.getFirstChild(node,
    		 'description'));
    0094         var point = this.getFirstChild(node, 'Point');
    0095         var lineString = this.getFirstChild(node, 'lineString');
    0096         var rawCoordinates = this.getChildText(
    0097           this.getFirstChild(point ? point : lineString, 'coordinates')
    0098         );
    0099         var coordinates = this.parseCoordinates(rawCoordinates);
    0100
    0101         // Create a MQ POI for KML points and MQ line overlay for KML
    			LineStrings
    0102         // KML is (lng,lat), MQ is (lat,lng)
    0103         if(point) {
    0104           var poi = new MQPoi(new MQLatLng(coordinates[1], coordinates[0]));
    0105           poi.setInfoTitleHTML(name ? name : '');
    0106           poi.setInfoContentHTML(description ? description : '');
    0107           this.map.addPoi(poi);
    0108         } else if(lineString) {
    0109           var points = new MQLatLngCollection();
    0110           for(var i = 0; i < coordinates.length; i = i + 3) {
    0111             points.add(new MQLatLng(coordinates[i + 1],coordinates[i]));
    0112           }
    0113           var lineOverlay = this.getLineOverlay();
    0114           lineOverlay.setShapePoints(points);
    0115           this.map.addOverlay(lineOverlay);
    0116         }
    0117       },
    0118
    0119       // convert KML coordinate string to a flat array of floats.
    0120       parseCoordinates: function(coordinateString) {
    0121         var coordinates = [];
    0122         var tuples =
    0123           coordinateString.replace(/^\s*/, '').replace(/\s*$/,
    		 '').split(/\s+/);
    0124         for(var i = 0; i < tuples.length; i++) {
    0125           var strs = tuples[i].split(',');
    0126           for(var j = 0; j < 3; j++) {
    0127             if(j < strs.length) {
    0128               coordinates.push(parseFloat(strs[j]));
    0129             } else {
    0130               // elevation is optional in KML
    0131               coordinates.push(undefined);
    0132             }
    0133           }
    0134         }
    0135         return coordinates;
    0136       },
    0137
    0138       // return a line overlay without points
    0139       getLineOverlay: function() {
    0140           var lineOverlay = new MQLineOverlay();
    0141           lineOverlay.setColor('#FF0000');
    0142           lineOverlay.setColorAlpha(1.0);
    0143           lineOverlay.setBorderWidth(2);
    0144           return lineOverlay;
    0145       },
    0146
    0147       // fetch text nodes contained by node
    0148       getChildText: function (node) {
    0149           return node
    0150             ? Element.cleanWhitespace(node).firstChild.nodeValue
    0151             : undefined;
    0152       },
    0153
    0154       // find a child element by tag name
    0155       getFirstChild: function(node, tagName) {
    0156         var found = node.getElementsByTagName(tagName).item(0);
    0157         return found;
    0158       }
    0159     }
    0160
    0161     </script>
    0162   </head>
    0163   <body>
    0164     <script>
    0165       MQInitDojo(initMap);
    0166     </script>
    0167     <h1>MapQuest KML Import</h1>
    0168     <hr>
    0169     <div id="mapWindow" style=""></div>
    0170     <hr>
    0171     <div id="kml">
    0172 <kml xmlns="http://earth.google.com/kml/2.1">
    0173 <Document>
    0174   <name>Appalachian Trail Centerline</name>
    0175   <description>Exported from at_centerline.shp 3/12/02 downloaded from
    0176     http://www.appalachiantrail.org using City of Portland's Export to
    		 KML 2.3.5
    0177     Go Hokies!
    0178   </description>
    0179   <LookAt>
    0180     <longitude>-78.92943061782194</longitude>
    0181     <latitude>37.87708055555556</latitude>
    0182     <altitude>0</altitude>
    0183     <range>1067501.314867445</range>
    0184     <tilt>32.73615635179154</tilt>
    0185     <heading>1.815747767697585</heading>
    0186   </LookAt>
    0187   <Folder>
    0188     <name>Features</name>
    0189
    0190 <!-- Routes -->
    0191
    0192 <Placemark>
    0193       <name>1778</name>
    0194       <styleUrl>#FEATURES_copy18</styleUrl>
    0195       <LineString>
    0196         <tessellate>1</tessellate>
    0197         <coordinates>
    0198 -78.0494085970235,38.9169067777347,0 -78.0494323854585,38.9169149040082,0 ...
    	 </coordinates>
    0199       </LineString>
    0200     </Placemark>
    
    ...
    
    1004 <!-- Shelters -- >
    1005
    1006     <Placemark >
    1007       <description>Notes:  Sheltered picnic table, fireplace, and privy;
    1008  spri
    1009
    1010 Fee:  No
    1011
    1012 Capacity:  6
    1013
    1014 Maintained By:  Potomac Appalachian Trail Club</description>
    1015       <name>David Lesser Shelter</name>
    1016       <View>
    1017         <longitude>-77.77954</longitude>
    1018         <latitude>39.22718</latitude>
    1019         <range>999.9999999999999</range>
    1020         <tilt>0</tilt>
    1021         <heading>0</heading>
    1022       </View>
    1023       <visibility>1</visibility>
    1024       <styleUrl>#cloned1</styleUrl>
    1025       <Point>
    1026         <coordinates>-77.77954,39.22718,607.9747496823218</coordinates>
    1027       </Point>
    1028       <styleURL>#cloned0</styleURL>
    1029     </Placemark>
    
    ...
    
    1182   </Folder>
    1183 </Document>
    1184 </kml>
    1185     <div>
    1186   </body>
    1187 </html>