Building a Web Mapping Application With Pergola and Polymaps

Introduction

Mapping applications are very popular, and it is easy to embed a simple map on a page using something like the Google Maps API. However, anyone who has tried to create a more complex mapping application will know that it can take a considerable investment in terms of time, and the number of issues can grow exponentially. In this article we are going to show how to build a fully fledged SVG-based, windowed mapping application using the Pergola framework and its libraries, with the Polymaps library plugged in, and tiles imported from Bing. Polymaps was chosen in preference to other mapping libraries because it is modern, compact and lightweight, and has all the features we needed.

Pergola overview

For those who are not familiar with Pergola, it is a JavaScript SVG framework with various libraries for facilitating building web applications. One of the key components of its API is the progressive DOM Helper. By exposing the DOM, as described in July's issue of SVG magazine, it nullifies the need to define classes for SVG elements, whether primitives or others. Pergola libraries are essentially SVG objects (symbols, markers, patterns, shapes, filters, etc.), and a comprehensive collection of classes specialising in the production of system objects, widgets and other utilities. Its general structure consists of a placeholder object defining some system variables and utility functions, a superclass defining prototype methods, and prototype extensions for the individual classes. It defines a consistent OOD model, similar in approach to ext.js. Moreover, this model adopts a shadow DOM referencing model, skipping the need for ad hoc node selection or referencing. It also introduces the use of the instance property owner, which makes up for the lack, in JavaScript 1.5, of a property similar or equivalent to the DOM's parentNode. It allows upstream identification of objects with a higher hierarchical rank and has several other implications in terms of flexibility.

Also noteworthy is the User Functions and Events mechanism implemented in all the low level component classes which, together with the possibility of overriding properties and methods, allows the addition of accessibility extensions by developers with the right know how. They may also find interesting the possibility of defining a global accessibility “skin” in the skin.es file, which, while functioning in a manner similar to a CSS document, allows multiple definitions of the same selectors in the same document, accepts expressions and allows us to use the powerful color utility functions, for example to enhance contrast for the whole interface, etc.

Pergola is 100% compatible with all the major SVG implementations, including IE9, and since version 1.3.5 all online examples have been shown both in a pure SVG context, and in an HTML context (mixed namespace). The switch is simply made in the config file that accompanies each project:

pergola.container = document.getElementById("svg");
pergola.doc = $C({
	element: "svg",
	width: "100%",
	height: "100%",
	appendTo: pergola.container
});

The default container is overridden (you can use any selection method), and the outmost <svg> is created and appended to the newly defined container. If the container does not exist you can create one on the fly:

pergola.container = $html({
	element: "div",
	style: "margin: 40px; overflow: hidden;",
	appendTo: document.body
});

In fact the SVG DOM helper comes with a sibling HTML DOM helper, which exposes the HTML library and its vocabulary in the same way.

Instantiation of the Window class, like any class designed to produce widgets or other concrete objects, is a two step process:

  • Definition of the object and inheritance
  • A call to the class's prototype build() method, a function expecting one object where we can override the prototype properties and define component objects.
  • This technique has multiple advantages over dynamic or deferred construction such as self-referencing and access to the object's own prototype during instantiation.

    In its simplest form, creating a Pergola application window looks like so:

    var myWindow = new pergola.Window();
    myWindow.build({});

    This produces an empty window with toolbar.

    That concludes our overview of the Pergola library. For more information, look at the documentation and online examples:

    Initialising our mapping window

    To start with, we'll wrap a map into a window in a few simple steps, then adding mapping features in a straightforward manner. First we create an instance of the Pergola.Window class to contain the map.

    var bingWin = new pergola.Window("Bing Maps");
    
    bingWin.build({
    	isFull: true,
    	type: "map",
    	mapWidth: 2048,
    	mapHeight: 1536,
    	fill: "#010413",
    	...
    });

    The first few properties we see here are self-explanatory. The width and height of the map should be set to at least window.screen.width and window.screen.height respectively, and they should also be multiples of 512, which is the size of the map tiles we are going to use. type: "map”, will produce a window with different behavior from a regular window: the class's prototype is extended with specific mapping properties and methods, and the behavior of the transformation tools, including scrollbars, overrides the regular behavior by sending requests rather than acting on the contained document's viewport.

    Note that in the object literal we are passing to the build method we can override the prototype properties of the class, thus, if we wanted to implement our own mapping tools instead of those implemented by default, we would assign the value false to the hasZoomAndPan property. We will see further down how to add custom tools.

    The menus

    Next up is adding a menu; Pergola has built-in functionality for menus, dialogs, static panels, check-boxes, radio buttons, pop-up selectors, etc. Adding a menu is as simple as this:

     ...,
    	menu: {
    		...
    	},

    Perhaps one of the most interesting things about maps is switching between different views: satellite shots, road maps, birds eye views, etc. In our map we'll create three views: Aerial, Aerial With Labels and Road. We do this by setting a property with title of Views, which defines the menu title and a list of three views:

    menu: {
    	menu1: {
    		title: "Views",
    		items: {
    			item1: {
    				string: "Aerial",
    				check: false,
    				exclusive: true,
    				view: "Aerial",
    				fn: tileSource
    			},
    			item2: {
    				string: "Aerial With Labels",
    				check: true,
    				exclusive: true,
    				view: "AerialWithLabels",
    				fn: tileSource
    			},
    			item3: {
    				string: "Road",
    				check: false,
    				exclusive: true,
    				view: "Road",
    				fn: tileSource
    			}
    		}
    	}
    }

    The user function tileSource is in charge of switching scripts in order to request the appropriate tiles for each view. It is a top level function closely associated with the request callback function. We will see later that there are several type values for user functions (fn) in Pergola.

    The Polymaps development model doesn't follow a typical Javascript OOD model. In order to facilitate communication with Polymaps we define the views property, designed to store information about Polymaps' tile layers:

    views: {
    	Aerial: {},
    	AerialWithLabels: {},
    	Road: {}
    }

    Populating the map

    Finally, we need to create the map and use it to populate the window. For this we have several methods at our disposal. Typically, appending the map can be done during instantiation or can be deferred. The first method is what we will use, as it is definitely the easiest:

    contains: function() {return this.mapMaker()}

    This is an instance property, defined by the user, but designed for being processed. Some APIs designate this kind of property as “special”. The mapMaker prototype method is a helper function. Its call is deferred until the this keyword is a valid reference. Other techniques are also possible.

    If we run the code at this point (BingWindow_step1.svg) we will see the map in a full screen window (in the browser) with all controls active (tool buttons, mouse, keyboard).

    var bingWin = new pergola.Window("Bing Maps");
    
    bingWin.build({
    	isFull: true,
    	type: "map",
    	mapWidth: 2048,
    	mapHeight: 1536,
    	fill: "#010413",
    	menu: {
    		menu1: {
    			title: "Views",
    			items: {
    				item1: {
    					string: "Aerial",
    					check: false,
    					exclusive: true,
    					view: "Aerial",
    					fn: tileSource
    				},
    				item2: {
    					string: "Aerial With Labels",
    					check: true,
    					exclusive: true,
    					view: "AerialWithLabels",
    					fn: tileSource
    				},
    				item3: {
    					string: "Road",
    					check: false,
    					exclusive: true,
    					view: "Road",
    					fn: tileSource
    				}
    			}
    		}
    	},
    	views: {
    		Aerial: {},
    		AerialWithLabels: {},
    		Road: {}
    	},
    	contains: function() {return this.mapMaker()}
    });

    For the hard core who may need more control, here is the code for appending the map manually, showing the use of private member functions (see the Polymaps documentation for more):

    pergola.Window.current = bingWin;
    var doc = bingWin.childDoc;
    polymaps.origin = {x: bingWin.x + doc.x, y: bingWin.y + doc.y};
    
    bingWin.map = polymaps.map(doc)
    .container(doc.transformable.appendChild($C({
    	element: "svg",
    	id: bingWin.id + "_tiles",
    	width: bingWin.mapWidth,
    	height: bingWin.mapHeight
    })))
    .add(polymaps.interact())
    .add(polymaps.hash());

    Adding additional tools

    Let's proceed now with adding a custom tool in the tool bar. For several of its interface features the Window class allows “inline” (defined in the object literal) or deferred construction/addition, like we've seen for the contains property. The addition of tools can be done while creating the window instance by setting the tools property, or at a later stage by invoking the addTools prototype method. This time let's use the second technique:

    bingWin.addTools({
    	group1: {
    		separator: true,
    		myMeasureTool": {
    			symbol: {
    				symbol: pergola.symbols.ruler,
    				x: 4,
    				y: 4
    			},
    			selected: false,
    			quickTip: "measureTool",
    			ev: "mouseup",
    			fn: function() {...}
    		}
    	}
    });

    The object literal can be passed one or more properties as parameters to designate tool groups that can be physically separated, in which case we set the property separator to true. Then follows one or more tool objects where the user defined properties ev and fn are of particular importance. There are a number of different value types that can be used to reference functions. In this example we use a function literal for the sake of simplicity. The quickTip property can also get different formats including definitions on the fly.

    The ToolButton class is a subclass of Button and when creating an instance we can override all its geometrical and paint properties (SVG attributes), or even declare any ones that are not in the collection of the prototype properties of the Button class. But the main purpose of the ToolButton subclass is to define a particular and consistent look and feel. The best place to edit the graphical properties of the class is in the skin file. If we take a look this is immediately apparent, and in any case exhaustively documented.

    The ToolButton class inherits its system logic from Button. Moreover, tool buttons can behave as radio buttons, but this is outside the scope of this article.

    One bit of good news is that a ruler tool with logic for measuring earth distances is implemented by default in a window of type "map": it can measure distances in kilometres, miles, or nautical miles (menu Unit), as in the live demo. The navigation help tool (“?”) is also part of the base kit of tools for a window of type "map".

    Adding custom map features

    We'll finish up by adding some custom map features — a custom menu and menu items to reveal interesting locations and data.

    Before we implement though, a little theory. Implementing a custom feature on a map involves a custom graphic, either scaled according to the map zoom level or not scaled. Paths showing itineraries or polygons for delimiting areas for example are meant to be scaled, while objects used to pinpoint a particular place or to show information about a particular region are just projected. Either way, we can handle these features using GeoJSON Objects. In our case, these objects can be processed by Polymaps' GeoJSON parser, enhanced in this custom version with some Pergola facilities.

    A custom feature is commonly placed in its own layer, overlaid on top of the map. We are going to add two layers, one that shows the Bing copyright, which is a Bing requirement, and one to pinpoint the location of the “SVG Open 2011” conference. For this, we define:

    • the instance property layers, an object where we store information about the layers for their management.
    • some controls to toggle our custom features on and off: custom menus/menu items in our case.

    Before proceeding, let's remember that these are just definitions, and the order in which the properties of the window are defined in the object literal passed as parameter to the window's buid method is irrelevant.

    The layers object

    The order in which the properties of this object are defined is also irrelevant.

    layers: {
    	copyright: {
    		feature: false,
    		display: "block"
    	},
    	svgOpen2011: {
    		feature: true,
    		display: "none"
    	}
    }

    The feature property indicates if the layer contains projection (true) or static material. The display property indicates the initial state we want for that layer.

    The “Layers” menu

    We add the definition of our “Layers” menu, containing custom items and their user functions, to the menu object (a window has exactly 1 menu object):

    menu: {
    	menu1: {
    		...
    	}
    	menu2: {
    		title: "Layers",
    		items: {
    			item1: {
    				string: "SVG Open 2011",
    				target: function() {
    					return {
    						obj: bingWin.layers.svgOpen2011,
    						center: {lat: 42.36131, lon: -71.08124},
    						zoom: 17,
    						view: "Road"
    					};
    				},
    				fn: 'showFeatureLayer',
    			},
    			item2: {
    				string: "Copyright",
    				check: true,
    				target: function() {return bingWin.childDoc.copyright;},
    				fn: function() {
    					var l = bingWin.layers.copyright;
    					l.display = l.display == "block" ? "none" : "block";
    					this.target().setAttributeNS(null, "display", l.display);
    				}
    			}
    		}
    	}
    }

    We have defined the “SVG Open 2011” feature directly in the object literal. We specify a latitude and a longitude as the new map center, an appropriate value for the zoom level (in the 1-21 range) and optionally, a view type that we estimate most appropriate.

    The user function of the "SVG Open 2011" menu item updates the Views menu, builds the feature layer if it doesn't exist, and centers and zooms the map. This is where the GeoJSON Geometry Object is defined:

    bingWin.menu.menu2.list.item1.showFeatureLayer = function(evt) {
    	var target = this.target(),
    			o = target.obj,
    			currentMap = pergola.Window.currentMap;
    
    	if (target.view) {
    		currentMap.mapViewsToggle(currentMap.menu.menu1.list.item3, target.view);
    	}
    
    	if (!o.container) {
    		currentMap.map.add(polymaps.geoJson(o)
    		.features([
    			{
    				"geometry": {
    					"type": "Point",
    					"coordinates": [-71.08124, 42.36131],
    					"elements": pergola.symbols.signalPaddle
    				}
    			}
    		]));
    	}
    
    	currentMap.centerMap(target.center);
    	currentMap.mapZoom(target.zoom);
    	currentMap.showMapFeatureLayer(o);
    };

    Note that the GeoJSON Geometry Object Point refers to a point on earth using coordinates. The elements property gets an array of one or more SVG elements whose origin 0,0 will be placed at coordinates. The SVG elements are in the format specified in the Pergola documentation and consistent throughout its libraries and classes. It is the format expected by Pergola's progressive DOM Helper, a method which builds and returns any SVG element. In the object expected by the method we can define any of the element's attributes using SVG syntax, without the need of any extra artifact or appendices. The method is extensively used in this custom version of Polymaps.

    The contents of the “Copyright” layer get built into the Bing callback function.

    The complete code

    We can check the BingWindow_step2.svg example now with the added layer and layers menu.
    var bingWin = new pergola.Window("Bing Maps");
    
    bingWin.build({
    	isFull: true,
    	type: "map",
    	mapWidth: 2048,
    	mapHeight: 1536,
    	fill: "#010413",
    
    	menu: {
    		menu1: {
    			title: "Views",
    			items: {
    				item1: {
    					string: "Aerial",
    					check: false,
    					exclusive: true,
    					view: "Aerial",
    					fn: tileSource
    				},
    				item2: {
    					string: "Aerial With Labels",
    					check: true,
    					exclusive: true,
    					view: "AerialWithLabels",
    					fn: tileSource
    				},
    				item3: {
    					string: "Road",
    					check: false,
    					exclusive: true,
    					view: "Road",
    					fn: tileSource
    				}
    			}
    		},
    		menu2: {
    			title: "Layers",
    			items: {
    				item1: {
    					string: "SVG Open 2011",
    					target: function() {
    						return {
    							obj: bingWin.layers.svgOpen2011,
    							center: {lat: 42.36131, lon: -71.08124},
    							zoom: 17,
    							view: "Road"
    						};
    					},
    					fn: 'showFeatureLayer'
    				},
    				item2: {
    					string: "Copyright",
    					check: true,
    					target: function() {return bingWin.childDoc.copyright;},
    					fn: function() {
    						var l = bingWin.layers.copyright;
    						l.display = l.display == "block" ? "none" : "block";
    						this.target().setAttributeNS(null, "display", l.display);
    					}
    				}
    			}
    		}
    	},
    
    	views: {
    		Aerial: {},
    		AerialWithLabels: {},
    		Road: {}
    	},
    
    	layers: {
    		copyright: {
    			feature: false,
    			display: "block"
    		},
    		svgOpen2011: {
    			feature: true,
    			display: "none"
    		}
    	},
    
    	contains: function() {return this.mapMaker()}
    
    });

    Conclusion

    Multiple polymaps.map instances are theoretically possible save for the fact that at this stage they end up being replicas of one another (work in progress). In this respect we must take note that the Polymaps library used here is not the official Polymaps release: it has been adapted to work in a standalone SVG context for specific purposes.

    That's all for now. It's nice to be able to create a full-featured SVG application this easily. A complete customisation of the look and feel of the interface objects can be done through the skin file. And you can check out the live demo of the example shown in this article.

    Note: One thing you'll notice about this application is that each time you move the map, a new URL is formed. This would quickly become really annoying for those wanting to use the back button to go back to a previous page. Therefore, in a production environment, the application would be best launched in a separate window or tab, with an explanation telling the user this is happening and why.