In Lesson 4, you learned how to code info windows for each marker and how to display your markers using a custom icon. However, you were still hard-coding the locations of the markers in your JavaScript, a less-than-ideal arrangement. In Lesson 5, you'll learn how to read and overlay data stored in a variety of formats. The project deliverable will require you to map a dataset of your choosing.
In addition, I will discuss how to use the Google geocoder and give you a taste of database-driven mapping (covered in greater depth in an optional lesson), a topic you may want to explore for your final project.
At the successful completion of this lesson, students should be able to:
If you have any questions now or at any point during this week, please feel free to post them to the Lesson 5 Discussion Forum. (That forum can be accessed at any time by clicking on the Discussions tab within Canvas.)
Lesson 5 is one week in length. (See the Calendar in Canvas for specific due dates.) To finish this lesson, you must complete the activities listed below. You may find it useful to print this page out first so that you can follow along with the directions.
Step | Activity | Access/Directions |
---|---|---|
1 | Work through Lesson 5. | You are in the Lesson 5 online content now. The Overview page is previous to this page, and you are on the Checklist page right now. |
2 | Complete the project on the last page of the lesson.
|
Follow the directions throughout the lesson and on the last page. |
3 | Take Quiz 5 after you read the online content. | Go to the Canvas Homepage and click on the "Lesson 5 Quiz" link to begin the quiz. |
Way back in 1998, Esri published a technical description of their shapefile data format [1]. That description makes it possible for third parties to develop shapefile readers and conversion algorithms. One such reader that I recently came across [2] was developed by Mano Marks, a member of the Google Geo development team. Mano's JavaScript-based shapefileloader project relies heavily on two object classes, SHPParser (which parses the geometry information) and DBFParser (which parses the tabular information).
You could probably dissect Mano's shp_load.html sample and figure out how to use his Parser classes to build your own maps. However, I thought I would try to make that job a bit easier with a couple of my own examples (adapted from his) along with explanations of how they work.
The first shapefile loader example [3] reads data from the all-too-familiar Jen and Barry's candidate_cities shapefile. Let's have a look at its source code to see how it works.
One of the first things you should note in the source code is the referencing of two JavaScript libraries -- shp.js and dbf.js. These libraries house the SHPParser and DBFParser classes mentioned above.
You don't necessarily have to create your own shapefile-based maps since this lesson's assignment allows you to choose a different data format. However, if you plan on using the Parser examples discussed here, I strongly recommend you download my copies of the libraries here. (I found a bug in the copies hosted on the Google site and also added some code that is needed for the second example below to work, which is why I'm suggesting you use mine.)
The createMarker() function is basically the same as we saw in the previous lesson, which brings us to the initMap() function. After creation of the Map object, comes the part where the shp file and dbf file are opened by their respective parsers using the load() method. Each parser's load() method accepts the name of a file, a callback function that will process the data after it's been read in, and a function that will be executed if an error is encountered. The data processing callback for the shp file is shpLoad(); for the dbf file it is dbfLoad().
Looking at these two Load() functions, they are basically mirror images of one another. In shpLoad(), the geometry data read in from the shp file can be accessed through the sh variable. In dbfLoad(), the tabular data read in from the dbf file can be accessed through the db variable. Those two pieces of data are assigned to the variables shp and dbf, which were declared with global scope near the top of the document, so that they can be accessed easily by multiple functions. After that assignment, each function has an identical check to see if the two files have both been parsed. When that happens, it is time to execute the render() function.
The first four lines of the render() function are concerned with zooming in on the area covered by the shapefile. This is done by constructing a LatLngBounds object using the maxX, maxY, minX and minY properties of the object returned by the SHPParser. That LatLngBounds object is then passed to the Map's fitBounds() method.
Now we're ready to loop through the shapefile's geometries. The geometries can be accessed as an array by reading the records property. A loop running from 0 to the length of the records array is set up. Within the loop, the expression shp.records[i].shape retrieves the geometry of the record at position i in the array. The geometry is stored in the variable shape. The latitude and longitude of the point geometry are then obtained using the expressions shape.content.y and shape.content.x. These values are passed to the LatLng constructor to create an object of that class, stored in the variable pt.
The DBFParser is set up similarly, in that the object it returns has a records property for getting the array of tabular data. So, the expression dbf.records[i] returns the tabular record at position i, which is stored in the variable dbfRecord. That variable itself holds an array of values associated with row i of the table. The array is associative, which means specifying the desired column name can access a value in the array. For this map, I was interested in only the city name and population for the marker's info window.
Finally, the end of the document contains two Error() functions, which, as you'll recall, were listed when the two Parsers were invoked. These functions output a very basic error message to the JavaScript console in the event there is a problem reading one of the files.
Only the .shp and .dbf files are needed by the Parsers. Other components of a shapefile that are used by Esri software need not be uploaded to the web server for a mashup like this.
In testing the map above, you may have noticed that other info windows remain open when when clicking on markers. While this behavior may be desirable in certain applications, it is probably not how a typical map should operate. To ensure that only one info window is open at any one time, follow these steps:
Move to the very beginning of the script element containing your JS code (just above the createMarker() function) and declare a new global variable:
var infoWindow;
Recall from the w3schools tutorial that declaring a variable outside of any particular function gives it a global scope, making it available to any function in the applicable script element.
Now move to the top of the initMap() function and create a new InfoWindow object without supplying an InfoWindowOptions object to the class constructor:
infoWindow = new google.maps.InfoWindow();
Our approach here will be to create a single InfoWindow object and re-use it throughout the life of the map. Because we're not creating a new InfoWindow on each call to createMarker(), we won't be defining the contents of the InfoWindow using an InfoWindowOptions object as before. Instead, we'll call the setContent() method on the existing infoWindow global variable. Before resetting the info window contents, we'll call its close() method to ensure the previously opened window is closed.
Inside the addListener() method's callback function and just before the infoWindow.open statement, add the following code:
infoWindow.close(); infoWindow.setContent(info);
This first example was relatively simple in that it dealt with a single point shapefile. The second shapefile loader example [7] ups the ante by handling multiple shapefiles of different geometry types.
In testing the behavior of this map, you may notice that it is difficult to click on an interstate feature because of the presence of the counties polygon layer. If you zoom in to the interstate feature in the NW part of the map that ends near Butler, PA, you should see that it extends a bit beyond the counties layer. Clicking on that part of the feature should yield the interstate data.
If you have any ideas on how to make the interstates clickable, feel free to share them in the Lesson 5 Discussion Forum.
Diving into the source code, start by having a look at the initMap() function. Instead of a shpfile variable holding the name of a single shapefile, this version has a shpfiles variable that holds an array of names. (The page is built to handle any number of shapefiles, performance issues notwithstanding.) A loop is then used to load each of the shapefiles using the same Parsers discussed above.
As before, the load() statements pass control to callback functions named shpLoad() and dbfLoad(). However, these versions of the callback functions have to deal with an unlimited number of shapefiles, not just one. This is handled by adding each parsed shp or dbf file to an array (arrShp and arrDbf). A check is then done to see if the lengths of the two arrays matches the length of the names array. If they do, then all of the files have been parsed and the render() function can be called.
Looking at the render() function, it now needs to determine the lat/long bounds of all of the specified shapefiles combined, not just of one. To do this, a LatLngBounds() object is created and stored in the bounds variable. A loop is then used to process each of the shapefiles. On each pass through the loop, a LatLngBounds object representing the bounding box for just the current shapefile is constructed and then unioned with the all-shapefile bounding box (held in bounds). So, because the shapefiles were specified in the order candidate_cities - interstates - counties, bounds starts out relatively small on the first pass through the loop, expands quite a bit on the second pass, and finally expands slightly more on the last pass.
As in the previous example, a loop is used to process all of the shapefile records. In this case, that loop (the j loop) is embedded within the i loop, which iterates through all of the specified shapefiles.
The previous map loaded only a point shapefile, so it was hard-coded to create only Marker objects. This map needs to handle points, lines and polygons, so a check has been added to determine what kind of geometry the current shapefile holds. The switch construct [8] used here may seem foreign to you unless you're an experienced programmer. It is basically an alternative to using if-else if and in this situation causes one block of code to be executed when shape.type is 1 (point data), another block when shape.type is 3 (line data), and another block when shape.type is 5 (polygon data).
As we saw above, retrieving the lat and long values from a parsed point shapefile can be done using the expressions shape.content.y and shape.content.x.
When dealing with a line shapefile, each geometry is composed of a sequence of vertices, not just one point. These vertices are accessed using the expression shape.content.points. The vertices returned by the points property are immediately passed to a function called pathToArray(), which turns them into an array of LatLng objects. With that array constructed, it is used to set the path property of a new Polyline object.
Polygon shapefiles are complicated by the fact that polygon features can be composed of multiple parts. Thus, the polygon rendering code begins by obtaining the parts of the current feature. If the length of the parts array is 1 (i.e., the feature has only 1 part), then all of the vertices can be passed to the pathToArray() function to convert them to LatLng objects. However, a loop is used (the k loop) to put together the right set of vertices for each part. I have to confess though, that I can't explain what's going on in the shape.content.points.subarray(2 * parts[k], 2 * parts[k + 1]) expression. The subarray() method must be grabbing a subset of vertices, but I haven't the slightest idea why the vertex arrays are being multiplied by 2. In any case, you don't need to understand all of the code to put it to work for yourself.
Two more parts of this example could use some further explanation. Note that after the switch block, the data associated with record j in the dbf file is passed to a function called recordHtmlContent(). That function iterates through each of the keys (which correspond to columns) in the data, putting each key and its associated value on a separate line.
That bit of HTML returned by recordHtmlContent() is then passed to a function called handle_clicks(), which displays the HTML in an InfoWindow.
With that, you've seen how overlay data can be read in from a shapefile. Seeing the distribution of features on the map is great, but the user may also like to have the ability to search for a feature by its name or some other attribute and find out where it is located on the map. Providing that ability is the subject of the next section.
Many of the Google Maps you see embedded on web pages are accompanied by a list of the items that are plotted on the map - sometimes referred to as a sidebar. Often, the items in the sidebar are links that trigger the opening of their associated info windows on the map.
In this section, we're going to add a sidebar to the page we built in the previous section that read and plotted points stored in a shapefile. This sidebar will list the names of the candidate cities as links that open their associated info windows when clicked.
Our approach to this problem will be to add a new sidebar div element to the page. We'll use styles to position the map and sidebar div elements. To construct the HTML that will go in the sidebar div, we'll insert a line into the script's loop that assigns some HTML to a variable. Each time through the loop, the variable will be assigned whatever text it held after the last record was processed, plus the text associated with the current record. In this way, we'll iteratively construct the list of feature info and once the loop is completed assign that list to the innerHTML property of the sidebar div.
With this general strategy in mind, let's go through the steps required to add this functionality one at a time:
First, be sure that you're editing the document from the last page in which you applied the info window fix.
Add a sidebar div element to the <body> section of the page as follows (note that this is HTML and not JavaScript):
<div id="sidebar"></div>
Next, add the following CSS settings to the style section:
#map { margin: 0; padding: 0; height: 600px; width: 800px; float: left; } #sidebar { width: 300px; height: 600px; float: right; }
Next, move to the top of the JavaScript section and add the following global variable beneath the global infoWindow variable created in the last section:
var markers;
Move to the top of initMap() and initialize the markers variable to an empty array:
markers = [];
The markers variable - as you might have guessed - will be used to store all of the Marker objects as an array.
Next, add a function (perhaps before or after createMarker) that will be used to handle clicks on the links in the sidebar:
function myclick(num) { google.maps.event.trigger(markers[num], "click"); }
Note that this function uses the event namespace to open a specific marker's info window by programmatically triggering its click event. The number passed into the function determines which info window gets displayed. For example, if 0 were passed, the info window associated with the first marker in the array would be opened. If 1 were passed, it would open the window associated with the second marker, etc.
Inside the createMarker() function, add the following statement after the var marker statement:
markers.push(marker);
This statement uses the push() method of the markers array to add the newly created marker to the end of the array. The last few steps will involve modifications to the render() function.
Just before the loop that processes the shapefile records, declare a new empty string variable to hold the sidebar HTML:
var side_html = "";
Next, move to the bottom of the loop and add a link to the sidebar HTML that will open the info window for the record being processed:
side_html += '<a href="javascript:myclick(' + i + ')">' + name + '</a><br />';
Note how this statement sets the href attribute of the link so that it executes a JavaScript function - namely, the myclick() function we added moments ago. Included in the call to myclick() is the number held in the loop variable i. The link text is pulled from the name variable, which we had already been obtaining in the previous version of the script.
The side_html variable will be added to incrementally on each pass through the loop. Once the loop is complete, we're ready to use that variable to set the side_bar div's innerHTML property.
Move just beyond the end of the for loop (but still inside the render() function) and set the sidebar HTML as follows:
document.getElementById("sidebar").innerHTML = side_html;
With that, your page should be ready for testing. If you run into any problems, you can compare your code against mine [9].
That was a fairly simple sidebar. Let's say we wanted to provide not just the names of the cities, but also their populations in a tabular format. To do so, we would just need to write some HTML to the side_html variable that's a bit more complex.
Begin by modifying the side_html variable's declaration statement as follows:
var side_html = '<table style="border-collapse: collapse" border="1" \ cellpadding="5"> \ <thead> \ <tr style="background-color:#e0e0e0"> \ <th>City</th> \ <th>Population</th> \ </tr> \ </thead> \ <tbody>';
This defines a table with a collapsed border (one solid line rather than two detached lines) that is one pixel in width. The cellpadding attribute specifies that the text inside the cells should be five pixels from all edges. A header row is added in a gray background color, followed by a <tbody> start tag, which is used to signal the beginning of the table's body. Note the use of the line continuation character (\) to spread the statement across multiple lines to improve readability.
Inside the loop, the existing side_html statement should be modified like this:
side_html += '<tr> \ <td><a href="javascript:myclick(' + i + ')">' + name + '</a></td> \ <td>' + pop + '</td> \ </tr>';
This code adds a new row (<tr>) and two new data cells (<td>) in that row. The first cell contains the link from the original version of this example and the second contains the population value for the current city.
Finally, after the loop we need to close out the <tbody> and <table> tags:
side_html += '</tbody> \ </table>';
Here is my version of this page [10] for your reference.
The last couple of sections have improved on what we had done previously by reading data from a shapefile instead of hard-coding it in the JavaScript and adding a sidebar to make your mashup more informative. Now, let's have a look at incorporating data in another common format, KML.
KML (Keyhole Markup Language, named for a company acquired by Google in 2004) was originally developed for the display of data in what would become Google Earth, but subsequent modifications to the Maps API made it possible to view KML data in Google Maps as well.
KML data can also be incorporated into your own mashup through the use of the KmlLayer class. This map [11] contains an example of the use of that class:
var kml = new google.maps.KmlLayer({ url: 'http://ssc.e-education.psu.edu/get_ssc_in_kml.php?day=0&app=maps' }); kml.setMap(myMap);
The points displayed in this KML are weather stations symbolized according to the type of weather occurring there (such as Dry Polar, Moist Tropical, etc.). The weather types are determined using a system called the Spatial Synoptic Classification (SSC), originally developed at the University of Delaware in the mid-1990s. Here is more SSC information if you're interested [12].
Your first reaction might be that this is a lot easier than the process for adding shapefile data outlined earlier in the lesson. Yes, it is true that for some applications adding your data as KML might be a far better solution. In addition to making it easier to add your data to a map, the other big advantage to using KML is that it is an open data standard that makes the data interoperable with other applications (most notably, Google Earth).
That said, adding KML data to a Google Map also has a number of drawbacks:
If you are interested in learning more about KML, I’ll refer you to the KML documentation [15] maintained by Google. There you’ll find a language reference and examples laid out in much the same way as the Google Maps API documentation site.
As a form of XML, KML is stored in plain text files. Thus, any text editor can be used to author a KML document. However, unless you’re dealing with just a few features, manually coding the KML file in a text editor is not practical. Fortunately, KML’s evolution into an open international standard has meant that it is not difficult to find tools that automate the conversion of other data formats into KML. For example, in ArcGIS, users can convert data stored in shapefile or geodatabase format to KML using the Layer to KML tool or entire maps using the Map to KML tool. I encourage you to try one or both of these tools, upload the output file to your personal web space and copy and paste the URL to the file into the Google Maps search box.
Now that we've talked about a couple of file-based options for data storage, let's shift our attention to a cloud-based option: Google's Fusion Table technology.
Google’s Fusion Tables technology offers a number of advantages to map developers. Among these is the ability to load a “layer” of overlays with just a single line of code and to define symbology without writing any code. Let's walk through the usage of Fusion Tables as a data source for a Google Map with our familiar Jen and Barry's data.
Typically the easiest way to populate a Fusion Table is to import the data from an existing spreadsheet, delimited text file or KML file. It is also possible to create a new table from scratch.
Point feature classes are convenient because it is possible to add fields to the attribute table in ArcMap, populate those fields with the coordinate values associated with the points, then export the feature class to a comma-delimited text file. That text file can then be imported into a Fusion Table. Let's follow those steps with the cities shapefile.
Once your ArcMap export file has been imported as a Fusion Table, you should confirm that the table is set to use the values in the X/Y columns to map the points.
See if you can set up a 3-bucket classification based on the Population column with the following bucket definitions: 0-50000, 50000-100000, 100000+. Then try a 2-bucket classification based on the 1/0 values in the University column.
With that, you are now ready to incorporate your Fusion Table data into a map.
Next, add the following lines of JavaScript code to your page:
var lyr = new google.maps.FusionTablesLayer('xxxxxxx'); // Replace xxxxxxx with your table’s ID lyr.setMap(map); // where ‘map’ is a reference to the Map object in your code
If you look at the FusionTablesLayer section of the API Reference [18], you should note that its constructor has a single FusionTablesLayerOptions parameter, whereas the code snippet above shows specifying the table ID (a string). The ID string syntax was documented in older versions of the API and still appears to work. Whether that syntax should be considered deprecated (i.e., going away eventually) or undocumented at this point is unclear.
In any case, let's talk about the FusionTablesLayerOptions object that appears in the documentation. This Options object offers a number of properties for configuring the layer. Most notable among these are:
Creating a FusionTablesQuery object involves the same kind of logic that one might use to build a query with SQL. The difference is that the parts of the query need to be specified in object literal notation. The API Samples page includes a link to a sample demonstrating the use of a FusionTablesQuery [19].
The styles property must be set to an array of 1 to 5 FusionTablesStyle objects. This array makes it possible to apply different styles to different categories of records in much the same way that the point-and-click interface makes it possible through the Buckets tab. Again, there is a sample in the API documentation that demonstrates styling [20].
The method of adding coordinate values to the attribute table in ArcMap before exporting outlined above works great for point feature classes, but is not practical for line or polygon feature classes. For those geometry types, a better solution is to export the data in KML format. Let’s do that for the counties shapefile.
As with KmlLayers, the downside to FusionTablesLayers is that there is no way to work with the individual features that are displayed by the layer. The FusionTablesQuery object can be used only to specify which features from the table to display in the layer. There appears to be nothing built into the Fusion Tables API that enables querying records from a table and working with those records individually. However, it is still possible to build a map with a clickable sidebar using data stored in a Fusion Table. The solution involves sending a query that retrieves the desired data to Google's Visualization API.
The Visualization API is typically used for building dynamic charts, but it is useful in this situation because it can be used to obtain a DataTable object that encapsulates the data requested by a query. The DataTable class and other classes are documented in the Visualization API Reference [21], which is formatted much like the Maps API Reference you've been consulting throughout the course.
Have a look at this Fusion Table Markers and Sidebar example [22] and view its source code to follow along with the discussion below:
This example works well when dealing with point data. Creating a clickable sidebar for line or polygon data in a Fusion Table is a much tougher nut to crack since those geometry types are typically stored in the table as KML. Those of you who are highly experienced programmers may want to consider having a go at this topic for your final project.
With that, let's switch gears a bit and move on to the topic of Ajax, a programming paradigm that has revolutionized both web mapping and web publishing in general.
For the first decade or so of the World Wide Web's existence, mapping sites like MapQuest were held back by the fact that any time the user wanted to zoom in/out or even just pan a bit in some direction, the whole page needed to be re-loaded. While these sites were still useful, their lack of responsiveness caused by the delay in waiting for data to stream from the server to the client machine clearly limited their effectiveness and frustrated users.
A major advancement in web application development occurred in early 2005 and was encapsulated in an article by Jesse James Garrett entitled, Ajax: A New Approach to Web Applications [24]. Ajax (shorthand for Asynchronous JavaScript and XML) is a programming technique that uses a number of web technologies (JavaScript, the DOM, XML, and HTML/XHTML, most notably) to provide a much more responsive user experience. While Garrett and his team were certainly not the first to employ the Ajax technique (in fact, Google Maps was released prior to Garrett's article), his article was the first to provide a name for the technique and to explain its conceptual framework to a wide audience.
The Google Maps API is made possible by the use of Ajax programming. When a user views a Google Maps page, tiles of map data are passed from the Google server as XML and translated into imagery on the user's machine. As the user pans around the map, the tiles that are just outside of the visible portion of the map are downloaded behind the scenes. This is what allows the seamless panning that Google Maps wowed the world with in 2005.
Google Maps developer Mike Williams points out in his API tutorial [25] that the important aspect of this web programming paradigm is not really the use of JavaScript or XML per se. Rather, the important aspect of Ajax is its basic philosophy of loading the static parts of a web page once and loading the dynamic parts quickly through the use of small asynchronous exchanges of data that go on behind the scenes without the user's knowledge.
Be sure to test Mike's page to see the Ajax technique at work. His page makes it possible to view three separate sets of markers by clicking on the A, B, and C buttons just below the map. Note how quickly the markers and sidebar text are updated when one of the buttons is clicked. This responsiveness is achieved because the page is only requesting the data required to plot the new points and update the sidebar. A non-Ajax page would ask the server to send back all of the HTML required to display the desired page - even the parts that are not changing - and would not load as quickly. (While Mike's example is written for V2 of the API, you should still be able to follow its use of Ajax.)
Mike's example calls upon a script written in the PHP language to return different sets of markers depending on an input parameter passed to the script. This brings up an important concept that we haven't discussed yet — the difference between client-side and server-side scripting languages. JavaScript is an example of a client-side scripting language. When you embed JavaScript in a web page, that code gets downloaded to and executed on the browser's machine. In contrast, code written in a server-side scripting language like PHP, ASP, or Perl is never downloaded to nor executed on the browser's machine. It is instead executed on the web server that hosts the program and the output of the program (if any) is sent to the browser's machine.
One reason you may want to incorporate a server-side script into your application is that JavaScript is limited to reading files that are stored in the same domain (i.e., on the same machine) as the page containing the JavaScript. Thus, if you were aware of a website that published data that you'd like to integrate into a mapping mashup, you would not be able to accomplish the task with JavaScript alone. You would need to use a server-side script (known as a proxy) to read the data and pass it to your JavaScript page.
Another reason to use a server-side script as part of a mashup solution is its ability to extract information from a DBMS like Oracle, SQL Server, or MySQL. Converting data from a DBMS to one of the formats discussed earlier in the lesson can be quite tedious, particularly if your data are constantly changing. Instead, you can set up a server-side script to query your database and output the data in a form that can be consumed by JavaScript. This could be XML, JSON or even simply comma-separated text. Your JavaScript page can then read in the output from this proxy just as it would if the data were actually stored in a flat file.
To further illustrate the concept of Ajax programming, let's take a look at a database-driven mashup that I've written. This solution uses MySQL and PHP to map the locations of students in the PSU online geospatial programs by their zip codes. [26]
The page plots the US and Canadian students in our various programs in different colors. Clicking on a student's hometown will zoom to that location and open the associated info window. By default, the map will display all of the students enrolled in the most recently completed term. However, any term going back to the launch of the GIS Certificate Program in winter 1999 can be selected from the drop-down list just above the map.
The data for this map are stored in a MySQL database. A PHP script is used to query the database and feed the query results to the JavaScript page in XML format. To see how this application works under the hood, start by viewing the XML output from the PHP script [27]. You could obtain data for other terms by replacing the 201503 part of the URL with 201502, 201501, etc. A detailed explanation of how to work with XML data like this is found in the database lesson.
Without going into too many details on the syntax of PHP, let's have a look at the PHP script [28] to give you a sense of how it works. (This is a version of the script saved in plain text format so that you're able to view its source code. If I hadn't posted this plain text version, you would only be able to see the script's output and not its source code. This is one of the big differences between client-side and server-side scripts.)
Note that all PHP variables begin with the dollar sign character ($). The first three lines of the script open a connection to the MySQL database. The $term statement retrieves the value that was passed to the script through the URL (e.g., 201503) and stores it in a local variable. That variable is then plugged into the WHERE clause of a long SQL SELECT statement that brings together the desired data from three tables. That query string is then used as the argument to a function that executes the query. A for loop is eventually used to process the query results one row at a time, but first an important line known as a response header is added to indicate the type of data that will be returned by the script:
header("Content-type: text/xml");
The script's output is created using a series of echo statements. The start tag for the root <students> element is echoed before the loop and its end tag is echoed after the loop. Within the loop itself, the various XML start and end tags are echoed with the appropriate values from the current row inserted in between. Note that the concatenation character in PHP is the period.
Now, have a look at the source of the wcgis_students.html page. Within the <head> section of the document are a number of required JavaScript functions, the most important of which is the load() function. This function requires that the desired term be supplied as an input parameter. The first thing the function does is set the main heading of the page to display the term being mapped using the DOM. Note that a call is made to the getTermName() function to convert the term in the format 201503 to 'SU-2015'.
The next bit of code removes all of the markers from the map (if there are any) and empties the markers array. These statements are necessary because this function is going to be called each time the user selects a different term from the drop-down list. Without them, markers from previously selected terms would remain on the map each time a new term is selected.
Next, note how the PHP script is plugged into the call to the downloadUrl() function. (This function comes from an external library written by the same Mike Williams cited above and is discussed in detail in the database lesson.) The complete URL is created by appending the term value originally passed into the function to the static beginning of the URL. The rest of the code in the load() function is written much like the Jen and Barry's example from earlier in the lesson.
Skipping down to the page's <body> section, recall that the div with id "heading" has its text set by the load() function.
Next, two <table>s are used to arrange the information below the headings. The first is used to show a key to the marker symbols, the term drop-down list, and a count of the number of students being mapped. The second table contains the sidebar and the map.
Note that the third cell in the first table contains an HTML <form> element and within that form element is a <div>. This div will have its innerHTML property set to a <select> element (the drop-down list) by code found in the init() function.
The drop-down is constructed dynamically in init() by obtaining the current year and month using JavaScript's built-in Date functions. These values allow me to determine the last term for which records are available in the database. This information is needed to initialize the map when the page is first loaded and also to have a starting point for constructing the list of options in the drop-down list.
After the setting of the last_term variable comes a critical statement. A variable called drop_down is created to hold the HTML for the drop-down list. The critical part of this statement is the setting of the onchange attribute to execute the load() function. The DOM is used to specify that the text of the option selected by the user should be passed to the load() function. Thus, if the user changes the selected option in the drop-down list from 201503 to 201502, the load() function will be called and passed the value 201502.
After that initial bit of HTML defining the <select> element, two loops are used to create the options associated with that element. The first loop adds all terms that have been completed in the current year. The second loop adds all terms for every year going back to 1999. This code is complicated by the fact that we switched from 4 terms per year to 5 terms per year in 2013.
After the loops, the HTML required to close the <select> element is appended to the drop_down variable. That variable is then finally used to set the innerHTML property of the appropriate <div>.
The next statement adds the map and sets its center and map type.
The last line of the script makes the initial call to the load() function, passing it the most recently completed term, as computed from the current year and month.
This section demonstrated how features stored in a database can be retrieved by the user through a responsive application using an Ajax approach. But what if you don't have the latitude/longitude coordinates of the locations you wish to map? If you have some kind of locational information such as a full postal address, city/state, or zip code, you can pass this information to Google's geocoder and ask it to give you the associated coordinates. That's the topic of the last page of this lesson.
The ability to geocode addresses in V3 of the API is provided by the Geocoder class [29]. This class appears to make use of the same geocoding engine that is utilized on Google's own maps.google.com page. The same variety of strings that can be entered in the search box on Google's site can also be passed to the geocoder (e.g., full addresses, city/state combinations, and zip codes all yield matches).
Addresses are passed to the geocoder by creating a GeocoderRequest object as an object literal. In addition to the address, the GeocoderRequest class also makes it possible to specify where to search (as a bounding box or within a country) and the preferred language for the results.
The API samples page provides a very good example of geocoding [30]. Go to that page, test its behavior, then view its source so you can follow along with my description of how the page works.
In the HTML <body> section is a <div> element containing two <input> elements. The first input element (the search box) has its type set to textbox and its initial value to Sydney, NSW. The second input element is of the button type and has its onclick attribute set to a function called codeAddress().
Moving up to the JavaScript, note that the Geocoder object is created before the two functions, making it possible to work with the object in either function through the global variable geocoder
That brings us to the codeAddress() function that gets executed in response to clicks on the Geocode button. Logically, the first step in this function is to obtain the user's entry in the search box using the DOM. This address is passed along as an object literal in a call to the Geocoder object's geocode() method. The second argument passed to the method is a callback function that handles the matches that are returned when the geocoding is complete. Within the callback function, the success/failure of the request is accessible through the status variable and, assuming the request returns one or more matches, those matches are accessible through the results variable.
Before processing the results though, the function first checks to make sure the request was successful by examining the status code and looking for a value of OK. (Here is the full list of codes. [31]) If the status variable holds something other than OK, that value is reported through an alert() box.
The geocoding results are returned as an array of GeocoderResult objects defined in JavaScript Object Notation (JSON) format. JSON is a commonly used method for exchanging data across networks and follows the same basic syntax you've used to define object literals throughout the course. A GeocoderResult object has a geometry property that returns an object of the GeocoderGeometry type. That object, in turn, has a location property that returns a LatLng object (a point). The expression results[0].geometry.location returns the position of the first match in the array of GeocoderResults. That position is used to set the map's center and to add a plain marker. If you were writing your own geocoding application, you might want to examine all the matches in the array using a loop instead of retrieving just the first match.
The accuracy of each matching point can be found using results[0].geometry.location_type. The location type codes are detailed in the API Reference [32].
For this lesson's graded assignment, I'd like you to select your own dataset (perhaps something from your work) and create a mashup that depicts those data on a map. Most students choose to map a set of points (markers), but you're welcome to try lines and/or polygons instead. You have the option of storing your data in shapefile, KML or Fusion Table format. When deciding on a format, keep in mind that I'd like to see a clickable sidebar similar to that discussed earlier in the lesson. The user should be able to click on links in the sidebar to open the info windows associated with the map's features. The lesson didn't provide a detailed outline of coding this behavior for every data format, so you may need to mix and match code from examples and/or consult outside examples.
Finally, I want to point out that the Lesson 7 project will also give you an opportunity to map data of your own choosing. Keep that in mind when selecting data and/or functionality to incorporate into this project.
This project is one week in length. Please refer to the course Calendar, in Canvas, for the due date.
In this lesson, you continued building on your experience with the Google Maps API by learning how to read data from various data sources, how to add a list of the overlay features to the side of your map, how Google's address geocoder works, and how Ajax programming techniques produce more responsive web applications.
In the next lesson, we'll shift gears by exploring Esri's JavaScript-based mapping API. It offers a lot of the same functionality that the Google API provides, but it also goes beyond to offer more of the capabilities that we're used to seeing in the GIS world.
Links
[1] http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf
[2] http://code.google.com/p/gmaps-samples/source/browse/trunk/shapefileloader
[3] http://www.personal.psu.edu/jed124/shapefileloader/shp_load_single.html
[4] http://www.personal.psu.edu/jed124/shapefileloader/shp.js
[5] http://www.personal.psu.edu/jed124/shapefileloader/dbf.js
[6] http://www.personal.psu.edu/jed124/shapefileloader/shp_load_single_info_window_fix.html
[7] http://www.personal.psu.edu/jed124/shapefileloader/shp_load_multiple.html
[8] http://www.w3schools.com/js/js_switch.asp
[9] http://www.personal.psu.edu/jed124/shapefileloader/shp_load_single_w_sidebar.html
[10] http://www.personal.psu.edu/jed124/shapefileloader/shp_load_single_w_sidebar2.html
[11] http://www.personal.psu.edu/jed124/load_kml.html
[12] http://sheridan.geog.kent.edu/ssc.html
[13] http://econym.org.uk/gmap/egeoxml.htm
[14] http://ssc.e-education.psu.edu/get_ssc_in_kml.php?day=0&app=maps
[15] https://developers.google.com/kml/documentation/?csw=1
[16] http://accounts.google.com
[17] https://drive.google.com/drive/
[18] https://developers.google.com/maps/documentation/javascript/3.exp/reference#FusionTablesLayer
[19] https://developers.google.com/maps/documentation/javascript/examples/layer-fusiontables-query
[20] https://developers.google.com/maps/documentation/javascript/examples/layer-fusiontables-styling
[21] https://developers.google.com/chart/interactive/docs/reference?csw=1#DataTable
[22] http://www.personal.psu.edu/jed124/fusion_table_markers_w_sidebar.html
[23] http://www.google.com/jsapi
[24] http://www.adaptivepath.com/ideas/ajax-new-approach-web-applications/
[25] http://econym.org.uk/gmap/basic11.htm
[26] http://courseware.e-education.psu.edu/php/wcgis_students.html
[27] http://courseware.e-education.psu.edu/php/get_wcgis_students.php?term=201503
[28] http://courseware.e-education.psu.edu/php/get_wcgis_students_php.txt
[29] http://code.google.com/apis/maps/documentation/javascript/reference.html#Geocoder
[30] https://developers.google.com/maps/documentation/javascript/examples/geocoding-simple?csw=1
[31] https://developers.google.com/maps/documentation/javascript/reference?csw=1#GeocoderStatus
[32] https://developers.google.com/maps/documentation/javascript/reference?csw=1#GeocoderLocationType