HOWTO: Using the XForms model to drive Google Maps

The XForms model is a powerful component which provides declarative syntax for defining data, actions and submission requests. This tutorial demonstrates how to use some of that power to drive the Google Maps JavaScript library with data from a UK public transport information feed; although any other feed that contains map co-ordinates in the data could be used instead.

Because this tutorial is purely concerned with the XForms model, we shall assume the existence of a number of JavaScript mapping functions. In addition, what follows also relies upon the existence of an HTML div element with @id="div-map" to act as a container for the map.

The first thing that the form must do is construct a map on the page. This is accomplished by the following actions:

<xf:action ev:event="xforms-ready">
	<xf:dispatch name="rssmap-create" target="mdl-rssmap" />
</xf:action>

...

<xf:action ev:event="rssmap-create">
	<xf:setvalue ref="instance('inst-dummy')" value="js:CreateMap('div-map')" />

	<xf:setvalue
	 ref="instance('inst-dummy')"
	 value="js:CenterMap(number(instance('inst-constants')/centre-point/@latitude),
	                     number(instance('inst-constants')/centre-point/@longitude),
	                     number(instance('inst-constants')/zoom-factor))"
	/>
</xf:action>

The model that we are building will use data from an RSS feed to drive the map. This requires two instances and a submission:

<xf:instance id="inst-request">
	<request xmlns="" />
</xf:instance>

<xf:instance id="inst-response">
	<response xmlns="" />
</xf:instance>

<xf:submission
 id="sbm-feed"
 ref="instance('inst-request')"
 action="http://www.bbc.co.uk/travelnews/tpeg/en/pti/pti_tpeg.xml"
 method="get"
 replace="instance"
 instance="inst-response"
/>

The first instance will remain empty, since our submission is not required to post any data. The second instance will be used to store the result of the submission; in this case, the feed from the URL http://www.bbc.co.uk/travelnews/tpeg/en/pti/pti_tpeg.xml.

Once the submission is implemented, we must write an action to handle the result data by overlaying it onto our map:

<xf:action ev:event="rssmap-overlay">
	<xf:setvalue ref="instance('inst-dummy')" value="js:ClearMapPoints()" />

	<xf:action iterate="instance('inst-response')//geo:lat[../geo:long]">
		<xf:setvalue
		 ref="js:CreateMapPoint(number(.),
		                        number(../geo:long),
		                        string(../description),
		                        instance('inst-dummy'))"
		 value="''"
		/>
	</xf:action>

	<xf:action iterate="instance('inst-response')/tpeg_message/public_transport_information//WGS84">
		<xf:setvalue
		 ref="js:CreateMapPoint(number(@latitude),
		                        number(@longitude),
		                        concat(../../../transport_operator_description/operator_name/@name,
		                               ': ',
		                               ../../../../summary),
		                        instance('inst-dummy'))"
		 value="''"
		/>
	</xf:action>
</xf:action>

After clearing any existing map point, there are two actions that iterate through all of the map locations in the response data. The first of these actions handles RSS response data containing geo:lat and geo:long elements. The second handles tpegML response data containing WGS84 elements. This apparent duplication enables the form to work with data from both feed types, without any noticeable difference to the user.

The 'rssmap-overlay' action should be invoked when the submission has returned successfully, like this:

<xf:submission
 id="sbm-feed"
 ref="instance('inst-request')"
 action="http://www.bbc.co.uk/travelnews/tpeg/en/pti/pti_tpeg.xml"
 method="get"
 replace="instance"
 instance="inst-response"
>
	<xf:action ev:event="xforms-submit-done">
		<xf:dispatch name="rssmap-overlay" target="mdl-rssmap" />
	</xf:action>
</xf:submission>

The next step is to invoke our submission when the form is loaded and then again at regular intervals. This ensures that the information on the map is kept up to date for as long as the form is open.

To invoke the submission on form load requires a send element to be added to our 'xforms-ready' action:

<xf:action ev:event="xforms-ready">
	<xf:dispatch name="rssmap-create" target="mdl-rssmap" />

	<xf:send submission="sbm-feed" />
</xf:action>

In order to then keep the map updated, we must first add an 'rssmap-refresh' action to the model:

<xf:action ev:event="rssmap-refresh">
	<xf:send submission="sbm-feed" />
</xf:action>

Then we need to incorporate two short JavaScript functions into the form:

function InitialiseTimer(interval, refnode)
{
	window.setInterval("UpdateMap();", interval);

	return "";
}

function UpdateMap()
{
	var evt = document.createEvent("Event");
	evt.initEvent("rssmap-refresh", false, false);

	var mdl = document.getElementById("mdl-rssmap");
	mdl.dispatchEvent(evt);
}

We can now call the first of these two functions to set up our timer. The second one will then be called by the script interpreter after each interval and simply dispatches our 'rssmap-refresh' event to the model element, invoking our new action. These functions can now be called from our 'xforms-ready' action:

<xf:action ev:event="xforms-ready">
	<xf:dispatch name="rssmap-create" target="mdl-rssmap" />

	<xf:send submission="sbm-feed" />

	<xf:setvalue
	 ref="instance('inst-dummy')"
	 value="js:InitialiseTimer(number(instance('inst-constants')/update-interval),
	                           instance('inst-dummy'))"
	/>
</xf:action>

All that is left to implement now is garbage collection. This can be achieved with a simple action for 'xforms-model-destruct':

<xf:action ev:event="xforms-model-destruct">
	<xf:setvalue ref="instance('inst-dummy')" value="js:DestroyMap()" />
</xf:action>

If you wish to run the complete sample application, you must first install the latest versions of formsPlayer and the Sidewinder Viewer; once they are installed you can execute the the sample by clicking here.

You can also get the full source.

Sidewinder Screenshot