Added by Brent Owens, last edited by Brent Owens on May 04, 2006  (view change)

Labels:

Enter labels to add to this page:
Wait Image 
Looking for a label? Just start typing.

h. Set up queries - AJAX

We wanted to be able to send off a query to the server and have hte results returned without having to reload the page. This would be beneficial if the user was looking around on the map and didn't want to lose their position when they searched for something.
To do this, we used a technique called AJAX: Asycronous Javascript And XML.
Really all it is, is sending off a request to the server, waiting for a response, then parsing and displaying the result; without reloading the page.
Asynchronous, meaning that multiple requests at once.
Javascript, meaning that it is all prepared on the client side in Javascript
XML, meaning that it communicates with XML.

The XML part of it is the request package that is sent to the server. The server then sends back an XML request package. What will it look like? Well that entirely depends on what the server is and what your request is. It can by anything.
Now to make some sense out of it, we will be using a protocol called WFS (Web Feature Service). It is a protocol for querying geographic data, inserting geographic data, modifying geographic data.
The request is defined by an XML schema (.xsd file) that has the rules for how the request must look.
The request we are using is a "GetFeature" request: this gets us a geographic feature.
So our request is XML in the form of a GetFeature request.

So what will the server send back? Hopefully features... or at least an error. Features are described in XML, called GML (Geographic Markup Language). The response back should be a collection of features: a FeatureCollection.

Ok, so we send XML (GetFeature request) and receive XML (GML feature collection).

Lets look at the javascript that does this:

var geo_xmlhttp = null;	// AJAX-ness

function sendRequest()
{
	// the server location where the request has to go
	URL  = "http://sigma.openplans.org/geoserver/wfs/";

	XML = makeCityQuery('vancouver'); // make the xml query, pass in the city name 'vancouver'

	getXML(URL,XML,XMLProgressFunction_gnis); // make the request
}

// returns a string of XML for the request
function makeCityQuery(location_name)
{
	XML  = '<?xml version="1.0" encoding="UTF-8"?>'+"\n";
	XML += '<wfs:GetFeature service="WFS" version="1.0.0"'+"\n";
	XML += '  outputFormat="GML2"'+"\n";
  	XML += '  xmlns:topp="http://www.openplans.org/topp"'+"\n";
  	XML += '  xmlns:wfs="http://www.opengis.net/wfs"'+"\n";
  	XML += '  xmlns:ogc="http://www.opengis.net/ogc"'+"\n";
  	XML += '  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'+"\n";
  	XML += '  xsi:schemaLocation="http://www.opengis.net/wfs'+"\n";
  	XML += '                      http://schemas.opengis.net/wfs/1.0.0/WFS-basic.xsd">'+"\n";
  	XML += '  <wfs:Query typeName="topp:gnis">'+"\n";
  	XML += '   <ogc:Filter>'+"\n";
  	XML += '    <ogc:PropertyIsLike wildCard="*" singleChar="." escape="\\">'+"\n";
	XML += '     <ogc:PropertyName>topp:full_name_lc</ogc:PropertyName>'+"\n";
   	XML += '     <ogc:Literal>'+location_name+'*</ogc:Literal>'+"\n";
	XML += '    </ogc:PropertyIsLike>'+"\n";
   	XML += '   </ogc:Filter>'+"\n";
  	XML += '  </wfs:Query>'+"\n";
	XML += '</wfs:GetFeature>'+"\n";

	return XML;
}

// Send the request
function getXML(url,post,procfunction)
{
	try {
		if (window.ActiveXObject)
		{
			// IE
			geo_xmlhttp =  new ActiveXObject("Microsoft.XMLHTTP");
			geo_xmlhttp.onreadystatechange = procfunction;
			geo_xmlhttp.open("POST", url, true);
			geo_xmlhttp.setRequestHeader('Content-Type', 'text/xml');  //correct request type
			geo_xmlhttp.setRequestHeader('Cache-Control', 'no-cache');	// don't cache the requests!
			geo_xmlhttp.send(post);
		}
		else if (window.XMLHttpRequest)
		{
			// Mozilla and others
			geo_xmlhttp =  new XMLHttpRequest();
			geo_xmlhttp.onreadystatechange = procfunction;
			geo_xmlhttp.open("POST", url, true);
			geo_xmlhttp.setRequestHeader('Content-Type', 'text/xml');	//correct request type
			geo_xmlhttp.setRequestHeader('Cache-Control', 'no-cache');	// don't cache the requests!
			geo_xmlhttp.send(post);
		}
		else
			log("Invalid browser format: not expecting this kind of browser.");
	 }
	 catch(e)
	 {
		alert(e);
		alert("If you just got a security exception, its because you need to serve the .html file from the same server as where you're sending the XML requests to!");
	 }
}

// Waits for requests and handles them.
function XMLProgressFunction_gnis()
{
	if ( (geo_xmlhttp.readyState == 4) && (geo_xmlhttp.status == 200) )
	{
		//we got a good response.  We need to process it!
		if (geo_xmlhttp.responseXML == null)
		{
			log("XMLProgressFunction(): abort 1");
			document.getElementById('working_anim_gnis_span').innerHTML = '';	// remove 'working' animation
			return;
		}

		log("response:\n"+geo_xmlhttp.responseText); // print out the response
	}

	// else, still waiting for a response...
}

It's fairly simple if you stand back and look at it. It boilds down to a few lines of code:
geo_xmlhttp = new XMLHttpRequest(); // mozilla
geo_xmlhttp.onreadystatechange = myAjaxProgressFunction; // set the 'listen' method
geo_xmlhttp.open("POST", "http://sigma.openplans.org/geoserver/wfs/", true); // open it up and pass in the values
geo_xmlhttp.send(post); // send it

This will prepare the XML request (getFeature request), send it to the URL "http://sigma.openplans.org/geoserver/wfs/" over "POST" and then hand off control to "myAjaxProgressFunction()" which will wait for the response.
The myAjaxProgressFunction() function gets called over and over until finally a response arrives. Then, if everything is ok, the response's readyState will be '4' and the status will be '200'.

In the example method above, we just print out the response text.
To parse the response using XML DOM objects (what you really need to do to get useful information) you need to call geo_xmlhttp.responseXML.

The request object has to be set up differently depending what browser you are using. That is why I used a separate method called getXML(). In that method I ask the browser who it is:

if (window.ActiveXObject) // Internet Explorer
...
else if (window.XMLHttpRequest) // Mozilla browsers

Ok, so we have our AJAX system set up to send off XML getFeature requests and expect back GML FeatureCollections. What does a typical GML FeatureCollection look like for a request? It looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs" 
			xmlns:topp="http://www.openplans.org/topp" 
			xmlns:gml="http://www.opengis.net/gml" 
			xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
			xsi:schemaLocation="http://www.openplans.org/topp 
						http://sigma.openplans.org:80/geoserver/wfs/DescribeFeatureType?typeName=topp:gnis 
						http://www.opengis.net/wfs 
						http://sigma.openplans.org:80/geoserver/schemas/wfs/1.0.0/WFS-basic.xsd">
	<gml:boundedBy>
		<gml:Box srsName="http://www.opengis.net/gml/srs/epsg.xml#4326">
			<gml:coordinates decimal="." cs="," ts=" ">-165.41778564,-48.03333664 166.88334656,60.54833221</gml:coordinates>
		</gml:Box>
	</gml:boundedBy>

	<gml:featureMember>
		<topp:gnis fid="">
			<topp:full_name>Vancouver</topp:full_name>
			<topp:full_name_lc>vancouver</topp:full_name_lc>
			<topp:sub_national>British Columbia</topp:sub_national>
			<topp:country_name>Canada</topp:country_name>
			<topp:country_code>CA</topp:country_code>
			<topp:type>Populated Place</topp:type>
			<topp:uniq_featcode>-575268</topp:uniq_featcode>
			<topp:the_geom>
				<gml:Point srsName="http://www.opengis.net/gml/srs/epsg.xml#4326">
					<gml:coordinates decimal="." cs="," ts=" ">-123.1333333,49.25</gml:coordinates>
				</gml:Point>
			</topp:the_geom>
		</topp:gnis>
	</gml:featureMember>

</wfs:FeatureCollection>

In the response above there is only one <gml:FeatureMember>, but it is possible there will be many more. In fact if you search the GNIS database for 'vancouver' you will get 44 results.

Narrow the search with Countries

We now have a system that will search the GNIS database for geographic location. Now lets search for geographic locations inside specific countries, to help narrow our search.
To do this I added an extra form field for the country name, and made a second getFeature request XML function that searches for "location AND country".
Here is what the request XML for the two field search looks like:

function makeCityCountryQuery(location_name, country)
{
	XML  = '<?xml version="1.0" encoding="UTF-8"?>'+"\n";
	XML += '<wfs:GetFeature service="WFS" version="1.0.0"'+"\n";
	XML += '  outputFormat="GML2"'+"\n";
  	XML += '  xmlns:topp="http://www.openplans.org/topp"'+"\n";
  	XML += '  xmlns:wfs="http://www.opengis.net/wfs"'+"\n";
  	XML += '  xmlns:ogc="http://www.opengis.net/ogc"'+"\n";
  	XML += '  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'+"\n";
  	XML += '  xsi:schemaLocation="http://www.opengis.net/wfs'+"\n";
  	XML += '                      http://schemas.opengis.net/wfs/1.0.0/WFS-basic.xsd">'+"\n";
  	XML += '  <wfs:Query typeName="topp:gnis">'+"\n";
  	XML += '   <ogc:Filter>'+"\n";
  	XML += '      <ogc:And>'+"\n";
	XML += '        <ogc:PropertyIsLike wildCard="*" singleChar="." escape="\\">'+"\n";
	XML += '           <ogc:PropertyName>topp:full_name_lc</ogc:PropertyName>'+"\n";
	XML += '           <ogc:Literal>'+location_name+'*</ogc:Literal>'+"\n";
	XML += '        </ogc:PropertyIsLike>'+"\n";
	XML += '        <ogc:PropertyIsEqualTo>'+"\n";
	XML += '           <ogc:PropertyName>topp:country_name</ogc:PropertyName>'+"\n";
	XML += '           <ogc:Literal>'+country+'</ogc:Literal>'+"\n";
	XML += '        </ogc:PropertyIsEqualTo>'+"\n";
	XML += '      </ogc:And>'+"\n";
   	XML += '   </ogc:Filter>'+"\n";
  	XML += '  </wfs:Query>'+"\n";
	XML += '</wfs:GetFeature>'+"\n";

	return XML;
}

For more information on setting up Filters, look here: http://www.opengeospatial.org/docs/02-059.pdf
(yes it's a spec, sorry)

Handling the Response

We now can send a request off, with multiple parameters, and get a response back. Next we have to parse the response into something nice to look at.
To do this we need to walk the DOM object sent back in the response.
(the DOM object is the geo_xmlhttp.responseXML object: a bunch of XML)
What this really is, is digging through the XML and finding the information we want. It looks like this:

var fc_node = geo_xmlhttp.responseXML.getElementsByTagName('FeatureCollection');	// mozilla only

This will walk the first level of the returned XML document and look for the tag "FeatureCollection").

<wfs:FeatureCollection>

To do this in IE and Mozilla, I had to write a separate function that would attach the namespace of the tag to the tag name for Internet Explorer:

// Example:
// tag_name=FeatureCollection
// tag_prefix=wfs
function getElements(node,tag_prefix,tag_name)
{
	if (window.ActiveXObject)
	{
		//IE has no idea of namespaces/prefixes
		return node.getElementsByTagName(tag_prefix+":"+tag_name);
	}
	else if (window.XMLHttpRequest)
	{
		//mozilla
		return node.getElementsByTagName(tag_name);
	}
	else
		log("Unsupported browser format: not expecting this kind of browser.");
}

Mozilla can search for tags regardless of the namespace, IE needs the namespace to be specified. To call the method to search for a "FeatureCollection" tag, it looks like this:

var node = geo_xmlhttp.responseXML;
fc_node_array = getElements(node,'wfs','FeatureCollection');
fc_node = fc_node_array[0]; // get the first one

An array of elements is returned from the getElements function. We know there is just going to be one FeatureCollection element, so we grab the first, [0].

Now, we want to run through the XML response and pull out all the 'FeatureMembers'. They are the results that the WFS server found, and is what we want to give back to the user.
Each FeatureMember has a handfull of information:

full_name	: "Vancouver"
full_name_lc	: "vancouver"
sub_national	: "British Columbia"
country_name	: "Canada"
country_code	: "CA"
type		: "Populated Place"
uniq_featcode	: "-575268"
geometry	: point geometry: -123.1333333,49.25

In order to get to this information, we have to dig a little deeper in the XML response to get at it. Here are the few lines that do just that:

fc_node = getElements(geo_xmlhttp.responseXML,"wfs","FeatureCollection")[0];
fm_nodes = getElements(fc_node,"gml","featureMember"); // many nodes
for (i=0; i<fm_nodes.length; i++)
{
	fm = fm_nodes[i]; // our individual FeatureMember
	gnis_node = getElements(fm,"topp","gnis")[0]; // get the next node: <topp:gnis>

	// now grab the values out of the XML tag and save them
	var type = getElements(gnis_node,"topp","type")[0].firstChild.nodeValue;
	var name = getElements(gnis_node,"topp","full_name")[0].firstChild.nodeValue;
	var province = getElements(gnis_node,"topp","sub_national")[0].firstChild.nodeValue;
	var country = getElements(gnis_node,"topp","country_name")[0].firstChild.nodeValue;

	geom = getElements(gnis_node, "topp", "the_geom")[0];
	point = getElements(geom, "gml", "Point")[0];
	feature_point = getElements(point,"gml","coordinates")[0].firstChild.nodeValue;

	var result = name + " is a " + type + " in " + province + ", " + country;
	// "Vancouver is a Populated Place in BC, Canada"

	log("Result item: " + result); // print it out
}

Here is a pseudo view of the returned XML response for the FeatureCollection so you can see the hierarchy:

<wfs:FeatureCollection>
	<gml:featureMember>
		<topp:gnis>
			<topp:full_name>Vancouver</topp:full_name>
			<topp:full_name_lc>vancouver</topp:full_name_lc>
			<topp:sub_national>British Columbia</topp:sub_national>
			<topp:country_name>Canada</topp:country_name>
			<topp:country_code>CA</topp:country_code>
			<topp:type>Populated Place</topp:type>
			<topp:uniq_featcode>-575268</topp:uniq_featcode>
			<topp:the_geom>
				<gml:Point>
					<gml:coordinates></gml:coordinates>
				</gml:Point>
			</topp:the_geom>
		</topp:gnis>
	</gml:featureMember>
</wfs:FeatureCollection>

What we did was first get the 'FeatureCollection', then get the child of that called 'FeatureMember', then get the child of that called 'gnis', and then grab all the information from the children in 'gnis'.

You can use the getElements method to walk a normal HTML document too. Give it a try, just put in a blank prefix.

At this point, we have sent a request to the WFS server, gotten a response, and parsed the response. In the end we have it printing out lines that look like:
Vancouver is a city in British Columbia, Canada
Vancouver Island is an island in British Columbia, Canada
Vancouver Island Ranges are mountains in British Columbia, Canada
Vancouver is a city in WA, United States
Vancouver is a city in TN, United States
Vancouver Junction is a city in WA, United States
Vancouver is a rock in New Zealand, New Zealand
Vancouver Arm is a bay in New Zealand, New Zealand
Vancouver Beach is a beach in Western Australia, Australia

I did some extra formatting to look at the 'type' of the feature and change the english response to better fit. I also sorted by city name first, everything else after.

The next step is to prepare for the user clicking on the the search result and having the map zoom to that location (pretty sweet!).
That is covered in a later section. First we need a map!

TROUBLESHOOTING

Getting errors when you send off your XML getFeature request? If you are using a Mozilla browser, it won't allow you to send content to a remote server. Sigh, but this is good! ... for security reasons.
To get around this, you have to put your page in the same spot where your WFS server is. This isn't actually getting around the problem, it is what you have to do.
We found that we also needed to have the server use the same port.
I encountered this problem when I was testing the page on my local machine and sending the request off to the remote server (GeoServer). I just moved the page to the server and tested from there.

SUMMARY

Created a form with two entries: 'geographic location name' and 'country'.

Took the users input from the form and build up an XML request (GetFeature request).

Created an XMLhttp request object.

  • Put the correct URL in the object:
  • Added the getFeature request XML to the object
  • Sent the object off to the server

Waited for a response from the server.

Once the response was received, we parsed the results by walking the XML structure (getElements() function).

Displayed the results to the user.

>>Proceed onto the next step, Step 4: Setting up a Map >>