Menu

Seattle Movie Finder: An AJAX- and REST-Powered Virtual Earth Mashup

March 1, 2006

Dare Obasanjo

I am a big fan of movies, especially summer blockbusters. Last summer I saw Fantastic Four, War of the Worlds, Batman Begins and Mr. and Mrs. Smith. Every Friday I visit sites like MSN Movies and IMDB to learn what new movies are available in my neighborhood and in what theaters they will be showing. However, I dislike the user interface of every movie website I've ever used, particularly when it comes to determining what movies are showing in my vicinity. Few, if any, of these sites give a good visual representation of the proximity of the theaters to my location. And it's often hard to tell how many different theaters are showing the movie I want to see that weekend.

I've always wanted a user interface that was map-based for browsing movie theater locations, and now thanks to the availability of the Virtual Earth Standard Map Control SDK I've been able to build one for myself. The Virtual Earth control enables developers to build applications using the same technology that powers Windows Live Local. It took me a few hours to figure out the Virtual Earth API and within a day I had produced the Seattle Movie Finder web page at http://www.25hoursaday.com/moviefinder. (I think this article describing Dare's mashup is generally interesting and useful, even though the service itself is currently hosted on a dynamic IP and is subject to going down. --Editor) The web page gives me a list of movies currently showing in the Seattle area, what movie theaters they are playing in, and their showtimes.

In this article, I explore how I built the Seattle Movie Finder application using XML, ASP.NET, and the Virtual Earth API.

Overview of Integrating with MSN Virtual Earth Using JavaScript

The first thing I had to learn was how to embed a Virtual Earth map on a web page. This turned out to be quite straightforward. The first step is to include the Virtual Earth map control and associated style sheet into your web page. Once the map control is included in your page, creating an instance of the map control simply requires invoking the Msn.Ve.MapControl constructor. The following example creates a 600 by 400 map centered on Seattle, Washington.

  <html>

    <head>

      <title>My Virtual Earth Sample</title>   



        <![if !IE]><script src="http://local.live.com/JS/AtlasCompat.js"></script><![endif]>

        <link href="http://dev.virtualearth.net/standard/v2/MapControl.css" type="text/css" rel="stylesheet" />

        <script src="http://dev.virtualearth.net/standard/v2/MapControl.js"></script> 

      <script>

        var map = null;



        function OnPageLoad()

        {

             var params = new Object();

             params.latitude = 47.71;

             params.longitude = -122.32;

             params.zoomlevel = 10;

             params.mapstyle = Msn.VE.MapStyle.Road;

             params.showScaleBar = true;

             params.showDashboard = true;

             params.dashboardSize = Msn.VE.DashboardSize.Normal;

             params.dashboardX = 5;

             params.dashboardY = 5;



             map = new Msn.VE.MapControl(document.getElementById("myMap"), params);

           map.Init();

        }

      </script>

    </head>

    <body onload="OnPageLoad()">

      <div id="myMap" style="WIDTH: 600px; HEIGHT: 400px; OVERFLOW:hidden">    </div>

   </body>

  </html>

This example should be straightforward to follow. The first few lines include directives to include the Virtual Earth JavaScript control as well as the associated CSS style sheet. There is also a conditional statement which loads some Microsoft Atlas libraries if the user's browser is not Internet Explorer. The OnPageLoad() method contains the code for creating an instance of a Virtual Earth map, specifying the parameters for the embedded map, and making it visible on the page.

Methods and Events on the Virtual Earth Map Control

The Msn.VE.MapControl object has a number of methods which are documented in the Virtual Earth Standard Map Control SDK documentation. The following table lists the methods available on the Msn.VE.MapControl object and their behaviors.

Method Description

Msn.VE.MapControl._constructor(map, params);

Creates a Virtual Earth map in an HTML container. Latitude, longitude and zoom level can be specified as the default when the map loads

AddPushpin(id, latitude, longitude, width, height, className, innerHtml, zIndex);

Adds a pushpin to the map at a specified location. Text and user-defined CSS styles can be added to the pushpin

AttachEvent(event, function);

Attaches a map control event to a specified function

ClearPushpins();

Clears all the pushpins on the map

ContinuousPan(deltaX, deltaY, count);

Pans the map by the desired amount in a fluid motion

GetCenterLatitude();

Returns the current Latitude value of the map

GetCenterLongitude();

Returns the current Longitude value of the map

GetMapStyle();

Returns the current map style

GetMetersPerPixel(latitude, zoomLevel);

Returns the approximate number of meters on the globe represented by each pixel on the map, at the specified latitude and zoom level

GetObliqueScene();

Returns an ObliqueScene object for the Bird's Eye image at the center of the map

GetX(longitude);

Gets the x position for a longitude

GetY(latitude);

Gets the y position for a latitude

GetZoomLevel();

Gets the current zoom level

IncludePointInViewport(latitude, longitude);

Changes the map view (MapView object) to include the specified LatLong point and the current center point

Init();

Initializes a new instance of the Msn.VE.MapControl class

IsAnimationEnabled();

Indicates whether animated zooming and panning are enabled

IsObliqueAvailable();

Determines whether Bird's Eye imagery is available in the current map view

LatLongToPixel(LatLong, zoomLevel);

Converts a LatLong object (latitude/longitude pair) to the corresponding pixel (Pixel object) on the map

PanMap(deltaX, deltaY);

Moves the map by the desired amount

PanToLatLong(latitude, longitude);

Moves the position of the map to a specified latitude and longitude

PixelToLatLong(Pixel, zoomLevel);

Converts a Pixel object (point on the map) to a LatLong object (latitude/longitude pair)

RemovePushpin(id);

Removes a pushpin from the map using the specified identifier

Resize(width, height);

Changes the size of the map

SetAnimationEnabled();

Enables or disables animated zooming and panning

SetBestMapView();

Determines the best map view (MapView object) that completely includes all of the LatLong objects in the specified array, and updates the current map view with the new map view

SetCenter(latitude, longitude);

Centers the map to a desired latitude and longitude

SetCenterAndZoom(latitude, longitude, zoomLevel);

Centers the map to a specific latitude and longitude and sets the zoom level

SetMapStyle(mapStyle);

Changes the style of the map, to road, aerial, oblique, or hybrid

SetObliqueOrientation(orientation);

Changes the orientation of the existing Bird's Eye image (ObliqueScene object) to the specified orientation

SetObliqueScene(id);

Displays the Bird's Eye image specified by the ObliqueScene ID

SetView(mapView);

Changes the map to the specified MapView object

SetViewport(lat1, lon1, lat2, lon2);

Determines the best map view (MapView object) that completely includes the area within the specified LatLongRectangle, and updates the current map view with the new map view

SetZoom(zoomLevel);

Zooms the map to the specified level

StopContinuousPan();

Interrupts a continuous pan

ZoomIn();

Zooms the map in to the next level

ZoomOut();

Zooms the map out to the previous level

There are also a number of events which are supported by the map control.  

Event Description

onChangeView

Event fired whenever the map view changes

onClick

Event fired when the user clicks on the map

onContextMenu

Event fired when the user right-clicks on the map

onEndContinuousPan

Event fired when the map finishes the continuous pan

onEndZoom

Event fired when the zoom finishes

onError

Event fired when there is a map control error

onMapStyleChange

Event fired when the map style changes

onMouseUp

Event fired when the user releases the click

onObliqueChange

Event fired when the Bird's Eye image scene ID is changed. This event only fires if the map is currently displaying a Bird's Eye image and that image is changed

onObliqueEnter

Event fired when switching to Bird's Eye imagery from another map style

onObliqueLeave

Event fired when switching from Bird's Eye imagery to another map style

onResize

Event fired when the map is resized

onStartContinuousPan

Event fired when the map starts continuous pan

onStartZoom

Event fired when the zoom starts

GeoCoding 101: Street Addresses to Latitudes and Longitudes

After learning how to embed a Virtual Earth map on a web page, the next thing I had to learn was how to add to the map a pushpin that corresponded to a physical location. As shown in the table in the previous section, adding a pushpin is done via the AddPushpin() method. However, there was a problem: the AddPushpin() method takes a latitude and longitude as input, while I knew only the street addresses of the movie theaters. I needed a way to convert the physical addresses of the theaters to latitudes and longitudes. This process is called geocoding.

To convert the addresses of the various movie theaters to latitudes and longitudes, I used the free services provided by the geocoder.us website.The website provides several options for geocoding addresses, from entering addresses into a web form to using a choice of SOAP, XML-RPC, or REST web services for mapping addresses to latitudes and longitudes.Thus it was quite straightforward for me to write a program that took a list of movie theaters in the Seattle area and obtained their latitudes and longitudes. Once I had obtained their latitudes and longitudes, I created an XML document where the information would be stored for use by my Seattle Movie Finder application.

Below is the XML schema for the list of movie theaters used by my Seattle Movie Finder service.

      <xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">

          <xs:element name="theaters" type="MovieTheaters"/>

          <xs:complexType name="MovieTheaters">

              <xs:sequence>

                  <xs:element minOccurs="0" maxOccurs="unbounded" name="theater" type="MovieTheater"/>

              </xs:sequence>

          </xs:complexType>

          <xs:complexType name="MovieTheater">

              <xs:sequence>

                  <xs:element minOccurs="0" maxOccurs="1" name="name" type="xs:string"/>

                  <xs:element minOccurs="0" maxOccurs="1" name="address" type="xs:string"/>

                  <xs:element minOccurs="1" maxOccurs="1" name="lat" type="xs:double"/>

                  <xs:element minOccurs="1" maxOccurs="1" name="long" type="xs:double"/>

                  <xs:element minOccurs="0" maxOccurs="1" name="movies" type="ArrayOfMovie"/>

              </xs:sequence>

          </xs:complexType>

          <xs:complexType name="ArrayOfMovie">

              <xs:sequence>

                  <xs:element minOccurs="0" maxOccurs="unbounded" name="movie" type="Movie"/>

              </xs:sequence>

          </xs:complexType>

          <xs:complexType name="Movie">

              <xs:sequence>

                  <xs:element minOccurs="0" maxOccurs="1" name="name" type="xs:string"/>

                  <xs:element minOccurs="0" maxOccurs="1" name="times" type="ArrayOfString"/>

              </xs:sequence>

          </xs:complexType>

          <xs:complexType name="ArrayOfString">

              <xs:sequence>

                  <xs:element minOccurs="0" maxOccurs="unbounded" name="time" type="xs:string"/>

              </xs:sequence>

          </xs:complexType>

      </xs:schema>

And below is an example of the XML document representing the various movie theaters in the Seattle area.

  <theaters>

      <theater>

          <name>Cinerama 1</name>

          <address>2100 4th Ave., Seattle, WA, 98121</address>

          <lat>47.61402</lat>

          <long>-122.341337</long>

      </theater>

      <theater>

          <name>Pacific Place 11</name>

          <address>600 Pine S., Suite 400, Seattle, WA, 98101</address>

          <lat>47.612320</lat>

          <long>-122.335137</long>

      </theater>

      <theater>

          <name>Loews Meridian 16</name>

          <address>1501 7th Ave, Seattle, WA 98101</address>

          <lat>47.611820</lat>

          <long>-122.333137</long>

      </theater>

      <theater>

          <name>Loews Oak Tree Cinemas 6</name>

          <address>10006 Aurora Ave. N., Seattle, WA 98133</address>

          <lat>47.701563</lat>

          <long>-122.344545</long>

      </theater>

      <theater>

          <name>Loews Uptown</name>

          <address>511 Queen Anne Ave N., Seattle, WA, 98109</address>

          <lat>47.623647</lat>

          <long>-122.356631</long>

      </theater>

      <!-- more theaters left out due to space constraints -->

  </theaters>

Building the Seattle Movie Finder Service

A core part of every AJAX application is the service on the web server with which the web browser communicates. In most AJAX applications this is a simple URL-based service from which XML or JSON can be retrieved and then parsed on the client using JavaScript. In my application I needed a URL endpoint that could provide me two classes of data: all the movies playing in the Seattle area, and information about the movie theaters showing a specific movie. Below is a screenshot of the web page showing both classes of information.

Figure 1
Figure 1. Screenshot of search results for "King Kong"

On the server side there are two primary methods of interest. The first is the GetMovies() method, which returns the list of movies currently playing in the Seattle area, and the other is the GetMovieListings() method, which returns the theaters currently showing a particular movie. Both methods return a MovieTheaters object which is then sent to the browser as serialized XML. Below is a definition of the MovieTheaters class and its related classes.

  [System.Xml.Serialization.XmlRootAttribute("theaters", IsNullable=false)]



     public class MovieTheaters{

        [System.Xml.Serialization.XmlElementAttribute("theater", Type = typeof(MovieTheater), IsNullable = false)]

        public ArrayList theaterList = new ArrayList();

     }



     public class MovieTheater{

        public string name;

        public string address;

        [System.Xml.Serialization.XmlElementAttribute("lat")]

        public double latitdue;

        [System.Xml.Serialization.XmlElementAttribute("long")]

        public double longitude;

        [System.Xml.Serialization.XmlArrayAttribute(ElementName = "movies", IsNullable = false)]

        [System.Xml.Serialization.XmlArrayItemAttribute("movie", Type = typeof(Movie), IsNullable = false)]       

        public ArrayList movieList = new ArrayList();

        [System.Xml.Serialization.XmlIgnoreAttribute()]

        public Uri url;

     }



     public class Movie{

        public string name;

        [System.Xml.Serialization.XmlArrayAttribute(ElementName = "times", IsNullable = false)]

        [System.Xml.Serialization.XmlArrayItemAttribute("time", Type = typeof(System.String), IsNullable = false)]   

        public ArrayList times = new ArrayList();   

     }

The XML obtained from serializing an instance of the MovieTheaters class conforms to the XML schema provided in the previous section.

The information provided by the GetMovies() and GetMovieListings() methods is always at most one day old. However, instead of invoking an external service every time a user interacts with the Movie Finder page, the movie information is cached within the Seattle Movie Finder application unless it is over a day old, in which case external services are invoked.

The GetMovies() method is exposed as a RESTful web service by accessing the URL at http://www.25hoursaday.com/moviefinder/MovieFinder.aspx?showall=true. The code for the GetMovies() method is shown below.

  

  private XmlDocument GetMovies(){

    DateTime dateMovieListUpdated = DateTime.MinValue;

    object mlu = Cache.Get("MovieListUpdated" );



    if(mlu != null){

        dateMovieListUpdated = (DateTime) mlu;

          }



    TimeSpan sinceLastUpdate = DateTime.Now.Subtract(dateMovieListUpdated);

    TimeSpan oneDay = new TimeSpan( 1,0,0,0);

    XmlDocument movies = (XmlDocument) Cache.Get("MovieList" );



    if(sinceLastUpdate > oneDay){

        movies = MoviesService.GetMovieList();

        Cache["MovieListUpdated" ] = DateTime.Now;

        Cache["MovieList" ] = movies;

          }



    return movies;

   }

The GetMovieListings() method is exposed as a RESTful web service by accessing the URL at http://www.25hoursaday.com/moviefinder/MovieFinder.aspx?movie={0} where {0} is replaced with the name of the target movie such as http://www.25hoursaday.com/moviefinder/MovieFinder.aspx?movie=Firewall. The code for the GetMovieListings() method is shown below.

private void GetMovieListing( string movieName, XmlWriter writer){

           DateTime dateMovieListingsUpdated = DateTime.MinValue;

           object mlu = Cache.Get("MovieListingsUpdated");



           if(movieName.ToLower().StartsWith("the ")){

             movieName = movieName.Substring(4) + ", The" ;

           }



           if(mlu != null){

             dateMovieListingsUpdated = (DateTime) mlu;

           }



           TimeSpan sinceLastUpdate = DateTime.Now.Subtract(dateMovieListingsUpdated);

           TimeSpan oneDay = new TimeSpan(1,0,0,0);



           if(sinceLastUpdate > oneDay){

             theaters = MoviesService.FetchMovieListings();

             Cache["MovieListingsUpdated"] = DateTime.Now;

             Cache["MovieTheaters"] = theaters;

               }



          MovieTheaters mts = new MovieTheaters();



          foreach(MovieTheater mt in theaters.theaterList){

              foreach(Movie m in mt.movieList){



                 if(m.name == movieName){

                    MovieTheater mt2 = new MovieTheater();

                    mt2.name   = mt.name;

                    mt2.address = mt.address;

                    mt2.latitdue = mt.latitdue;

                    mt2.longitude = mt.longitude;

                    Movie m2 = new Movie();

                    m2.name = m.name;

                    m2.times = m.times;

                    mt2.movieList.Add(m2);

                    mts.theaterList.Add(mt2);

                    break;

                 }

              }

           }

           XmlSerializer serializer = new XmlSerializer( typeof(MovieTheaters));

           serializer.Serialize(writer, mts);

        }

Putting It All Together

There are two primary dynamic components of the Seattle Movie Finder page; the list of movies currently showing in the Seattle area and the locations of movie theaters on the map. The list of available movies is toggled by clicking the hyperlinked text that alternatively reads "Show Available Movies" and "Hide Available Movies" depending on whether the list of available movies is being shown or not. The locations of movie theaters are displayed on the map based on the last movie that was searched for by clicking the Locate Theaters button.

The function that toggles the list of movies is named ToggleMovies() and is referenced in the HTML for the hyperlink that toggles the list of movies, as shown below.

<a id="avail_movies_link" href="javascript:ToggleMovies()"

  oncontextmenu="return false">Show Available Movies</a>

The ToggleMovies() function is pretty straightforward; it changes the hyperlink text and attempts to download the list of available movies asynchronously. 

  function ToggleMovies(){

    var availmovies = document.getElementById("avail_movies_link");    



    if (availmovies.innerHTML == "Show Available Movies") {

           availmovies.innerHTML = "Hide Available Movies";

           if (movies != null) {

             document.getElementById("avail_movies").innerHTML = movies;

               } else {   

                   loadXMLDoc("http://www.25hoursaday.com/moviefinder/MovieFinder.aspx?showall=true");

               }

            } else {

               availmovies.innerHTML = "Show Available Movies";

               document.getElementById("avail_movies").innerHTML = "";

            }

     }

Once the movie list has been downloaded, it is inserted into the page by the ProcessMovies() method.

 function ProcessMovies(xmlDoc){

      var m = xmlDoc.selectNodes("//movie");

      var newContent = '';

      movies = document.createElement('temp');

      for (var i=0; i < m.length; i++) {

          newContent += (i + 1) + '. <a href=javascript:ShowMovieLocations("' + escape(GetTextValue(m[i]))

                + '")>' + GetTextValue(m[i]) + "</a><br>";

      }

     movies = newContent;

     var availmovies = document.getElementById("avail_movies");

     availmovies.innerHTML = newContent;    

  }

When the user selects a movie to search for, either by clicking on a movie name in the list of available movies or by typing a movie title into the search box, the ShowMovieLocations() function is invoked. The function is shown below.

  function ShowMovieLocations(movieName){

     map.ClearPushpins();

     htm();

     document.getElementById("moviename").value = movieName;        

     loadXMLDoc("http://www.25hoursaday.com/moviefinder/MovieFinder.aspx?movie=" + movieName);                  

  }

Once the list of movie theater locations and showtimes for the specified movie have been downloaded, they are processed by the ProcessTheaters() method, which iterates over each movie theater in the returned XML document and adds it to the map as a pushpin. The ProcessTheaters() function is shown below.

  function ProcessTheaters(xmlDoc){

      var theaters = xmlDoc.selectNodes("//theater");

      theaterInfo = new Array(theaters.length);



      for (var i=0; i < theaters.length; i++) {

          var t = theaters[i];

          var name = GetTextValue(t.firstChild) + " ";

          var address = GetTextValue(t.firstChild.nextSibling);

          var lat = GetTextValue(t.firstChild.nextSibling.nextSibling);

          var lon = GetTextValue(t.firstChild.nextSibling.nextSibling.nextSibling);

          var addressNtimes = "<br>Address: " + address + "<br>Showtimes: ";

          var times = t.getElementsByTagName("time");



          for (var j=0; j < times.length; j++) {

              addressNtimes += GetTextValue(times[j]) + " " ;

          }



          theaterInfo[i] = new Array(2);

          theaterInfo[i][0] = "Theater: " + name;

          theaterInfo[i][1] = addressNtimes;   

          //stm([ theaterInfo[i][0], theaterInfo[i][1]  ], TooltipStyle);

          //stm(theaterInfo[i], TooltipStyle);



          var markup = "<div class='pin' onMouseOver='showMovieInfo(theaterInfo[" + 

          i + "])' onMouseOut='clearMovieInfo()'>" + ( i + 1) + "</div>";  

          map.AddPushpin(

             'pushpin' + i, // id

             lat,        // latitude

             lon,     // longitude

             2,          // width

             2,          // height

             'bluepin',   // className

             markup,         // innerHtml

              3);

      }

  }

For completeness, the code for the helper functions loadXMLDoc() and ProcessReqChange(), which are used in the aforementioned JavaScript functions, is shown below.

var req;

  function loadXMLDoc(url) {

     req = false;

     if(window.XMLHttpRequest) {

        try {

               req = new XMLHttpRequest();

          } catch(e) {

           req = false;

          }

      // branch for IE/Windows ActiveX version

      } else if(window.ActiveXObject) {

           try {

                  req = newActiveXObject("MSXML2.XMLHTTP.3.0");

           } catch(e) {

           try {

                  req = newActiveXObject("Microsoft.XMLHTTP");

           } catch(e) {

                 req = false;

          }

        }

      }

      if(req) {

              req.onreadystatechange = ProcessReqChange;

                 req.open("GET", url, true);

                 req.send(null);

      }

  }



  function ProcessReqChange() {

      // only if req shows "loaded"



      if (req.readyState == 4) {

          // only if "OK"

          if (req.status == 200) {

             var doc  = req.responseXML;        

             if(doc.documentElement.nodeName == "movies"){

               ProcessMovies(doc);

             }else if(doc.documentElement.nodeName == "theaters"){

               ProcessTheaters(doc);

             }

              } else {

              alert("There was a problem retrieving the XML data:\n" +

              req.statusText);

          }

      }

  }

Conclusion

This is my first article on building mashups with Windows Live services and I had quite a lot of fun doing it. As I've shown, it doesn't take much more than moderate knowledge of using JavaScript and building RESTful web services to create an interesting mashup. Thanks to Steve Lombardi and Chandu Thota for their ideas and feedback while writing this article.