Seattle Movie Finder: An AJAX- and REST-Powered Virtual Earth Mashup
March 1, 2006
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 |
---|---|
Creates a Virtual Earth map in an HTML container. Latitude, longitude and zoom level can be specified as the default when the map loads |
|
|
Adds a pushpin to the map at a specified location. Text and user-defined CSS styles can be added to the pushpin |
Attaches a map control event to a specified function |
|
Clears all the pushpins on the map |
|
Pans the map by the desired amount in a fluid motion |
|
Returns the current |
|
Returns the current |
|
Returns the current map style |
|
Returns the approximate number of meters on the globe represented by each pixel on the map, at the specified latitude and zoom level |
|
Returns an |
|
Gets the |
|
Gets the |
|
Gets the current zoom level |
|
Changes the map view ( |
|
Initializes a new instance of the |
|
Indicates whether animated zooming and panning are enabled |
|
Determines whether Bird's Eye imagery is available in the current map view |
|
Converts a |
|
Moves the map by the desired amount |
|
Moves the position of the map to a specified latitude and longitude |
|
Converts a |
|
Removes a pushpin from the map using the specified identifier |
|
Changes the size of the map |
|
Enables or disables animated zooming and panning |
|
Determines the best map view ( |
|
Centers the map to a desired latitude and longitude |
|
Centers the map to a specific latitude and longitude and sets the zoom level |
|
Changes the style of the map, to road, aerial, oblique, or hybrid |
|
Changes the orientation of the existing Bird's Eye image ( |
|
Displays the Bird's Eye image specified by the |
|
Changes the map to the specified |
|
Determines the best map view ( |
|
Zooms the map to the specified level |
|
Interrupts a continuous pan |
|
Zooms the map in to the next level |
|
Zooms the map out to the previous level |
There are also a number of events which are supported by the map control.
Event | Description |
---|---|
|
Event fired whenever the map view changes |
|
Event fired when the user clicks on the map |
|
Event fired when the user right-clicks on the map |
|
Event fired when the map finishes the continuous pan |
|
Event fired when the zoom finishes |
|
Event fired when there is a map control error |
|
Event fired when the map style changes |
|
Event fired when the user releases the click |
|
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 |
|
Event fired when switching to Bird's Eye imagery from another map style |
|
Event fired when switching from Bird's Eye imagery to another map style |
|
Event fired when the map is resized |
|
Event fired when the map starts continuous pan |
|
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. 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.