January 27, 2008
Alex Shpurov, Sr. Software Developer
Great thanks to Nathan Thomas, Software Developer who helped me in writing this article.

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

This article will show you how easy it is to setup interactive maps for your web page; including multiple layers and interactivity between your view for your web page and your backend business logic.

When you reach a point that it would be nice to have a map at 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. So let's look at some common implementations and their ups and downs.

Common problems when creating GIS application:

1. Presentation:
Of course you would like to implement your maps in the best way possible. Using rich UI clients like applets, flash, 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 browser. This could be inconvenient for those people who don't have permissions to install extra components to their computers or don't want to install them. Thus I'm trying to say that I believe javascript maps is the very reasonable choice. Basically once the user goes to your web page he/she sees maps immediately and no other activities or installation required. I think Google maps have proved this approach.

2. Initializing parameters:
When you want to render the javascript map you are supposed to provide some initialization parameters that setup 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 your tending 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 however does not reuse existing components as a good application should.
So what we need to have is a map template and reuse this template across different pages of the application. The map template should be in-between the web page rendering and the javascript, using template technologies such as Apache Velocity project helps a lot to solve the problem of reusing you javascript components.

3. Communication:
There is only one answer for javascript – Ajax

4. Fast and Reliable
As usual the maps consist of a base layer and extra layers. The base layer is 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.

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 just 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 of the map servers support WMS. In particular WMS defines: 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 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 using 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.
Also 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 of 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 on 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 on 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 may 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 on the map

So as you may see 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 page and get values out of them. If the maps exist in different web pages we can use the Spring Framework to send the proper bean there. But what about 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));

Thus we need to use some template approach like JSP tags Apache Velocity. Velocity is better since everything will be in the same template while JSP tags should be at JSP page; So 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 some velocity template for maps Example: http://shpuroff.com/maps/mapVector.vm

Extra layers

The map could contain some additional layers. We have OGS WMS specification 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 WMS service simultaneously and this is causing some performance issues. Here I have added a new Oplenlayers 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 additional selection layer.

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

In this example we inherit our base map to reuse all the basic properties. Then we define a new event called [sateselected]. In the method addControls we create a new vector layer, the layer 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 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.

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

Vector selection looks nice and fast, however, 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 addition 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, and then it 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

OpenLayers framework has the control allows you to draw different kind of vector shapes. Let's have a look at the example

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 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 DOM model of HTML Form. Basically it adds some parameters and these parameters will be sent to the server upon submit.

       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"
       });
Then you can 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

Ok, 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 maximum and default extent, zoom levels, tiles or WMS server locations. Let's use the Spring framework for that and define extents for USA map and some parameters for tiles 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>

Then 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>

And 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 getter's and setter's for properties. The bean mapRender has two POJO properties: velocityEngine and baseMapp. Most importantly it renders the map script and it 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. Right now we will create 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 out 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 mapController = new VectorMapController();
	mapController.parseFromRequest(request);

	String mapScript = mapController.getMapScript(this);
	out.write(mapScript);
%>
<script type="text/javascript">
<!--
 
       // global init
       var myMap  = null;
        
       function init() {
        myMap = new MyGisRenderVector();
        myMap.init();
       }
 // -->
</script>
</head>
<body onload="init()">
An example showing OpenLayers using String
<div id="map"
	style="width: 600px; height: 500px; border: 1px solid black;"></div>

<form id="mapForm" method="post"
	onsubmit="myMap.mapFormPrepare('mapForm');"><input type="submit"
	name="Submit to Server"></form>
<br>
<a href="Map.jsp">Remove all</a>
</body>
</html>

As you see, we create the MapController class and then try to restore its state by calling the parseFromRequest method, then we render the javascript.

When the user presses submit we call the JS mapFormPrepare method for adding extra request parameters to send geometry content back in order to restore them by parseFromRequest.

Then we look at velocity templates. First we are using very basic map class which will be the same for all the application and it has basic init parameters like server URLs, default extent, etc. Here is the default velocity template:

<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  : $map.defpoint.lon,
        lat  : $map.defpoint.lat, 
        zoom : 1,
        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 () {
        var resArray = new Array();
        #foreach($r in $map.resolutionList)
        	resArray.push($r);
        #end 
        return new OpenLayers.Map( 'map', {
					maxExtent: new OpenLayers.Bounds($map.extent.minX,$map.extent.minY,
																 $map.extent.maxX, $map.extent.maxY),
					maxResolution: $map.maxResolution, 
					resolutions: resArray, 
					tileSize : new OpenLayers.Size($map.tileSize.w,$map.tileSize.h), 
					tileOrigin: new OpenLayers.LonLat($map.origin.lon,$map.origin.lat), 
					units: 'degree', controls: []} );
        },
        
        createBaseLayer: function () {
         return new OpenLayers.Layer.AGS( "AGS", 
                    "$map.baseLayerUrl", {layername: '$map.baseLayerName', type:'png', 
                    tileOrigin: new OpenLayers.LonLat($map.origin.lon,$map.origin.lat)
                    } );
        },
        
        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" 
       }
      
        // -->
    </script>

This template will give use the foundation for all the maps in our application. And then let's have a look at the template we have our VectorMapController for.

#parse("/WEB-INF/map-templates/map.vm")

<script type="text/javascript">
<!--
 MyGisRenderVector = OpenLayers.Class.create();
 MyGisRenderVector.prototype =  OpenLayers.Class.inherit ( MyGisRender, {
 	
 	  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));
          this.addVectorLayer();
    
         }, 
  	
	 
 	 addVectorLayer: function() {
 	  var vlayer = this.vlayer;
 	  var bounds = new OpenLayers.Bounds();
 	  var lat, lon, r, circleBounds,ring,points,waypoint,lineString;
 	  var features = [] ;
 	  
 	 

 	   #foreach( $shape in $vectorShapeData) 
	 	    #if ($shape.objectType == "POINT") //point 
	 	      #foreach( $p in $shape.points )
	 	      	lat = $p.lat;
		 	    lon = $p.lon;
		 	    features.push(new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(lat,lon)));
		 	    r = 0.01;
		 	    circleBounds = new OpenLayers.Bounds(lat-r,lon-r,lat+r,lon+r);
		 	    bounds.extend( circleBounds );
	 	      #end
	 	    #end
	 	    #if ($shape.objectType == "POLYGON") //polygon 
	 	        points = [] ;
	 	    	#foreach( $p in $shape.points ) 
		 	    	points.push(new OpenLayers.Geometry.Point($p.lat,$p.lon));
		 	    #end
		 	   ring = new OpenLayers.Geometry.LinearRing(points);
	 	       features.push(new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Polygon([ring])));
	 	       ring.calculateBounds();
		 	   bounds.extend(ring.bounds);
	 	    #end
	 	    #if ($shape.objectType == "LINE") //waypoint 
	 	     	waypoint = [] ;
	 	    	#foreach( $p in $shape.points ) 
		 	    	waypoint.push(new OpenLayers.Geometry.Point($p.lat,$p.lon));
		 	    #end
		 	    lineString = new OpenLayers.Geometry.LineString(waypoint) ; 
		 	    features.push(new OpenLayers.Feature.Vector(lineString));
		 	    lineString.calculateBounds();
		 	    bounds.extend(lineString.bounds);
	 	    #end
 	   #end
 	   
 	   this.vlayer.addFeatures(features);
 	 },
 	 
 	 
 	

	componentGeometry2String: function (poly) {
	    var str="";
	    var i;
	    for (i=0;i<poly.length;i++) {
	    var a = poly[i];
	        str+=a.x+"," + a.y;
	
		if (i != poly.length - 1 ) {
		  str+=";";
		}
	    }
	    return str;
	}, 

	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"  
});

// -->
</script> 

First, note that we using our basic map [#parse("/WEB-INF/map-templates/map.vm")] and then we are extending this class.

You remember we put in the velocity context: vc.put("vectorShapeData", shapes). The Template method addVectorLayer will be readjusted by adding new lines to restore the geometry object from shapes back–end bean. The method mapFormPrepare will send all the objects back to the server when the user presses the submit button:

<form id="mapForm" method="post"
	onsubmit="myMap.mapFormPrepare('mapForm');"><input type="submit"
	name="Submit to Server"></form>

In this article I have tried to demonstrate how easy it is to use maps with javascript and OpenLayers. Also I have tried to show a method for reusing most of your base code for use when you have multiple pages with maps. The examples and war file are in java but you can do the same in ASP.NET. Finally, I would recommend using a UI framework like STRUTS or JSF, since they are powerful and easy to use with the Spring framework.

Download war file