The author

XForms: an Unusual Worked Example

Steven Pemberton, CWI, Amsterdam

XML

"You know, the people who invented XML were a bunch of publishing technology geeks, and we really thought we were doing the smart document format for the future. Little did we know that it was going to be used for syndicated news feeds and purchase orders." - Tim Bray, 2005

An Unusual Example

I am going to work through an extensive example.

It is coherent (it really does something)

It uses most parts of XForms

It is amazingly fun!

Output

In XForms you can put the URL of an image in your data:

<instance>
   <data xmlns="">
      <url>http://tile.openstreetmap.org/10/511/340.png</url>
   </data>
</instance>

and output it with

<output ref="url"/>

This would give as output:

http://tile.openstreetmap.org/10/511/340.png

Output an image

But if you add a mediatype to the <output>, the image itself is output instead:

<output ref="url" mediatype="image/*" />

London

URL Structure

An Open Street Map URL is made up as: http://<site>/<zoom>/<x>/<y>.png

So we can represent that in XForms data:

<instance>
   <map xmlns="">
      <site>http://tile.openstreetmap.org/</site>
      <zoom>10</zoom>
      <x>511</x>
      <y>340</y>
      <url/>
   </map>
</instance>

and calculate the URL from the parts:

<bind ref="url"
      calculate="concat(../site, ../zoom, '/', ../x, '/', ../y, '.png')"/>

Input

But now that we have the data, we can also input the different parts:

     <input ref="zoom"><label>zoom</label></input>

This means that we can enter different values for the tile coordinates, and because XForms keep all relationships up-to-date, a new tile URL is calculated and the corresponding tile is displayed.

Live Tile

Source

Nudge buttons

However, since entering numbers like this is inconvenient, we can also add some nudge buttons, of the form:

<trigger>
   <label>→</label>
   <setvalue ev:event="DOMActivate" ref="x" value=". + 1"/>
</trigger>

Live tile with nudge buttons

Source

Zoom

At each level of zoom the x and y coordinates change

So we must save our location in world coordinates, each value between 0 and 226 (18 levels of zoom, plus 8 bits for the 256 pixels of each tile), and then calculate the tile from that:

scale=226 - zoom
x=floor(posx/scale)
y=floor(posy/scale)

In XForms:

<bind ref="scale" calculate="power(2, 26 - ../zoom)"/>
<bind ref="x" calculate="floor(../posx div ../scale)"/>
<bind ref="y" calculate="floor(../posy div ../scale)"/>

Live tile with working zoom

Source

Location

You may have noticed that as you zoom you get the tile that contains the location, but the location jumps around the tile.

This is because if you have a tile where the location is in the middle of the tile, when you zoom in, you get one of the 4 quadrants, and so by definition, the location is no longer at the centre of the tile:

Zooming

Keeping it central

We create a 3×3 array of tiles, with a porthole over it.

The porthole stays static, and we shift the tiles around underneath so that our location remains in the centre.

This we do by calculating offsets that the tile array has to be shifted by, and then using these to construct a snippet of CSS to move the tile array:

Array of tiles under a porthole

<bind ref="offx" 
   calculate="0 - floor(((../posx - ../x * ../scale) div ../scale)*../tilesize)" />
<bind ref="offy" 
   calculate="0 - floor(((../posy - ../y * ../scale) div ../scale)*../tilesize)" />
 ...
<div style="margin-left: {offx}; margin-top: {offy}">

Tiled map

Source

Visible version

To help you understand this better, here is a version with the elided bits made visible:

Visible version

Source

Mouse

Of course, what we really want is to be able to drag the map around with the mouse, not have to click on nudge buttons. Now we're really going to see the power of live data! We will want to know the position of the mouse, and the state of the button, up or down. So we create instance data for that:

<mouse>
   <x/><y/><state/>
</mouse>

and then we catch the mouse events:

<action ev:event="mousemove">
   <setvalue ref="mouse/x" value="event('clientX')"/>
   <setvalue ref="mouse/y" value="event('clientY')"/>
</action>
<action ev:event="mousedown">
   <setvalue ref="mouse/state">down</setvalue>
</action>
<action ev:event="mouseup">
   <setvalue ref="mouse/state">up</setvalue>
</action>

Now we have live data for the mouse!

Mouse live

Source

Mouse state presentation

We want to show the state of the mouse by changing the mouse cursor from a hand into a clenched hand:

<mouse>
    <x/><y/><state/><cursor/>
</mouse>
<bind nodeset="mouse/cursor"   
      calculate="if(../state='up', 'pointer', 'move')"/>

And then style the cursor suitably:

<div style="cursor: {cursor}">... 

Mouse with cursor

Source

Capturing a move

The last bit is that we want is to save the start and end point of a move, so we can calculate how far we have dragged. The instance data is extended:

<mouse>
   <x/><y/><state/>
   <start><x/><y/></start>
   <end><x/><y/></end>
   <move><x/><y/></move>
</mouse>

We capture the start point of the drag when the mouse button goes down:

<action ev:event="mousedown">
   <setvalue ref="mouse/state">down</setvalue>
   <setvalue ref="mouse/start/x" value="event('clientX')"/>
   <setvalue ref="mouse/start/y" value="event('clientY')"/>
</action>

Move

While the mouse button is down, we keep the end position updated:

<bind ref="mouse/end/x"
      calculate="if(mouse/state = 'down', mouse/x, .)"/>
<bind ref="mouse/end/y"
      calculate="if(mouse/state = 'down', mouse/y, .)"/>

And calculate the distance moved as just end - start:

<bind ref="mouse/move/x"
      calculate="mouse/end/x - mouse/start/x"/>
<bind ref="mouse/move/y"
      calculate="mouse/end/y - mouse/start/y"/>

Move

Source

Dragging the map

So now we have the scaffolding we need to be able to drag the map. You may recall that the position of the map is recorded in posx and posy. That position now also depends on the mouse dragging. So we add instance data to record the last position:

<lastx/><lasty/>

and add a calculation to keep posx and posy updated (remember scale is the number of positions represented on a tile, so we divide by the tile size to get the number of positions represented by a pixel):

<bind ref="posx"
      calculate="../lastx - ../mouse/move/x * (../scale div ../tilesize)"/>
<bind ref="posy"
      calculate="../lasty - ../mouse/move/y * (../scale div ../tilesize)"/>

One last thing

Reset lastx and lasty when the dragging stops:

<action ev:event="mouseup">
   <setvalue ref="lastx" value="posx"/>
   <setvalue ref="lasty" value="posy"/>
   <setvalue ref="mouse/start/x" value="mouse/end/x"/>
   <setvalue ref="mouse/start/y" value="mouse/end/y"/>
</action>

Now it is possible to drag the map around. Although from the user's point of view it feels like you are grabbing the map and dragging it around, all that is happening underneath is that we are tracking the live data representing the mouse, and using it to alter the live data that represents the centre of the map.

Draggable map

Source

Bells. Whistles

Once we have this foundation, it is trivial to add things like a "Home" button, to add keystroke shortcuts, to zoom in and out with the mouse wheel, or to select tiles for another version of the map. For instance:

<select1 ref="site">
   <label>Map</label>
   <item>
      <label>Standard</label>
      <value>http://tile.openstreetmap.org/</value>
   </item>
   <item>
      <label>Cycle</label>
      <value>http://tile.opencyclemap.org/cycle/</value>
   </item>
   <item>
      <label>Transport</label>
      <value>http://tile2.opencyclemap.org/transport/</value>
   </item>
   ...
</select1>

Thanks to the live data, any time a different value is selected for "site", all the tiles get updated, without any further work from us.

Map

Source

Conclusion

~150 lines

Not a single while loop

In an abstract sense, a map like the one presented above can be seen as the presentation of two values, an x and y coordinate, overlaid with an input control to affect the values of x and y.

The ability of XForms to abstract the data out of an application and make the data live via simple declarative invariants that keep related values up to date makes the construction of interactive applications extremely simple.

The implementation used here is XSLTForms.

To use it, download the zip, extract the zip somewhere in your website, and then have each XForms file reference it with a stylesheet, for instance as

<?xml-stylesheet href="../xsltforms/xsltforms.xsl" type="text/xsl"?>