From Bugwoodwiki

Contact Info

Mike Dolbow

GIS & Graphics Supervisor

MN Department of Agriculture

e-mail: mike (dot) dolbow (at)

Phone: (651) 201-6497

MN Collaborator with Early Detection and Distribution Mapping System

Creating a Region-Specific Reporting Page for EDDMapS Data

This section contains details on how to create a region-specific reporting page for a small number of species, using JSON-formatted community observation data from EDDMaps. To see this code in action, visit the MN Department of Agriculture Early Detection Program (info link, interactive map). A full page of example code (about 250 lines) is available by contacting me. Below are some basic snippets with comments.


This concept behind this "mashup" is to provide users who are interested in a particular region (or "Area of Interest"), and a particular subset of invasive weed species. Users and partners are still required to use to enter in observations, but as those observations become approved, they dynamically show up in the mashup without any downloads or database synching. The user or manager can view a region and quickly get a feel for where certain invasive species have been found in the area of interest. The mashup relies on a Google Map (focused on the Area of Interest, or AOI), a drop-down of the species of concern, and a series of javascript functions that add the appropriate EDDMaps data to the map.

The Magic - and the Danger

This code relies on what is known as the "dynamic script tag hack" to get around cross-site-scripting restrictions inside browsers. The basic concept is that when a user picks a species, a javascript function dynamically adds the EDDMaps JSON feed as a script into the header of the page and passes the script/JSON to a callback function. The callback function handles the data and adds it to the map. This technique is detailed in many articles, such as this, this, and this.

Note, however, that this technique can leave your page vulnerable to cross-site scripting attacks. As a result, the code included here makes every attempt to validate the JSON feeds themselves, and their contents - for both type and scope. Nevertheless, before implementing this technique, you should make sure your page does NOT contain any sensitive data and you should read this article about JSON security in the browser. Luckily, not a lot of invasive weeds have bank accounts. But consider yourselves warned!

Useful Links

For the sake of brevity, I'm going to focus here on things that aren't well documented elsewhere. But if you've never done a Google Maps mashup, you'll want to visit these links first:

Note: This mashup, and the code posted below, was originally developed under Google Maps API version 2, which is a stable version that should be supported until about 2012 or so. The production page has now been migrated to version 3, which is the current version. That code is available by contacting me - the code below HAS NOT been migrated and will essentially fail under version 3 of the API.

The Code

Is this what you've been patiently waiting for?

Script References

This mashup relies on two external script sources: one for the Google Maps API and one for the Marker Clustering:

<script src="" type="text/javascript"></script>  
<script src="" type="text/javascript"></script>

The rest of the code below, down to the HTML section, can be placed inside a single <script type="text/javascript"> tag.

Base Variables and Arrays

Here's some base variables that get used throughout this code and some javascript arrays that define your species of concern for the whole implementation.

//Base Variables
var map = null;
var feedID = null;
var markerCluster = null;
var curMapBounds = new GLatLngBounds();
var sciNameText = "None selected";
var AOICountText = "None selected";

//Establish the bounding box of your Area of Interest (AOI)
var genAOIMapBounds = new GLatLngBounds(new GLatLng(39.0,-99.5), new GLatLng(49.5,-87.0));

//Establish Arrays for the Species List that feeds all other functions. Get IDs from
//There are THREE arrays defined below - they MUST correspond with one another!
var sppListCommon = [];
sppListCommon[0]="Narrowleaf bittercress";
sppListCommon[1]="Yellow starthistle";
sppListCommon[2]="Meadow knapweed";
sppListCommon[3]="Black swallow-wort";
sppListCommon[4]="Grecian foxglove";

var sppListSciName = [];
sppListSciName[0]="Cardamine impatiens"; //Narrowleaf bittercress
sppListSciName[1]="Centaurea solstitialis"; //Yellow starthistle
sppListSciName[2]="Centaurea x moncktonii"; //Meadow knapweed
sppListSciName[3]="Cynanchum louiseae"; //Black swallow-wort
sppListSciName[4]="Digitalis lanata"; //Grecian foxglove

var sppListFeedID = [];
sppListFeedID[0]=11539; //Narrowleaf bittercress
sppListFeedID[1]=4390; //Yellow starthistle
sppListFeedID[2]=4348; //Meadow knapweed
sppListFeedID[3]=3398; //Black swallow-wort
sppListFeedID[4]=13990; //Grecian foxglove

The Map Load Function

This loads up the map with a center of interest (usually the center of your AOI) and the default Google Map user interface.

//Load up the map
function load() {
  if (GBrowserIsCompatible()) {
	map = new GMap2(document.getElementById("map"));
	//Below is a setcenter for this application
	map.setCenter(new GLatLng(44.25, -93.25), 4);
	curMapBounds = map.getBounds();
  } // end if Browser compatible
} //end function load

The Switch Species Function

This function gets called when a user selects a species in the drop-down of the page. It sends the species chosen to the dynamic scripts function (appJsonScripts), which comes next.

//Switch the species if the drop-down species list is changed	
function switchSpecies(iValArray) { //capture the species ID from the drop-down
	if (iValArray != "NA") {
		AOICountText = "Working..."; 
		if (markerCluster !== null) {
		feedID = sppListFeedID[iValArray];
		sciNameText = sppListSciName[iValArray];
		appJsonScripts(feedID); //send the feedID to the appJsonScripts function to add the script to the document
	} else { 
		if (markerCluster !== null) {
		sciNameText = "None Selected";
		AOICountText = "None Selected";
	} // end if (sppFeedID != 0)
	document.getElementById('sciName').innerHTML = sciNameText;
	document.getElementById('AOICount').innerHTML = AOICountText;
} //end function switchSpecies

The Dynamic Script Tag Function

This is the magic hack as described above. The function gets called after the Switch Species function and appends the JSON to the header of the document inside a <script> tag. The JSON is sent to a callback function called "parseFeed".

//append the selected JSON string as a script in the header of the document
function appJsonScripts(selFeedID) {
  if (sppListFeedID.indexOf(selFeedID) != -1) {
	var headID = document.getElementsByTagName("head").item(0);
	var script = document.createElement ('script');
	script.setAttribute("type", "text/javascript");
	script.src = ""+selFeedID+"&callback=parseFeed";
	} else {
	alert("You're attempting to load an invalid ID. Aborting process."); 
  } //end if (sppListFeedID.indexOf(selFeedID) =! -1)
} //end function appJsonScripts

The JSON Feed Parsing Function

This is the meat of the code. the parseFeed function is the callback function specified by the JSON string as its dynamically loaded up in the previous function. This function takes the species feed, validates it, and parses the JSON for markers appropriately. It adds markers (and/or clusters) to the map, updates the observation count inside the AOI, and zooms the map to include the markers if they can't be seen inside the current map view.

//Parse the JSON feed to create new markers showing community observations	
function parseFeed(feed) {
var clmarkers = [];
var mcOptions = {gridSize: 40, maxZoom: 12};	
var AOImarkers = [];
	// Read the JSON data 
	if (feed !== null) {
	  var bMapBounds = new GLatLngBounds();
		  var markers = feed.markers;
		  if (markers !== "None") {
			for (var i = 0; i < markers.length; i++) {
			// obtain the attribues of each marker, extend the bounding box, and cluster
				var lat = markers[i].lat;
				var lng = markers[i].lng;
				var name = markers[i].label;
					if (name.indexOf('(') !== -1 || name.indexOf(')') !== -1 || name.indexOf('<') !== -1 || name.indexOf('>') !== -1) {
						alert("A marker label contains invalid characters. Please contact this website's author.");
					} //end if name.IndexOf catch
					if (typeof(lat) == "number" && typeof(lng) == "number"  && typeof(name) == "string") {
						if (lat >= -90.1 && lat < 90.1 && lng >= -180.1 && lng < 180.1) {
							var point = new GLatLng(lat,lng);
							} else {
							alert("A marker latitude or longitude contains invalid coordinates. Please contact this website's author.");
						} //end if lat >=0 ...
						var marker = createMarker(point,name);
							//determine if the marker is inside AOI area, push into AOImarkers array
							if (genAOIMapBounds.containsLatLng(point)) {
							} //end if point is inside AOI
					} else {
					  alert("A marker element does not match appropriate data types. Please contact this web page's author.");
					} //end if typeof for the 3 variables
			} //end for (var i = 0 ...
			//create Cluster object and pass in clmarkers array, change the map extent
			markerCluster = new MarkerClusterer(map, clmarkers, mcOptions);
			curMapBounds = map.getBounds();
				if (curMapBounds.containsBounds(bMapBounds)) {
					//don't change zoom extent
				} else {
				} //end if curMapBounds.containsBounds(bMapBounds))
			} else {
				alert("There are no locations currently mapped for this species.");				
			} //end if markers !== "None"
	 AOICountText = AOImarkers.length;
	 document.getElementById('AOICount').innerHTML = AOICountText;
	} else { 
	 alert("The data feed is currently not functioning. Please check back later.");
	} //end if feed !=null
} // end function parseFeed

The Create Marker Function

This function just creates the marker with a listener that can point the user to EDDMaps' detailed record of the observation. The detail is only seen once a user zooms to a point where the clusters aren't shown. (Clustering is handled in the parseFeed function.)

//create a marker with the listener for some HTML that provides more detailed links to
function createMarker(point, name) {
  var marker = new GMarker(point);
  var baseURL = "";
  GEvent.addListener(marker, "click", function() {
	var myHtml = "<p align=\"left\"><b>Site #" + name + "<\/b>:<br> <a href=\""+ baseURL + name +"\"  target=\"_blank\">Click for more detail<\/a><\/p>";
	map.openInfoWindowHtml(point, myHtml);
  return marker;  
} //end createMarker function


This is the HTML that implements everything - from the drop-down to the dynamic text to the map. Scripts are used to populate the drop down so you don't have to hard-code the choices - you only need to set up your arrays in the beginning of the main script. A small script is also used to size the height of the map if the user has a screen resolution where the height is larger than 800 pixels.

<body onload="load()" onunload="GUnload()">
<p><b>JavaScript must be enabled in order for you to use Google Maps.</b> However, 
  it seems JavaScript is either disabled or not supported by your browser. To 
  view Google Maps, enable JavaScript by changing your browser options, and then 
  try again. </p>
<table width="95%" border="0" cellspacing="5" cellpadding="5">
    <td> <p>Use the javascript arrays to fill in your species of interest and 
        create the drop-down below.</p>
        <form action="" method="post" name="frmSpeciesSelect">
          <select id="lstSppSelect" name="lstSpeciesSelect" onchange="switchSpecies(this.options[this.selectedIndex].value)">
            <option label="Select a species" value="NA" selected>Select a species</option>
            <script type="text/javascript">
				//Add array index values and Common Names for each species in the list
				for (var i = 0; i<sppListCommon.length; i++) {
					document.write("<option value=\""+i+"\">"+sppListCommon[i]+"</option>");
				} //end for sppListCommon loop
          <p>Scientific Name:<br>
            <strong id='sciName'>None selected</strong></p>
          <p>Define your Area of Interest (AOI) with the genAOIMapBounds variable.</p>
          <p> Area Of Interest reports:<br>
            <strong id='AOICount'>None selected</strong> </p>
        <p align="left">To enter your own observations or see more species (including 
          "feed IDs"), visit our partners: </p>
        <p align="left"><a href="" target="_blank"><img src="" alt="EDD Maps" width="288" height="84" border="0" longdesc=""></a></p>
        <p align="left"> </p>
          	<script type="text/javascript">
				//Adjust the screen height so our map div isn't squished on high-res monitors
				var scrHeight = screen.height;
				if (scrHeight < 800) {
					document.write("<td align=\"right\" valign=\"top\" width=\"100%\" height=\"400px\">");
					} else {
					document.write("<td align=\"right\" valign=\"top\" width=\"100%\" height=\""+scrHeight*.5+"px\">");
				} //end if scrHeight < 800
    <div align="right" id="map" style="width:100%; height:100%;"></div>