Tuesday, May 03, 2005

XForms Patterns: Dynamic URLs and Google Maps



Update: A newer blog takes the following topic much further, allowing markers to be overlaid onto the map. Please see XForms Map Viewer.

XForms Map Viewer
Originally uploaded by Mark Birbeck.



So here's the thing; what we're really concerned with is finding patterns in programming that we can encapsulate in mark-up, and then re-use in many different applications. But of course, what we're also concerned with is that jaw-dropping moment when we look at a piece of software, and say, "how the...did they do that"?

For some, Google Suggest was just such an application, and we looked at some of its features in my previous blog. We saw how a great deal of its functionality could be captured in XForms, and in particular how the two main features -- receiving data from a server without losing the UI, and periodically requesting data from the server as the user enters some input -- could be implemented completely by any author using just a few pieces of declarative mark-up.

But whilst Google Suggest is pretty impressive, the application from the Google stable that has been most responsible for getting people thinking about web applications, is Google Maps. With its ability to scroll different parts of a very large map into view -- and then to overlay markers for shops and restaurants, and the routes to get to them -- Google Maps is an incredibly impressive display of what is possible with script in today's browsers.

What am I getting at? Well, simply this; that although we're searching out real-world patterns that we can make reusable, I know that the question that's lurking -- what people really want to know -- is whether XForms can do 'eye candy'. So we might as well leap right in and take a look at Google Maps.

However, I'm going to do so within the goals that I've outlined in my last two blogs; we'll still be trying to encapsulate functionality so that it can be re-used, since whilst the JavaScript used in Google Maps is impressive, it is highly complex, and would require far more resources than most organisations would be willing to spare.

A Quick Guide to Google Maps

Google Maps is based on a back-drop of an enormous number of pre-rendered GIFs that represent all of the US (the UK has also been added). There are 14 levels of zoom, each level twice the resolution of the previous. This makes things pretty easy to manage; when we start at the lowest zoom level we have a grid that is 6x4, if we zoom in one level we have a grid that is 12x8, and so on.

To get a tile for any position at any zoom level, Google uses URLs of the following format:

http://mt.google.com/mt?v=.3&x=x&y=y&zoom=z

So, you might zoom in on New York, from here (x=75, y=-5 and zoom=8):

Very high-level map of New York

to here (x=150, y=-10 and zoom=7):

Not quite so high-level map of New York ... but still pretty high

and so on, down to this part of Central Park (x=2408, y=-164 and zoom=3):

Lower-level map of New York, focusing on part of Central Park

Note that to retain our position when zooming from the first tile to the second, we had to double the x and y values.

There's a great deal more to Google Maps than this, and we'll probably pick up on other features later on, but for this blog we're concerned with how we might render this map as the user navigates around it.

XForms and Images

Now that we know how to obtain a particular tile, let's look at how XForms renders an image. In many situations you wouldn't bother using XForms to render your images, since you can often simply use the host language. For example, if you are using XHTML for your host language, you can just use the familiar <img> tag. However, a limitation of this tag is that it can only take a static URL -- as the author you would need to know in advance what the URL of the image is going to be. If you don't know what the URL will be until run-time -- as is the case with Google Maps, since there are just too many images -- then the only thing you can do is to write script.

As I've said before, XForms is all about finding patterns, and a simple one like this -- a dynamic URL for images -- is one that XForms will have for breakfast. XForms design philosophy already says that form controls should be rendered based on datatype. For example, if the underlying instance data is boolean, then an xf:input will give us a check-box, if the type is a date we get a calendar widget, and so on. XForms 1.1 extends this and says that URLs can be further qualified by adding a mediatype. A URL could therefore be rendered as an image, video, sound, PDF, Flash, and so on.

An example of this feature in action is provided in the contacts database that comes as part of the Sidewinder Viewer. Since the image associated with a person in the database could be stored anywhere -- on your local machine or on a web-server -- then we obviously need 'dynamic URLs' for our images:


Contacts database viewed through the Sidewinder Viewer Posted by Hello

The syntax for this is pretty straightforward; we use an xf:output as normal, but with an additional attribute -- mediatype. The following XForms mark-up will render the first of the images that we showed above:

<xf:output value="'http://mt.google.com/mt?v=.3&amp;x=75&amp;y=-5&amp;zoom=8'" mediatype="image/*" />

Note that since @value takes an XPath expression we have to indicate that our URL is a literal string -- hence the apostrophes inside the quotes. But the fact that it does take an XPath expression gives us a lot of power, as we shall now see.

Constructing URLs

Given that the Google Maps URL template requires three values, all we need to do is create some instance data that contains three elements -- x, y and zoom, and we can then get any Google Maps tile. The following mark-up shows how this might be done:

<xf:output
value="concat(
'http://mt.google.com/mt?v=.3&amp;x=',
x,
'&amp;y=',
y,
'&amp;zoom=',
zoom
)"
mediatype="image/*"
/>

With this simple mark-up, all the form would need to do is change the values of x, y and zoom in the instance data, and we would immediately get the corresponding tile. And as with anything else in XForms we get the new URL (and hence the new image) immediately, since the creation of a new URL is triggered by changes to any of the dependent values -- once again assisted by our trusty XForms dependency-engine.

A little form that shows three xf:input controls and an xf:output with @mediatype set to image/* (to render the selected tile) is available here. If you have the Sidewinder Viewer installed then you'll see the form, otherwise you'll get the source. A screen-shot of the form in action is here:


Dynamically Generated Image URLs in XForms Posted by Hello

Using xf:repeat for a grid

So we've used a few lines of XForms to manage the URL of one image for us -- all with no script -- but will it handle nine?

To get a backdrop much like the one that Google Maps uses, all we need to do is use a xf:repeat that contains as many xf:output elements as we want tiles.

There are many ways that you could do this, but the solution I opted for was to store a 'position' in some instance data (actually, position plus zoom level), and then to have further instance data with an element for each tile that I wanted (i.e., to get a 3x3 grid I need nine elements). I gave each of these tiles an offset from the 'current position', and then just used XForms to calculate which Google Maps tile I needed for each iteration of the xf:repeat, based on the 'current position'.

That sounds more complicated than it is, so I'll step through it.

First, we have an instance that tracks the user's position and zoom level:

<xf:instance id="inst-control">
<control xmlns="">
<position x="0" y="0" zoom="14" />
</control>
</xf:instance>

Then we have another instance that holds an element for each tile that we want to show:

<xf:instance id="inst-maps">
<maps xmlns="">
<row>
<map x="-1" y="-1" /> <map x="0" y="-1" /> <map x="1" y="-1" />
</row>
<row>
<map x="-1" y="0" /> <map x="0" y="0" /> <map x="1" y="0" />
</row>

<map x="-1" y="1" /> <map x="0" y="1" /> <map x="1" y="1" />
</row>
</maps>
</xf:instance>

As you can see, each map element has an offset from the centre of the grid which we'll use to help us work out which tile we need -- if the user is currently at position (23, 7) then the tile we need for the top right is (23 + 1, 7 - 1) or (24, 6).

But as I say, there are many ways this could be done, so the detail is unimportant. The main thing is that somehow or other you need to generate the URLs for the tiles, and XForms provides us with numerous ways that we could do this.

To finish off the particular approach I have outlined here, I need two xf:repeats; the first is for the rows, and the second for the columns:

<xf:repeat nodeset="instance('inst-maps')/row" id="rpt-row">
<xf:repeat nodeset="map" id="rpt-map">
<xf:output
value="concat(
'http://mt.google.com/mt?v=.3&amp;x=',
instance('inst-control')/position/@x + @x,
'&amp;y=',
instance('inst-control')/position/@y + @y,
'&amp;zoom=',
instance('inst-control')/position/@zoom
)"
mediatype="image/*"
style="width: 128px; height: 128px;"
/>
</xf:repeat>
</xf:repeat>

Note that as with any other xf:repeat these will iterate across the nodeset, regardless of how many nodes are in it. So if you didn't like my 3x3 grid, and wanted 4x4 or 2x2, you can just change the number of row and map elements in the instance data. And note that it would even be possible to set this up as a configuration option for your users, so that they can choose their own grid size.

Putting it all Together

Using this still small number of lines of XForms mark-up, we now have a 3x3 grid of Google Map tiles, and by changing the position or zoom value in the position element in the instance data, we will automatically change all nine images. I say 'automatically' because the XForms dependency-engine will do the whole thing for us -- it will spot that x, y or zoom has changed, recalculate all dependents, and then of course, load the new images. But interestingly, since these are just the same calculations that would be performed by a script version, our XForms version performs just as well.

Now that we have a grid that will faithfully represent our position using Google Maps tiles, all we need to do now is add some way to change the position and zoom level. I have added xf:triggers to do this, as well as some xf:bind statements that will show and hide the xf:triggers as required, when the user hits an edge of the map, or zooms in or out to the limit.

The technique used to do this is pretty standard so I won't go into it here, but I would like to draw attention to one interesting feature that arises from the way that the limits are stored. The limit values are set in the instance data for 'current position', as follows:

<xf:instance id="inst-control">
<control xmlns="">
<position x="0" y="0" zoom="14">
<limits top="-3" bottom="1" left="-4" right="2" zoommax="15" zoommin="-1" />
</position>
</control>
</xf:instance>

This means that if our y position hits -2 then we don't want to show a 'north' button, since we can't go any further north. Similarly, if we zoom in all the way to zero, we don't want to show a 'zoom in' button.

An interesting consequence is that if you wanted to have a map on your site that was limited to a certain area, you could set these 'limit' values accordingly. For example, we could show a section of Central Park and limit our users to navigating only an area around it:

<position x="1204" y="-83" zoom="4">
<limits top="-84" bottom="-82" left="1203" right="1205" zoommax="5" zoommin="-1" />
<position>

Note that we've also started the user at quite a high zoom level, and we've allowed them to zoom in further, but not out. Since the form is still using the same xf:bind rules as before, then on load of the form all buttons, except for 'zoom-in', would be hidden. If the user zooms in, then navigational buttons do become available, but the user is only able to move as far in any direction as they would have been able to do at the initial zoom level.

The form to navigate across the whole of the US is here, and it also contains the settings for the 'Central Park only' example, but they are commented out. The form will load in the Sidewinder Viewer if you have it installed.

If you want to see the map in action, click on the right-hand mouse over the following Flash movie and select 'play' (about 1.32 MB):









Conclusion

Google Maps packs in a lot of features, and I hope to look at more of them in future blogs. However, of fundamental importance is the ability to load an image from a URL that is calculated at run-time, and we've shown how XForms provides such a feature without having to use script. This feature was then used to render Google Maps images in an XForm, and allow a user to navigate the map.


Update: A newer blog takes the following topic much further, allowing markers to be overlaid onto the map. Please see XForms Map Viewer.

XForms Map Viewer
Originally uploaded by Mark Birbeck.




Tags: | | | | | |

1 Comments:

Blogger karl said...

Hi,

I have read the recent posts since Ajax, Xforms and SVG sounds really interesting. I also looked at the xforms player 1.3 and sidewinder demos. Tough there are two points for which I didn't find any answer yet.

1.) How does xforms address the web browser back button problem, lets say you fill in the half of a complex form and the user clicks the browser back button. I guess there are no differences to vanilla web browsers, or are there any?

2.) The second is related to the above one. Does xforms support modal windows? I have worked with hidden scripts and little Ajax in the past, a lot of hacks, but if there is a single item that I am missing then its support for modal windows. It means, give the user a form, and let the user complete the form unless the user clicks a Cancel button or closes the window, and for complex forms it should be possible to open sub forms. Again, I guess xforms does not address this problem, or does it? So, if xforms does run inside a vanilla web browser I don't see any chance that the problem gets solved, unless all web browsers support modal windows. Or am I missing something?

May 06, 2005 7:45 PM  

Post a Comment

Links to this post:

Create a Link

<< Home