Articles / Need to have maps on your W…

Need to have maps on your Web page? It's simple!

This article will show you how easy it is to set up interactive maps for your Web page, including multiple layers and interactivity between your view of your Web page and your backend business logic.

When you reach the point that it would be nice to have a map on a Web page, you always have a choice of what to use and what to expect from the API. Also, people may have a need for backend logic for their maps. Let's look at some common implementations and their ups and downs.

Common problems when creating a GIS application

  • What presentation tier to choose: JavaScript, Flash, applets, ActiveX
  • Initialization of parameters for maps
  • Communication between presentation tier and backend logic
  • How to make it fast and reliable

Presentation

Of course you would like to implement your maps in the best way possible. Using rich UI clients like applets, Flash, or ActiveX will give you pretty much all you need, but once you implement your maps in this way, people will have to install these components as add-ons to their Web browsers. This could be inconvenient for those people who don't have permission to install extra components on their computers or don't want to install them. I believe JavaScript maps are a more reasonable choice. Once a user goes to your Web page, he/she sees maps immediately, and no other actions or installation are required. I think Google Maps has proved this approach.

Initializing parameters

When you want to render a JavaScript map, you are supposed to provide some initialization parameters that set up the basic look and feel of the map. One way is to have some beans and get the values from them when the page is being rendered. But if you tend to use maps in different pages, you need to copy and paste some extra JavaScript and provide some beans for the new page. This approach does not reuse existing components as a good application should.

What we need is to use a map template and reuse this template across different pages of the application. The map template should be between the Web page rendering and the JavaScript. Using template technologies such as Apache Velocity helps to solve the problem of reusing JavaScript components.

Communication

There is only one answer for JavaScript: Ajax.

Speed and Reliability

Maps consist of a base layer and extra layers. The base layers are supposed to be static, and it's a good practice to have them pre-rendered and saved as a set of tiles somewhere. The extra layers usually have some specific information (e.g., school boundaries).

Let's start implementing maps.

Making Maps with OpenLayers

I have chosen the OpenLayers framework since its easy and MVC-based. Everything in OpenLayers is based on events, and all you need to do is subscribe to events. I'm very excited about what the people at OpenLayers have done because of its simplicity and convenience.

First, we need to know what the GIS WMS Server is. WMS (Web Map Service) is an open standard for rendering maps. Most map servers support WMS. In particular, WMS defines:

  • How to request and provide a map as an image (GetMap).
  • How to get and provide information about the content of a map such as the value of a feature at a location (GetFeatureInfo).
  • How to get and provide information about what types of maps a server can deliver (GetCapabilities).

OpenLayers has the WMS Layer as a primary layer for maps. You can find a lot of free WMS servers on the Internet. Here is an example from the OpenLayers examples directory:

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <style type="text/css">
        #map {
            width: 100%;
            height: 100%;
            border: 1px solid black;
        }
    </style>
    <script src="OpenLayers.js"></script>
    <script type="text/JavaScript">
        <!--
        var lon = 5;
        var lat = 40;
        var zoom = 5;
        var map, layer;

        function init(){
            map = new OpenLayers.Map( 'map' );
            layer = new OpenLayers.Layer.WMS.Untiled( "OpenLayers WMS", 
                    "http://labs.metacarta.com/wms/vmap0", {layers: 'basic'} );
            map.addLayer(layer);

            map.setCenter(new OpenLayers.LonLat(lon, lat), zoom);
        }
        
        // -->
    </script>
    </head>
    <body onload="init()">
    <div id="map"></div>
    </body>
</html>

Example: http://shpuroff.com/maps/wms.html

In this example, we use the http://labs.metacarta.com/wms/vmap0 WMS Server. The URL http://labs.metacarta.com/wms/vmap0?REQUEST=GetCapabilities&Service=WMS will tell us all the information about this map, including maximum extent and layers.

It is difficult to reuse this script, since we have the functions only, and map logic is not encapsulated in a class. However, since WMS does map rendering on request, it could be slow, especially when you have a complicated map. Moreover, you need to have a WMS server.

One option to avoid using a WMS server is to have a pre-rendered set of images of the map, called tiles. These tiles could be rendered for one or multiple layers, and they are just pictures using standard formats like GIF, PNG, or JPEG. Since no server API is involved in map rendering, you will achieve maximum performance from your maps.

I have prepared some tiles for the base layer using the ArcGis cache. Disk space is now cheap, and you can pre-render huge maps in different zoom levels to speed up the map.

Here is the code of a basic map:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<style type="text/css">
</style>

<script src="OpenLayers.js"></script>
<script src="AGS.js"></script>
<script type="text/JavaScript">
        <!--
	// create a class for future reuse
	MyGisRender = OpenLayers.Class.create();
	MyGisRender.prototype= { 
  		lon: -107.0612,
        lat:  38.9435, 
        zoom: 0,
        map: null,
        baseLayer: null,

  	   initialize: function() {
	   },

       init: function(){
            this.map = this.createMap();
            this.baseLayer = this.createBaseLayer();
            this.map.addLayer(this.baseLayer);
            this.addControls();
        },
        
        createMap: function () {
        return new OpenLayers.Map( 'map', {
								maxExtent: new OpenLayers.Bounds(-205.075,10.14911608,-49.1132 , 88.112),
								maxResolution: 0.15228550153247, 
								resolutions: new Array( 
								0.15228550153247, 
								7.61427507662349E-02, 
								3.80713753831174E-02, 
								1.90356876915587E-02,
								9.51784384577936E-03
								), tileSize: new OpenLayers.Size(256,256), 
								tileOrigin: new OpenLayers.LonLat(-400,400), 
								units: 'degree', controls: []} );
        },
        
        createBaseLayer: function () {
         return new OpenLayers.Layer.AGS( "AGS", 
                    "http://arcgisserver/arcgiscache/phis/Layers", {layername: '_alllayers', type:'png', 
                    tileOrigin: new OpenLayers.LonLat(-400,400)
                    } );
        },
        
        addControls: function () {
        	this.map.addControl(new OpenLayers.Control.PanZoomBar());
            this.map.addControl(new OpenLayers.Control.Navigation());
            this.map.addControl(new OpenLayers.Control.MousePosition());
            this.map.setCenter (new OpenLayers.LonLat(this.lon, this.lat), this.zoom);
        },
        
        CLASS_NAME: "MyGisRender" 
       }
       
       // global init 
       function init() {
        var myMap = new MyGisRender();
        myMap.init();
       }
        // -->
    </script>
</head>
<body onload="init()">
An example showing OpenLayers using the MapCache of ArcGIS server 9.2
<div id="map" style="width:600px;height:500px;border: 1px solid black;"></div>
</body>
</html>
Example: http://shpuroff.com/maps/arcGis.html

As you see, I have a class here which controls the map behavior. Using the class instead of set of functions will allow us to reuse it, especially when we need to change its behavior. For example:

MyGisRender1 = OpenLayers.Class.create();
       MyGisRender1.prototype =  
        OpenLayers.Class.inherit(MyGisRender, {
       
         addControls: function () {
          // call inherited method
          MyGisRender.prototype.addControls.apply(this);
          // do our stuff
          this.map.addControl(new OpenLayers.Control.MouseToolbar());
    
         }, 
         
       	CLASS_NAME: "MyGisRender1"
       });

Example: http://shpuroff.com/maps/arcGis1.html

There we added some new control to the map

The big disadvantage of these examples is hard-coded parameters like zoom, extent, etc. As I mentioned before, we can use Java beans exposed to the page and get values from them. If the maps exist in different Web pages, we can use the Spring Framework to send the proper bean there. But what if you need some simple logic when setting up beans? For example, we need to add some points to the map:

points.push(new OpenLayers.Geometry.Point(-72,42));
points.push(new OpenLayers.Geometry.Point(-71,42));
points.push(new OpenLayers.Geometry.Point(-73,42));
points.push(new OpenLayers.Geometry.Point(-70,42));
points.push(new OpenLayers.Geometry.Point(-71,41));

We need to use a template approach like JSP tags or Apache Velocity. Velocity is better, since everything will be in the same template, while JSP tags would be in a JSP page. Let's have a look:

#foreach( $p in $shape.points )
  points.push(new OpenLayers.Geometry.Point($p.lat,$p.lon));
#end

Here is an example of velocity templates for maps:

http://shpuroff.com/maps/mapVector.vm

Extra layers

The map could contain additional layers. We have the OGS WMS specification for this, and if you need something standard such as a satellite layer, you can plug it in very easy. But as I said before, most of the Internet WMS services are slow. OpenLayers supports WMS, but only in tile-based mode. Usually, about 20-40 tiles are being requesting from a WMS service simultaneously, and this causes performance issues. Here, I have added a new OpenLayers WMS layer class which has one image at a time:

createMap: function () {
        return new OpenLayers.Map( 'map', {
			 maxExtent: new OpenLayers.Bounds(-205.075,10.14911608,-49.1132 , 88.112),
			 maxResolution: 0.15228550153247, 
			 units: 'degree', controls: []} );
        },
        
        createBaseLayer: function () {
          return new CRISatalliteImage( "si1", 
           "http://terraserver-usa.com/ogcmap6.ashx?version=1.1.1&request=GetMap&Layers=DOQ&Styles=&SRS=EPSG:4326&format=image/jpeg&Exceptions=se_xml");
        },
Example: http://shpuroff.com/maps/arcGis2.html

This layer has a URL to hit the server. Depending on user interaction, this URL can be changed in order to have the image you need according to WMS request specifications. The user can use a URL and point it anywhere in order to have a custom layer.

Map Features Selection

Selection of some feature on the map can be done by using OpenLayers vector drawing primitives or by creating an additional selection layer. For example:

http://shpuroff.com/maps/Selection.html

In this example, we inherit our base map to reuse all the basic properties, then define a new event called [sateselected]. In the method addControls, we create a new vector layer (the layer which will allow us to handle vector primitives like rectangles, circles, and polygons), then we subscribe to the event mouse click [click]. After the mouse is pressed on the map, the method selectState will be called. In this method, I use the constant value STATES_ENV. This constant has the rectangle extent for the states and their basic information. When a point is selected within a state, a new rectangle is created, and it is added to the map. Also, we trigger the event [sateselected], then the user can register that event somewhere and handle it.

Let's have a look at the vector selection first:

       MyGisRenderSelection = OpenLayers.Class.create();
       MyGisRenderSelection.prototype =  
        OpenLayers.Class.inherit(MyGisRender, {
        
        // define vector layer 
        vlayer: null, 
        
        // add our event types
        EVENT_TYPES: [ 'sateselected' ],
		events: null,
		
		 // constructor
		 initialize: function() {
		 // call MyGisRender constructor 
		 MyGisRender.prototype.initialize.apply(this);
		 
		 // init our events 
		  this.events = new OpenLayers.Events(this, this.div, this.EVENT_TYPES);
	     },
       
         addControls: function () {
          // call inherited method
          MyGisRender.prototype.addControls.apply(this);
          // do our stuff
          this.map.addControl(new OpenLayers.Control.MouseToolbar());
    
    	  // create vector layer and add it to the map 
    	  this.vlayer = new OpenLayers.Layer.Vector("Editable");
    	  this.map.addLayer(this.vlayer);
    	  
              this.map.events.register("click", this, this.selectState);
    
         }, 
         
         selectState: function(evt) {
                var latlon = this.map.getLonLatFromViewPortPx(evt.xy);
                for (var i=0;i<STATES_ENV.length;i++) {
                 if (STATES_ENV[i].extent.containsLonLat(latlon,true)) {
                 
					var env = STATES_ENV[i].extent;
					
					// create geometry (rectangle)
	                var points=[
	                new OpenLayers.Geometry.Point(env.left,env.top),
	                new OpenLayers.Geometry.Point(env.left,env.bottom),
	                new OpenLayers.Geometry.Point(env.right,env.bottom),
	                new OpenLayers.Geometry.Point(env.right,env.top),
	                new OpenLayers.Geometry.Point(env.left,env.top)
	                ];
	                
	                //add it to the vector layer
	                var ring = new OpenLayers.Geometry.LinearRing(points);
	                this.vlayer.addFeatures(new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Polygon([ring])));
					
					// trigger our event	                
	                this.events.triggerEvent("sateselected",STATES_ENV[i]);
                    break;
                  }
                }
         },
         
       	CLASS_NAME: "MyGisRenderSelection"
       });
       

       // global init 
       function init() {
        var myMap = new MyGisRenderSelection();
        myMap.init();
        
        myMap.events.register("sateselected", this, function (selection) {
         alert ('You have selected '+ selection.state);
         });
       }

I used a constant to define all the states, but in the real application, it should be an Ajax call. A rectangle was used for this demonstration, but a polygon should be used to conform with the states shape better.

OpenLayers can add a marker to the map and show some HTML in the message window, as Google does. You can find samples in the examples directory.

Vector selection looks fast, but if you have complex shapes to be selected, it will be slow because a lot of points will have to be displayed by your Web browser. In that case, I would recommend using server-side image rendering and displaying the selection as an additional image layer.

The image selection layer has to have a transparent background. For this purpose, I have implemented the class CRIOneTileLayer, which you can see in the war file. It has a URL as an input where your selected features should be coded, and it adds the BOX parameter in order to get the proper extent, then sends the request to the server. Server-side implementation in this case should be done by a developer.

Vector map drawing and sending shapes back to the server

The OpenLayers framework has controls which allow you to draw different kinds of vector shapes. For example:

http://shpuroff.com/maps/Vector.html

Here, we are creating a vector layer as in the previous example and adding the new control EditingToolbar. This control allows us to draw shapes on the map, but then we have a submit button, and we'd like to see the data on the server side. I wrote the function mapFormPrepare, and it allows you to modify the DOM model of HTML forms. Basically, it adds some parameters, and these parameters will be sent to the server upon submission.

       MyGisRenderVector = OpenLayers.Class.create();
       MyGisRenderVector.prototype =  
        OpenLayers.Class.inherit(MyGisRender, {
        
        // define vector layer 
         vlayer: null, 
       
         addControls: function () {
          // call inherited method
          MyGisRender.prototype.addControls.apply(this);
          // do our stuff
          this.map.addControl(new OpenLayers.Control.MouseToolbar());
    	  // add vector layer
    	  this.vlayer = new OpenLayers.Layer.Vector("Editable");
          this.map.addLayer(this.vlayer);
          
          // add editing toolbar  
          this.map.addControl(new OpenLayers.Control.EditingToolbar(this.vlayer));
    
         }, 
         
         	mapFormPrepare: function (mapFormName) {
		    var features = this.vlayer.features;
		    var form = document.forms[mapFormName];
		    var i = 0;
		        
		    if (features != null && features.length>0) {
		     for (i=0;i<features.length;i++) {
		        var geometry = features[i].geometry;
		        var str = null;
		        if (geometry.CLASS_NAME == "OpenLayers.Geometry.Point") {
		            str = "POINT:"+geometry.x+","+geometry.y
		         }
		        if (geometry.CLASS_NAME == "OpenLayers.Geometry.Circle") {
		            str ="CIRCLE:"+geometry.x+","+geometry.y+","+geometry.radius;
		         }
		         if (geometry.CLASS_NAME == "OpenLayers.Geometry.Polygon") {
		            str ="POLYGON:"+this.componentGeometry2String (geometry.components[0].components) 
		         }
		         if (geometry.CLASS_NAME == "OpenLayers.Geometry.LineString") {
		            str ="LINE:"+this.componentGeometry2String (geometry.components)
		         }
		         if (str != null ) {
					    var input = document.createElement("input");
		  				input.id = "MAP_FEATURE"+i;
		  				input.name = "MAP_FEATURE"+i;
		  				input.value=str;
		  				input.type = "hidden"; //Type of field - can be any valid input type like text,file,checkbox etc.
		  				form.appendChild(input);
		         }
		
		      }
		    }
		    
			var latlon 	= this.map.getCenter();
			var zoom 	= this.map.getZoom();
			
		 	var inputForm = document.createElement("input");
		 	inputForm.id =   "MAP_FEATURE"+i;
		 	inputForm.name = "MAP_FEATURE"+i;
		 	inputForm.value= "MAP:"+latlon.lat+","+latlon.lon+","+zoom;
			inputForm.type 	= "hidden"; 
			form.appendChild(inputForm);
		 
		    inputForm = document.createElement("input");
			inputForm.id 	= "MAP_FEATURE_POST";
			inputForm.name 	= "MAP_FEATURE_POST";
			inputForm.value	= "true";
			inputForm.type 	= "hidden"; 
			form.appendChild(inputForm);
			
	
		},
         
       	CLASS_NAME: "MyGisRenderVector"
       });

You can the read them from the request, for example:


public class MapDataManager {
	 private static String MAP_FEATURE = "MAP_FEATURE";

	 public static List<GeometryObject> parseFromRequest(javax.servlet.ServletRequest request) {
	
		int featureId =0;
		String value = null;
		List<GeometryObject> list = new ArrayList<GeometryObject>();  
		
		while ( (value  = request.getParameter(MAP_FEATURE+featureId))!= null ) {
			GeometryObject obj = ParsingFactory.createGeometryObject(value);
			if (obj != null ) {
				list.add(obj);
			}
			featureId++;		
			}
		
		return list;
	}
}

You will find the completed class in the war file.

Maps for Enterprise application

It's time to have a look at how we can plug OpenLayers into our Web application.

First, we need to define all the configuration parameters, like the maximum and default extents, zoom levels, and tiles or WMS server locations. Let's use the Spring framework for that, and define extents for a USA map and some parameters for tile handling:

        <bean id="extent" class="org.maps.Extent">
		<property name="maxY" value="88.112" />
		<property name="minY" value="10.14911608" />
		<property name="maxX" value="-49.1132" />
		<property name="minX" value="-205.075" />
	</bean>

	<bean id="defpoint" class="org.maps.Point">
		<property name="lat" value="38.9435" />
		<property name="lon" value="-107.0612" />
	</bean>

	<bean id="origin" class="org.maps.Point">
		<property name="lat" value="400" />
		<property name="lon" value="-400" />
	</bean>

	<bean id="tileSize" class="org.maps.Size">
		<property name="w" value="256" />
		<property name="h" value="256" />
	</bean>

	<bean id="map" class="org.maps.BaseMap">
		<property name="extent" ref="extent" />
		<property name="defpoint" ref="defpoint" />
		<property name="origin" ref="origin" />
		<property name="tileSize" ref="tileSize" />
		<property name="baseLayerName" value="data" />
		<property name="baseLayerUrl" value="maps" />
		<property name="maxResolution" value="0.15228550153247" />
		<property name="resolutionList">
			<list>
				<value>0.15228550153247</value>
				<value>7.61427507662349E-02</value>
				<value>3.80713753831174E-02</value>
				<value>1.90356876915587E-02</value>
				<value>9.51784384577936E-03</value>
			</list>
		</property>

	</bean>

Next, we need to specify a bean which will render maps, and we need to provide velocity configuration:


	<bean id="mapRender" class="org.maps.MapRender">
		<property name="baseMap" ref="map" />
		<property name="velocityEngine" ref="velocityEngine" />
	</bean>


	<bean id="velocityEngine" class="org.springframework.ui.velocity.VelocityEngineFactoryBean">
		<property name="resourceLoaderPath">
			<value>/</value>
		</property>
	</bean>

Finally, we will use all of the above:

	<bean id="vectorMap" class="org.maps.VectorMap">
		<property name="mapRender" ref="mapRender" />
		<property name="mapTemplate"
			value="/WEB-INF/map-templates/mapVector.vm" />
	</bean>

Basic configuration parameters like extent, zoom, or map are just POJO beans with getters and setters for properties. The bean mapRender has two POJO properties: velocityEngine and baseMap. Most importantly it renders the map script and binds spring beans to velocity beans by calling the setGisMapping method:

public String getRenderedMap(HttpServlet servlet, String templateName,
			MapRenderExtra mapRenderExtra) throws Exception {
		String response = "";

		VelocityContext vc = new VelocityContext();
		Template t = getVelocityEngine().getTemplate(templateName);
		StringWriter sw = new StringWriter();
		setGisMapping(vc, servlet);

		if (mapRenderExtra != null) {
			mapRenderExtra.setExtraVelocityParameters(vc);
		}

		t.merge(vc, sw);
		response = sw.toString();

		return response;
	}

	protected void setGisMapping(VelocityContext vc, HttpServlet servlet) {

		vc.put("ctxPath", servlet.getServletContext().getContextPath());
		vc.put("map", getBaseMap());

	}

As you see, it binds the baseMap bean and ctxPath to the velocity context in order to use them as variables in velocity templates. Note that it has the parameter mapRenderExtra. This is just an interface allowing classes calling this method to provide additional implementation where you may specify extra velocity bindings. This will allow us to use a velocity template as a base template and then add as many templates as you want. The interface has only one method:

public interface MapRenderExtra {

	void setExtraVelocityParameters(VelocityContext vc);
}

This is the foundation of our maps. Now we will create a controller:

public class VectorMapController implements MapRenderExtra {

	private HttpServlet servlet;
	List<GeometryObject> shapes;

	private static String MAP_FEATURE = "MAP_FEATURE";

	public String getMapScript(HttpServlet servlet) {
		this.servlet = servlet;

		ApplicationContext springContext = (ApplicationContext) servlet
				.getServletContext()
				.getAttribute(
						WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);

		VectorMap bean = (VectorMap) springContext.getBean("vectorMap");
		String templateName = bean.getMapTemplate();
		String scriptString = null;
		try {
			scriptString = bean.getMapRender().getRenderedMap(servlet,
					templateName, this);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return scriptString;
	}

	public void parseFromRequest(javax.servlet.ServletRequest request) {

		int featureId = 0;
		String value = null;
		List<GeometryObject> list = new ArrayList<GeometryObject>();

		while ((value = request.getParameter(MAP_FEATURE + featureId)) != null) {
			GeometryObject obj = ParsingFactory.createGeometryObject(value);
			if (obj != null) {
				list.add(obj);
			}
			featureId++;
		}

		shapes = list;
	}

	@Override
	public void setExtraVelocityParameters(VelocityContext vc) {
		vc.put("vectorShapeData", shapes);

	}
}

Note that we implement the MapRenderExtra interface because we are going to customize our map a little bit:

	public void setExtraVelocityParameters(VelocityContext vc) {
		vc.put("vectorShapeData", shapes);

	}

And here we notify the MapRender class that the previous method should be visited:
VectorMap bean = (VectorMap) springContext.getBean("vectorMap");
		String templateName = bean.getMapTemplate();
		String scriptString = null;
			scriptString = bean.getMapRender().getRenderedMap(servlet,
					templateName, this);

We are sending "this" since we have implemented MapRenderExtra in this controller. We need to add an extra velocity context variable since we are trying to send Geometry objects to the map. This geometry object is restored from the HTTP Request by calling the parseFromRequest method.

Then our JSP page will look like:

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
	pageEncoding="ISO-8859-1"%>
<%@ page import="org.maps.VectorMapController"%>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<%
	VectorMapController 





Recent comments

24 Dec 2008 11:57 Avatar reynaldosheppard

nice
well its interesting and sounds good! gonna try it now.
regards

Screenshot

Project Spotlight

Kigo Video Converter Ultimate for Mac

A tool for converting and editing videos.

Screenshot

Project Spotlight

Kid3

An efficient tagger for MP3, Ogg/Vorbis, and FLAC files.