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.
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.
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.
There is only one answer for JavaScript: Ajax.
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.
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:
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
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.
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.
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.
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
nice
well its interesting and sounds good! gonna try it now.
regards