Tuesday, July 26, 2005

Using XSL to Transform Google Earth (KML) and GPX to Google Maps API

I noticed that Google Earth ships with something called GPSBabel (and my old friend expat.dll). Now I knew almost nothing about the world of GPS devices but I soon found that there are (too) many GPS file formats in the world. GPX seems to have become the de-facto XML standard but because of the wide variety of GPS hardware there are still plenty of different plain text file formats kicking about. GPSBabel is GPS format conversion software. I noticed that Google Earth can load GPX files! I found that using free GPS conversion utilities like GPS utility, G7toWin and GPSBabel I can pretty much convert anything I find on the net into a format that I can import in Google Earth (the freeware version!).

Secondly, I thought I could write an XSL to convert GPX to KML and vice versa but I found to my delight that GPSBabel has provisional support for converting GPX to KML, so I can leave the XSL stylesheet to someone who really needs to do it.

All this is good so far but what if I want to put the route of a walk on my website using Google Maps API. How difficult is it to convert KML (or GPX for that matter) into the Google Maps API. I wrote two stylesheets that generate the Google Maps JavaScript that make a map containing a "GPolyline" of a route. I used a GPS text file of walk around the Lake District in Cumbria (in England). Surprisingly it is *much* easier to transform GPX to Google Maps than the KML format. My conclusion would be if you're veering towards using Google Earth professionally then don't throw away your GPX files until someone writes an XSL to convert them back from KML format (you need an escape strategy).

Get your Google Maps API key here.

KML to Google Maps API XSL Stylesheet.

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method = "text" version="1.0" omit-xml-declaration="yes" />

<xsl:template match="/">
<xsl:apply-templates select="*[local-name()='kml']/*[local-name()='Document']"/>
</xsl:template>

<xsl:template match="*[local-name()='kml']/*[local-name()='Document']">
<xsl:apply-templates select="*[local-name()='Folder']"/>
</xsl:template>

<xsl:template match="*[local-name()='Folder']">
<xsl:apply-templates select="*[local-name()='Folder' or local-name()='Placemark']"/>
</xsl:template>

<xsl:template match="*[local-name()='Placemark']">
<xsl:apply-templates select="*[local-name()='MultiGeometry']"/>
</xsl:template>

<xsl:template match="*[local-name()='MultiGeometry']">
<xsl:apply-templates select="*[local-name()='LineString']"/>
</xsl:template>

<xsl:template match="*[local-name()='LineString']">
<xsl:apply-templates select="*[local-name()='coordinates']"/>
</xsl:template>

<xsl:template match="*[local-name()='coordinates']">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"&gt;
&lt;html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"&gt;
&lt;head&gt;
&lt;meta http-equiv="content-type" content="text/html; charset=UTF-8"/&gt;
&lt;title&gt;Google Maps API Example - overlay&lt;/title&gt;
&lt;style type="text/css"&gt;
v\:* {
behavior:url(#default#VML);
}
&lt;/style&gt;
&lt;script src="http://maps.google.com/maps?file=api&amp;v=1&amp;key=ABQIAAAA4Wxrd1ZmQfRHvggZWM0QkxSywvohUEBj468j1bHLctjAi9H1aRTgpH5EJsqp8F3DqOP3spOw36wc2A" type="text/javascript"&gt;&lt;/script&gt;
&lt;script type="text/javascript"&gt;
//&lt;![CDATA[
function onLoad() {
var map = new GMap(document.getElementById("map"));
map.setMapType(G_HYBRID_TYPE)
map.addControl(new GSmallMapControl());
map.addControl(new GMapTypeControl());
<xsl:call-template name="firstPoint">
<xsl:with-param name="str" select="normalize-space(.)"/>
</xsl:call-template>
var points = [];
<xsl:call-template name="split">
<xsl:with-param name="str" select="normalize-space(.)"/>
</xsl:call-template>
map.addOverlay(new GPolyline(points));
}
//]]&gt;
&lt;/script&gt;
&lt;/head&gt;
&lt;body onload="onLoad()"&gt;
&lt;div id="map" style="width: 500px; height: 300px"&gt;&lt;/div&gt;
&lt;div id="message"&gt;&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</xsl:template>

<xsl:template name="firstPoint"><xsl:param name="str"/>
<xsl:if test="contains($str,' ')"> map.centerAndZoom(new GPoint(<xsl:value-of select="substring-before(substring-before($str,' '),',0')"/>), 5);
</xsl:if>
</xsl:template>

<xsl:template name="split"><xsl:param name="str"/>
<xsl:choose><xsl:when test="contains($str,' ')"> points.push(new GPoint(<xsl:value-of select="substring-before(substring-before($str,' '),',0')"/>));
<xsl:call-template name="split">
<xsl:with-param name="str" select="normalize-space(substring-after($str,' '))"/>
</xsl:call-template></xsl:when>
<xsl:otherwise></xsl:otherwise>
</xsl:choose>
</xsl:template>

</xsl:stylesheet>
GPX to Google Maps API XSL Stylesheet

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method = "text" version="1.0" />

<xsl:template match="/">
<xsl:apply-templates select="*[local-name()='gpx']"/>
</xsl:template>

<xsl:template match="*[local-name()='gpx']">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"&gt;
&lt;html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"&gt;
&lt;head&gt;
&lt;meta http-equiv="content-type" content="text/html; charset=UTF-8"/&gt;
&lt;title&gt;Google Maps API Example - overlay&lt;/title&gt;
&lt;style type="text/css"&gt;
v\:* {
behavior:url(#default#VML);
}
&lt;/style&gt;
&lt;script src="http://maps.google.com/maps?file=api&amp;v=1&amp;key=ABQIAAAA4Wxrd1ZmQfRHvggZWM0QkxSywvohUEBj468j1bHLctjAi9H1aRTgpH5EJsqp8F3DqOP3spOw36wc2A" type="text/javascript"&gt;&lt;/script&gt;
&lt;script type="text/javascript"&gt;
//&lt;![CDATA[
function onLoad() {
var map = new GMap(document.getElementById("map"));
map.setMapType(G_HYBRID_TYPE)
map.addControl(new GSmallMapControl());
map.addControl(new GMapTypeControl());
var points = [];
<xsl:apply-templates select="*[local-name()='trk']/*[local-name()='trkseg']/*[local-name()='trkpt']"/>
map.addOverlay(new GPolyline(points));
}
//]]&gt;
&lt;/script&gt;
&lt;/head&gt;
&lt;body onload="onLoad()"&gt;
&lt;div id="map" style="width: 500px; height: 300px"&gt;&lt;/div&gt;
&lt;div id="message"&gt;&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</xsl:template>

<xsl:template match="*[local-name()='trk']/*[local-name()='trkseg']/*[local-name()='trkpt']">
<xsl:if test="position() = 1">
map.centerAndZoom(new GPoint(<xsl:value-of select="@lon"/>, <xsl:value-of select="@lat"/> ), 5);
</xsl:if>
points.push(new GPoint(<xsl:value-of select="@lon"/>, <xsl:value-of select="@lat"/> ));
</xsl:template>


</xsl:stylesheet>

Google Earth

Google Maps

Also a few other miscellaneous things I've noticed recently about Google Earth and it's sister Google Maps.

  • The user-agent for Google Earth is kh_unk/unk
  • Google Maps core functionality isn't really that AJAXy, all the co-ordinates for the map tile URLs are calculated in the client side JavaScript (granted, it is still extremely clever)
  • Both Google Earth and Google Maps use the same server for the keyhole satellite maps: kh.google.com
  • Google Maps also uses a server called: mt.google.com, I'm guessing the "mt" stands for map tiles
  • Both mt and kh servers run Linux! Hurray!
  • The "web server" on kh is running Keyhole Server 2.4
  • The "web server" on mt is running something called "tfe" - (educated guess, does the "t" stand for telemetry?)
  • There is a Google Earth server hardware available for industry but no mention anywhere of how much it will cost, I suspect if you have to ask you can't afford it!

Monday, July 25, 2005

Combining CASFilter with Tomcat Realms using SecurityFilter

I have managed to combine using CASFilter and SecurityFilter. This means I can use CAS for authentication and Tomcat Realms (e.g JDBCRealm, DataSourceRealm, JNDIRealm) for the actual role authorization (isUserInRole).

See this page on the CAS wiki for details:Combining CASFilter with Tomcat Realms using SecurityFilter

Sunday, July 24, 2005

A simple Java KMZ Servlet Filter for use with the Google Earth client

Google Earth reads KML and KMZ files, the MIME types for these are:

  • application/vnd.google-earth.kml+xml kml
  • application/vnd.google-earth.kmz kmz

The KMZ format looks to be simply a PKZipped version of the KML file. Although rather interestingly there is scope to add images and perhaps other media (e.g. 3d models, video and audio) into the zip file.

It seems that at present the main body of Google Earth developers are using PHP but as you can see from my recent posts I have been having some success developing for it with JSP technologies.

First, I tried to load a GZipped file into Google Earth but that didn't appear to work. It looks that Google Earth currently only supports "PKZIP" style files at present.

I thought to myself that I could create a Servlet Filter to dynamically create PKZIP files. I found an old article entitled Two Servlet Filters Every Web Application Should Have (circa 2003, virtually Jurassic!) on O'Reilly's OnJava that describes how to create a GZIP compression servlet filter to automatically compress the output of JSPs. I need to produce something very similar but this time with the emphasis on PKZIP.

It took a little longer that I expected to get my KMZFilter working. It seems that "all modern browsers" are assumed to accept the gzip encoding. Therefore there no longer seems to much point in searching for "gzip" in the accept-encoding request header as most browsers don't bother to send that header anymore. Additionally it isn't quite as straight forward as changing all the java.util.zip.GZIPOutputStream references in the GZIPFilter to java.util.zip.ZipOutputStream either.

The KMZ servlet filter version I produced does no yet support zipping up multiple files. To support multiple files I'd need to work out from the requested filename (e.g. index.kmz) the file list that I'd need to include in the zip file, perhaps an XML properties file would do the trick (actually that would be quite an elegant solution).

My web.xml contains the following, note that I have mapped the KMZ extension to JSP so that I can carry on writing JSP to do the actual KML file creation:


<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd"
>

<web-app>

<filter>
<filter-name>KMZFilter</filter-name>
<filter-class>markmc.KMZFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>KMZFilter</filter-name>
<url-pattern>*.kmz</url-pattern>
</filter-mapping>

<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.kmz</url-pattern>
</servlet-mapping>

<taglib>
<taglib-uri>http://jakarta.apache.org/taglibs/string-1.0</taglib-uri>
<taglib-location>/WEB-INF/taglibs-string.tld</taglib-location>
</taglib>

</web-app>

Kudos to Jayson Falkner as I stole most of the code from his GZIPFilter but you are very welcome to have the source code filter for the KMZFilter I produced. I make no guarantees about the filters reliability, actually that probably goes for almost everything I produce (I will not be a hostage to responsibility)!!

Download KMZFilter source code

I ran into another little problem that was fixed by updating my Google Earth client to the latest version (at this time 3.0.0395(beta)) and "Presto Chango!" it works great.

[Update]
Inspired by the compression filter example that comes with Tomcat I have improved my filter and it works much better now.

$CATALINA_HOME/webapps/examples/WEB-INF/classes/compressionFilters

This was not helped by the Servlet Filters responses of <html><body></body></html> rather than an error I could actually do something with! If I were being professional about it I'd also add some code to make sure, even when responding with a servlet exception that the response from this filter was always KML/KMZ.

Saturday, July 23, 2005

Quick Tip: Use Google hack to find Google Earth KML files

Find only Google Earth KML files by using the well known "filetype:" Google hack. There are around 5290 KML files indexed by Google at the time of writing this. Since KMZ files are binary it doesn't seem worth searching for those at the moment.

http://www.google.com/search?hl=en&q=filetype%3Akml+kml

Friday, July 22, 2005

GeoURL To Google Earth client using KML

I'm going to show how to combine the GeoURL RSS feed with Google Earth client using JSTL.

In my last blog entry, I showed how you could get a web service and Google Earth client to communicate using a KML file dynamically generated by JSP. Now I'm going to extend this approach so that a list of blog in the immediate vicinity of a location are displayed inside the Google Earth client by making use of the GeoURL RSS feed.

Essentially this is quite easy because in the last entry we already produced a script that could calculate the centre co-ordinates of a Google Earth map from details passed to it from the client application. Since we have the central map co-ordinates we can query the GeoURL feed for blogs in that vicinity. The final stage is to convert the RDF feed returned into the KML format that the Google Earth client requires.

In my XSLT I borrow a few tricks from one of my *favourite* XSLT related articles Never Mind the Namespaces: An XSLT RSS Client (which is incidentally very useful if you want to create an XSLT that can transform multiple RSS formats). There are a couple of namespaces knocking about in the RDF feed that GeoURL returns and for my purposes I'd be happier ignoring them. So here is the XSL stylesheet which will transform the GeoURL RDF into KML format.


<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://earth.google.com/kml/2.0">
<xsl:output method="xml" omit-xml-declaration="no" indent="yes" />

<xsl:template match="/">
<kml xmlns="http://earth.google.com/kml/2.0">
<Folder>
<name>GeoURL</name>
<open>1</open>
<xsl:apply-templates select="*[local-name()='RDF']" />
</Folder>
</kml>
</xsl:template>

<xsl:template match="*[local-name()='RDF']">
<xsl:apply-templates select="*[local-name()='item']" />
</xsl:template>

<xsl:template match="*[local-name()='item']">
<Placemark>
<description>
<xsl:element name="a">
<xsl:attribute name="href">
<xsl:value-of select="*[local-name()='link']" />
</xsl:attribute>
<xsl:value-of select="*[local-name()='title']" />
</xsl:element>
</description>
<name>
<xsl:value-of select="*[local-name()='title']" />
</name>
<LookAt>
<longitude>
<xsl:value-of select="*[local-name()='longitude']" />
</longitude>
<latitude>
<xsl:value-of select="*[local-name()='latitude']" />
</latitude>
<range>540.68</range>
<tilt>0</tilt>
<heading>3</heading>
</LookAt>
<Point>
<coordinates>
<xsl:value-of select="*[local-name()='longitude']" />,<xsl:value-of select="*[local-name()='latitude']" />,0
</coordinates>
</Point>
</Placemark>
</xsl:template>
</xsl:stylesheet>

It is simply now a matter of creating an KML document describing the "Network Link".


<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://earth.google.com/kml/2.0">
<NetworkLink>
<name>GeoURL Blogs Link</name>
<open>1</open>
<Url>
<href>http://localhost/GoogleEarth/geourlRSSFetch.jsp</href>
<viewRefreshMode>onStop</viewRefreshMode>
<viewRefreshTime>5</viewRefreshTime>
</Url>
<visibility>1</visibility>
</NetworkLink>
</kml>

...and modifying the JSP I previously created to make the GeoURL feed request and perform the XSL transformation.


<%@ page contentType="application/vnd.google-earth.kml+xml; charset=UTF-8" %><%--
--%>
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %><%--
--%>
<%@ taglib prefix="x" uri="http://java.sun.com/jstl/xml_rt" %><%--
--%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt_rt" %><%--
--%>
<%@ taglib uri="http://jakarta.apache.org/taglibs/string-1.1" prefix="str" %><%--
--%>
<c:if test="${empty applicationScope.geo2kmlXSLT}"><%--
--%>
<c:import var="geo2kmlXSLT" url="geourl2kml.xsl" scope="application" /><%--
--%>
</c:if><%--

longitude_west, latitude_south, longitude_east, latitude_north

--%>
<c:choose><%--
--%>
<c:when test="${!empty param.BBOX}"><%--
--%>
<str:split separator="," var="bbox"><%--
--%>
<c:out value="${param.BBOX}" /><%--
--%>
</str:split><%--
--%>
<c:set var="longitude_west" value="${bbox[0]}"/><%--
--%>
<c:set var="latitude_south" value="${bbox[1]}"/><%--
--%>
<c:set var="longitude_east" value="${bbox[2]}"/><%--
--%>
<c:set var="latitude_north" value="${bbox[3]}"/><%--
--%>
<c:set var="userlon" value="${(longitude_east - longitude_west)/2 + longitude_west}"/><%--
--%>
<c:set var="userlat" value="${(latitude_north - latitude_south)/2 + latitude_south}"/><%--
--%>
</c:when><%--
--%>
<c:otherwise><%--
--%>
<c:set var="userlon" value="-2.6018"/><%--
--%>
<c:set var="userlat" value="51.4595"/><%--
--%>
</c:otherwise><%--
--%>
</c:choose><%--

http://geourl.org/near/?lat=51.4595&long=-2.6018;format=rss10

--%>
<c:set var="long"><c:out value="${userlon}" /></c:set><%--
--%>
<c:set var="lat"><c:out value="${userlat}" /></c:set><%--
--%>
<c:set var="fetchurl">http://geourl.org/near/?lat=<c:out value="${lat}"/>&amp;long=<c:out value="${long}"/>&amp;format=rss10</c:set><%--
--%>
<c:import var="geourl_rss" url="${fetchurl}"/><%--

--%>
<x:transform xml="${geourl_rss}" xslt="${geo2kmlXSLT}"/>

Using Google Earth's KML with J2EE (JSP/JSTL)

Google Earth is cool despite being proprietary software which is not usually my thing. A few weeks ago I did some experiments with the Google Maps API and this was quite successful. I've just discovered that there is an "API" of sorts for the Google Earth client and thought I'd give that a whirl too. Google Earth's underlying communication format is called KML (an XML format). See:

Google Earth KML Tutorial
Google Earth KML Documentation and
Google Earth Community forums

The examples in the KML tutorial use PHP, I've re-implemented one of them into J2EE which I will reproduce here.

First you need to install the Google Earth Desktop Client.

Google Earth has defined two new MIME content types:

application/vnd.google-earth.kml+xml
application/vnd.google-earth.kmz

KML is just plain text XML and KMZ is compressed (e.g. gzipped) KML.

The following will describe how to produce the "Tracking a Point Directly Under Your View" using J2EE. This uses something calledView-Based Refresh.

First create a KML format document (with extension .kml) and install it on your server containing something like:


<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://earth.google.com/kml/2.0">
<NetworkLink>
<name>Test Network Link</name>
<open>1</open>
<Url>
<href>http://localhost/GoogleEarth/index.jsp</href>
<viewRefreshMode>onStop</viewRefreshMode>
<viewRefreshTime>1</viewRefreshTime>
</Url>
<visibility>1</visibility>
</NetworkLink>
</kml>

This essentially informs the Google Earth client of the details of your web service, when and how it should be called. Once you've written your dynamic script (as below) then all you need do is load it in your web browser [e.g http://localhost/GoogleEarth/test.kml]. Once installed Google Earth should pickup KML files automatically.

You'll see in the above document that I've asked it to retrieve a JSP page from my local server. This is the JSP page which will dynamically create KML documents and will be called periodically (at an interval defined in the previous document) by the Google Earth Client:


<%@ page contentType="application/vnd.google-earth.kml+xml; charset=UTF-8" %><%--
--%>
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %><%--
--%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt_rt" %><%--
--%>
<%@ taglib uri="http://jakarta.apache.org/taglibs/string-1.1" prefix="str" %><%--

--%>
<jsp:useBean id="now" class="java.util.Date"/><%--

longitude_west, latitude_south, longitude_east, latitude_north

--%>
<str:split separator="," var="bbox"><%--
--%>
<c:out value="${param.BBOX}" /><%--
--%>
</str:split><%--
--%>
<c:set var="longitude_west" value="${bbox[0]}"/><%--
--%>
<c:set var="latitude_south" value="${bbox[1]}"/><%--
--%>
<c:set var="longitude_east" value="${bbox[2]}"/><%--
--%>
<c:set var="latitude_north" value="${bbox[3]}"/><%--
--%>
<c:set var="userlon" value="${(longitude_east - longitude_west)/2 + longitude_west}"/><%--
--%>
<c:set var="userlat" value="${(latitude_north - latitude_south)/2 + latitude_south}"/><%--
--%>
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://earth.google.com/kml/2.0">
<Placemark>
<description><c:out value="${param.BBOX}" /></description>
<name><fmt:formatDate value="${now}" type="time" timeStyle="full" /></name>
<LookAt>
<longitude><c:out value="${userlon}"/></longitude>
<latitude><c:out value="${userlat}"/></latitude>
<range>540.68</range>
<tilt>0</tilt>
<heading>3</heading>
</LookAt>
<Point>
<coordinates><c:out value="${userlon}"/>,<c:out value="${userlat}"/>,0</coordinates>
</Point>
</Placemark>
</kml>

I've used the Jakarta String tag library to split the BBOX parameter by the comma separator but apart from that this is JSTL. This JSP page is called after the "camera" comes to a stop in the Google Earth client. So you are presented with a new marker after every pan around the globe comes to a halt. In my next entry I plan to describe how I managed to extend this method to combine Google Earth with GeoURL feed data.

[Note] In my case I ran these scripts on Tomcat 5, depending on your server you may or may not need the addition "_rt" on the end of the JSTL tag library references.

Tuesday, July 19, 2005

IBM's aDesigner: Useful accessibility testing app

IBM alphaWorks aDesigner is described as "A disability simulator that helps Web designers ensure that their pages are accessible and usable by the visually impaired." It is a desktop client based on Java and Eclipse technology.

What is particularly nice about it is that it allows me conduct accessibility tests on web applications that require authentication; where external online accessibility validators like WebXACT (previously called Bobby) and Cynthia Says wouldn't be able to access. To be fair I believe you can purchase a version of Bobby that can be configured to work with authentication protected web pages ( there is also a desktop version of Cynthia Says which might possibly also work). Although I'm easily discouraged from using any software that I have to pay for.

Unlike with the online versions of Cynthia Says and WebXACT, with aDesigner I was able to login to my instance of uPortal and test the pages directly. Prior to this tool I had to conduct any accessibility testing on static dumps of the portal pages.

Also as far as I can make out it is free for Academic use. The only possible downside I can see so far is that you need to register with the IBM site to download it.

Efficient parsing of largish XML with JSTL and the XMLFilter

I discovered whilst browsing through Google Print that Shawn Bayern (in his excellent book JSTL in Action) described using an XMLFilter with JSTLs <x:parse>. What an XMLFilter allows you to do is discard the parts of an XML Document which you don't need before further processing. The XMLFilter is SAX rather than DOM based which apparently means that it can be more efficient in terms of the processing time and the memory it uses. The DOM method would need to build the entire document in memory before any processing can occur, whereas SAX works more like a parser reading only those parts of the XML document that it needs to.

In Shawn's example he created a taglib called Spath that is used to produce an XMLFilter via a limited subset of XPath. Part of SPath is generated with JavaCC. This all looked a bit complicated for me to use for my own purposes. I then discovered that it is actually quite simple to create an XMLFilter from an XSL transformation document.

For small XML documents you are unlikely to gain anything from adding an XMLFilter but if you are dealing with XML documents of a reasonable size then an additional filter can make quite a difference.

As an example I obtained a copy of Hamlet in XML format (I removed the DTD reference from it as it was causing trouble). For the filtered example I discarded all but the first scene of the first act, in the unfiltered example I processed the whole play (apologies for the scriptlets).


<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<%@ taglib prefix="x" uri="http://java.sun.com/jstl/xml" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt" %>

<%@ page import="org.xml.sax.XMLFilter" %>
<%@ page import="javax.xml.transform.sax.SAXTransformerFactory" %>
<%@ page import="javax.xml.transform.Source"%>
<%@ page import="javax.xml.transform.stream.StreamSource"%>
<%@ page import="java.io.StringReader"%>
<%@ page import="javax.xml.transform.TransformerFactory"%>

<c:set var="xsl" scope="page">
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="/">
<xsl:copy-of select="//PLAY/ACT[1]/SCENE[1]"/>
</xsl:template>

</xsl:stylesheet>
</c:set>

<%
String xslt = (String) pageContext.getAttribute("xsl");
Source xsltSource = new StreamSource(new StringReader(xslt));
TransformerFactory tfactory = TransformerFactory.newInstance();
SAXTransformerFactory stf = (SAXTransformerFactory) tfactory;
XMLFilter filter = stf.newXMLFilter(xsltSource);
pageContext.setAttribute("filter", filter);
%>

<c:import url="hamlet.xml" var="feed" />

<jsp:useBean id="before" class="java.util.Date" />
<h1>
<fmt:formatDate value="${before}" type="both" pattern="HH:mm:ss:SSS" />
</h1>

<x:parse var="a" filter="${filter}"><c:out value="${feed}" escapeXml="false" /></x:parse>

<x:forEach select="$a//SPEECH" var="speech">
<p style="font-size: 8pt;">
<b><x:out select="$speech/SPEAKER"/></b><br />
<x:forEach select="$speech/LINE" var="line">
<x:out select="$line"/><br />
</x:forEach>
</p>
</x:forEach>

<jsp:useBean id="after" class="java.util.Date" />
<h1>
<fmt:formatDate value="${after}" type="both" pattern="HH:mm:ss:SSS" />
</h1>

<x:parse var="b"><c:out value="${feed}" escapeXml="false" /></x:parse>

<x:forEach select="$b//PLAY/ACT[1]/SCENE[1]/SPEECH" var="speech">
<p style="font-size: 8pt;">
<b><x:out select="$speech/SPEAKER"/></b><br />
<x:forEach select="$speech/LINE" var="line">
<x:out select="$line"/><br />
</x:forEach>
</p>
</x:forEach>

<jsp:useBean id="after2" class="java.util.Date" />
<h1>
<fmt:formatDate value="${after2}" type="both" pattern="HH:mm:ss:SSS" />
</h1>

The result is the filtered document processing looks like it is faster than the unfiltered document processing. Granted this is not a very scientific test and I'm not claiming that it is the most efficient use of either SAX or DOM techniques. It does however highlight a relatively easy way to make use of an interesting and little known feature of JSTL.

Monday, July 04, 2005

GeoURL To Google Maps using the Official API: Part II

Previously, I showed how to display the blogs in your immediate vicinity using GeoURL and Google Maps with the Official Google Maps API. I received a comment following this from Ask Bjørn Hansen who runs the GeoURL site. Ask suggested that it would be really good if you could make it so that as you scroll around the map it automatically updates the display showing the blogs in that vicinity. I took this as a bit of a challenge (I occasionally like the odd challenge) and I like to play with AJAXy stuff at the moment with it being flavour of the month.

It is easy using the Google Map API to add an event listener to monitor map scrolling events and read off the longitude and latitude readings. There is even an example of the code needed to do this in the API documentation. After looking through some of the GeoURL source code I discovered that I could receive a GeoURL RSS feed based on a longitude/latitude pair. My first attempt fetched new RSS feeds after every "moveend" event, this initially appeared to be working until the browser slowly ground to a halt. The problem was that I had been adding more and more "markers", those little red icons, until it just became too much for the browser to handle.

The API documentation gives three methods for dealing with markers these are referred to as overlays in the API documentation (as they can be a icon marker or a series of points joined together). These are:


addOverlay(overlay)
# Adds the given overlay object (e.g., GMarker or GPolyline) to the map

removeOverlay(overlay)
# Removes the given overlay object from the map

clearOverlays()

So therefore, I can add an overlay, remove an overlay (if I had a reference to it) or clear all the overlays on the map.

I modified my code to add the clearOverlays() command before adding any new markers and this made the code work better. The problem now was that the markers would blink on and off as they were removed and re-added after every new RSS feed load. This was not very satisfactory.

What I needed to do was this, after a "moveend" event:

  1. Remove overlays that were not now inside the bounds of the visible map (called viewport in the API documentation)
  2. Only add overlays if they would be visible within the bounds of the visible map

Now there didn't appear to be a way to iterate through the existing overlays on a map page, at least not in the official API documentation. I can forgive this as Google Maps is only in Beta and I put it down to an accidental omission in their documentation. After looking at a Google Maps Hacking page I saw a reference to overlays being a property of the map object. I thought that must be the array containing all the overlays in the map. By guesswork and pure dumb luck I was able then to access the longitude and latitude values of each overlay using:


map.overlays[0].point.x
map.overlays[0].point.y

Now I can access the overlay objects, their associate co-ordinates and I can delete the overlays using removeOverlay(overlay) method. I can now remove the overlays that would be outside of the visible map.

Hey presto, it works...it only took about a half day of head scratching. The following is a link to the live example, it works but I'm sure it could probably be improved further.

Access the standalone version here

You can of course copy the static HTML from the above page, I have opted for a Creative Commons license for the code I produce on this site.

It required a modified JSP page to proxy the RSS feed so that the AJAX code could read the XML from a local source. This jsp will fetch a GeoURL RSS feed if called with a reference to an URL or a longitude and latitude pair (For reference: I currently use JSP on Tomcat 4 and I believe it would need to be modified to use the JSTL core_rt taglib on Tomcat 5).


<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %><%--

// http://geourl.org/near/?lat=51.4595&long=-2.6018;format=rss10

--%>
<%@ page contentType="text/xml; charset=UTF-8"%><%--
--%>
<c:choose><%--
--%>
<c:when test="${not empty param.url}"><%--
--%>
<c:set var="fetchurl">http://geourl.org/near/?p=<c:out value="${param.url}" />;format=rss10</c:set><%--
--%>
</c:when><%--
--%>
<c:otherwise><%--
--%>
<c:set var="long"><c:out value="${param.long}" default="-2.6018" /></c:set><%--
--%>
<c:set var="lat"><c:out value="${param.lat}" default="51.4595" /></c:set><%--
--%>
<c:set var="fetchurl">http://geourl.org/near/?lat=<c:out value="${lat}"/>&amp;long=<c:out value="${long}"/>;format=rss10</c:set><%--
--%>
</c:otherwise><%--
--%>
</c:choose><%--
--%>
<c:import url="${fetchurl}"/>

Sunday, July 03, 2005

GeoURL To Google Maps using the Official API

This morning I didn't really know what a GeoURL was, I've seen the button badge on a few blogs but I didn't get as far as clicking on them. It turns out that getting a "GeoURL" is really easy, all you need is the longitude and latitude of the blogs location, you can get this from multimap and the like, and you then just insert a couple of metatags in the head of your webpage and register with GeoURL (they even give you the HTML for the button badges). In my case the metatags are:


<meta name="geo.position" content="51.4595;-2.6018" />
<meta name="ICBM" content="51.4595, -2.6018" />
<meta name="DC.title" content="Mark McLaren's Weblog" />

This is swell you can now go to GeoURL and see what other blogs are registered in your area. One very sexy application of this is to combine this data with Google Maps (UK and US maps only at the moment) to get a visual representation of what blogs are in my vicinity. Leigh Dodds, from Bath [howdy neighbour], did this using the unofficial myGmaps web service. See Leigh's app here

Now as anybody who knows me will tell you, I am a good boy ;-> so when I saw that those clever chaps at Google had realised an official Google Maps API I just had to see what I could do with it. I thought why not try and use it to emulate Leigh's clever little application and blimey, I only went and got it working!!!!

Some interesting points I found along the way, the Google API requires you to sign up for an API key with your domain name in order to use it. It looks to me that this API key is actually verified in the client side JavaScript because in order to get my Tomcat server working I had to specify the domain localhost:8080 (API key for localhost:8080 shown in the example code below, I hope I'm not breaking any rules). The live example uses another API key obtained specifically for use on this blogs server (including the application directory). Out of interest, I did try to look at the Google JavaScript but anyone would think they were trying to obfuscate it or something (strange for a company that are starting to embrace the Open Source culture, see: Google code).

The official API uses AJAX technology to obtain the XML and process it. Due to security concerns the XML that the JavaScript accesses must be located on the same host as the JavaScript page. In order to obtain the GeoURL RSS feed that I need to use I needed another JSP page that downloads this feed on my behalf so that it appears local for the JavaScript's benefit.

I use Firefox browser but I recognise that most people would want this to work in IE (I recognise it but still don't understand why!!). The GeoURL RSS stores two of its key elements (longitude and latitude) with a special GeoURL XML namespace, as you would expect as these elements are currently not part of the RSS specification. I found that I had to do some browser detection in my JavaScript in order to get the namespace stuff working. This is because IE does not support the getElementsTagnameNS method and Firefox doesn't support the way IE supports obtaining tags with namespaces (no surprise there as the IE method looks just plain wrong).

One final point is I have learnt how to spell longitude, I’d always previously thought the word was longtitude (you live and learn!).

Access the standalone version here to see a live working example showing the blogs in my vicinty.

Here is the very simple GeoURL RSS feed proxying JSP needed so that the JavaScript can load the XML from a local server:


<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %><%--
--%>
<%@ page contentType="text/xml; charset=UTF-8"%><%--
--%>
<c:import url="http://geourl.org/near/?p=http://cse-mjmcl.cse.bris.ac.uk/blog;format=rss10"/>

Here is the page containing the JavaScript that uses the Google Maps API to do the work (API key shown is for localhost:8080):


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script src="http://maps.google.com/maps?file=api&amp;v=1&amp;key=ABQIAAAA4Wxrd1ZmQfRHvggZWM0QkxTwM0brOpm-All5BF6PoaKBxRWWERTxZC7TxHvaaZq75azqC6aFU4f4gA" type="text/javascript">
</script>
<script type="text/javascript">
//<![CDATA[

function onLoad() {
var ua = navigator.userAgent;
var isFirefox = ( ua != null && ua.indexOf( "Firefox/" ) != -1 );
var isMSIE = ( ua != null && ua.indexOf( "MSIE" ) != -1 );
var request = GXmlHttp.create();
request.open("GET", "obtain_geourl_rss.jsp", true);
request.onreadystatechange = function()
{
if (request.readyState == 4)
{
var xmlDoc = request.responseXML;
var blogs = xmlDoc.documentElement.getElementsByTagName("item");
var results="";
// Get first set of co-ordinates to set the centre of the map.
var long1;
var lat1;
if (isMSIE)
{
long1=blogs[0].getElementsByTagName("geourl:longitude")[0].firstChild.data;
lat1=blogs[0].getElementsByTagName("geourl:latitude")[0].firstChild.data;
}
if (isFirefox)
{
long1=blogs[0].getElementsByTagNameNS("http://geourl.org/rss/module/","longitude")[0].firstChild.data;
lat1=blogs[0].getElementsByTagNameNS("http://geourl.org/rss/module/","latitude")[0].firstChild.data;
}
// Centre on the first item in the list
var map = new GMap(document.getElementById("map"));
map.addControl(new GSmallMapControl());
map.addControl(new GMapTypeControl());
map.centerAndZoom(new GPoint(parseFloat(long1), parseFloat(lat1)), 4);

for (var i = 0;i < blogs.length;i++)
{
// Title
var title=blogs[i].getElementsByTagName("title")[0].firstChild.data;
// Link
var link=blogs[i].getElementsByTagName("link")[0].firstChild.data;

var long2;
var lat2;

if (isMSIE)
{
long2 = blogs[i].getElementsByTagName("geourl:longitude")[0].firstChild.data;
lat2 =blogs[i].getElementsByTagName("geourl:latitude")[0].firstChild.data;
}
if (isFirefox)
{
long2 =blogs[i].getElementsByTagNameNS("http://geourl.org/rss/module/","longitude")[0].firstChild.data;
lat2 =blogs[i].getElementsByTagNameNS("http://geourl.org/rss/module/","latitude")[0].firstChild.data;
}
var point = new GPoint(long2,lat2);
var marker = createMarker(point, "<b>"+title+"<\/b><p><a href="\""+link+"\">View Blog<\/a><\/p>");
map.addOverlay(marker);
}
}
}
request.send(null);
}

function createMarker(point, html)
{
var marker = new GMarker(point);
GEvent.addListener(marker, "click", function()
{
marker.openInfoWindowHtml(html);
}
);
return marker;
}
//]]>
</script>
<title></title>
</head>
<body onload="onLoad()">
<div id="map" style="width: 500px; height: 500px"></div>
</body>
</html>