How to create an online map with a non-Mercator Projection: Part 2

In the first part of this article we created a WMS server of basemaps using an unconventional map projection (the Mollweide projection). In this second part, we shall implement the client side of the project, creating a working application that will plot user data on the basemaps.

In common with many MapServer applications, we shall be using the OpenLayers toolkit to create the web map. This can be easily configured to view WMS tiles. Additional layers can be added to plot other data (eg. GeoRSS or KML). The latest version of OpenLayers can be downloaded from the main OpenLayers website. In order to implement a scalebar, you will need the OpenLayers ScaleBar add-in. This may be automatically included in your OpenLayers package. If it is not, it can be downloaded from the OpenLayers website, here.

Finally, we will be using the Proj4JS library to transform coordinates from the geographic WGS84 coordinates (eg. as used by KML files) to our chosen projection (Mollweide WGS84 in our case). OpenLayers uses Proj4JS to perform the transformations, but the standard OpenLayers package does not include it. Proj4JS can be downloaded from the Proj4JS website.

After everything has been downloaded and installed, we can create the actual client webpage. This is a simple application with one HTML webpage.

First we need to define the required styles, and to include the various JavaScript libraries. The following code, located in the header, performs this:

<link rel="stylesheet" href="./theme/default/style.css" type="text/css" />
<link rel="stylesheet" href="./theme/default/framedCloud.css" type="text/css" />
<link rel="stylesheet" href="./theme/default/scalebar-fat.css" type="text/css" />

<style type="text/css">
    #map {
        width: 100%;
        height: 512px;
        border: 1px solid black;
    }
    .olPopup p { margin:0px; font-size: .9em;}
    .olPopup h2 { font-size:1.2em; }
</style>

<script src="./proj4js/lib/proj4js-compressed.js"> </script>
<script src="OpenLayers.js"></script>
<script src="./control/ScaleBar.js"></script>

Note that you may need to modify the paths, depending on where you installed the code. The styles include special styles for popup windows, the scale bar, and the map object.

Next we come to the functionality. This is implemented as JavaScript, and should also be included in the page’s header:

// These are the Projection definitions for Proj4JS
// EPSG 54009 is the one we are interested in for this article
// The others are included for reference
// Note: Many of these projection types are not implemented in Proj4JS.
//       See the article text for futher information

Proj4js.defs["EPSG:4326"] = "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs";
Proj4js.defs["EPSG:54008"] = "+proj=sinu +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";
Proj4js.defs["EPSG:54009"] = "+proj=moll +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";
Proj4js.defs["EPSG:54010"] = "+proj=eck6 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";
Proj4js.defs["EPSG:54012"] = "+proj=eck4 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";

// The Craster Parabolic on WGS84 does not have an EPSG code, 
// so EPSG:102012 is our invention for the Craster Parabolic
Proj4js.defs["EPSG:102012"] = "+proj=crast +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";

Proj4js.defs["EPSG:102013"] = "+proj=mbtfpq +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";
Proj4js.defs["EPSG:102014"] = "+proj=hammer +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";

Proj4js.defs["EPSG:54017"] = "+proj=cea +lon_0=0 +x_0=0 +y_0=0 +lat_ts=30 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";
Proj4js.defs["EPSG:54024"] = "+proj=bonne +lon_0=0 +x_0=0 +y_0=0 +lat_1=60 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";


// Multiple data layers can be added. Use a different marker for each RSS layer
// (we rotate through 4 marker colors)
var icon_list = new Array();
icon_list[0] = 'http://www.equal-area-maps.com/img/marker.png';
icon_list[1] = 'http://www.equal-area-maps.com/img/marker-blue.png';
icon_list[2] = 'http://www.equal-area-maps.com/img/marker-gold.png';
icon_list[3] = 'http://www.equal-area-maps.com/img/marker-green.png';

// Globa variables 

var nRSSLayers=-1;

var map, scalebar;
var base_layer;
var selectControl;


// Initialise the map: This is a callback from the HTML body tag's onInit event	  
function init()
{
   map = new OpenLayers.Map('map', { 
                         maxExtent: new OpenLayers.Bounds(-18040096,-22672290,18040096,22672290), 
			 maxResolution: 100000, units: 'meters',
                         projection: "EPSG:54009" 
			  } );

   // Create the base layer using the WMS server we created in Part 1
   base_layer = new OpenLayers.Layer.MapServer( "Base Map", 
            "http://www.your-domain.com/map/mapserv.cgi?map=mollweide.map",
                        {layers: 'outline'}, 
                        {gutter: 15 } );

   base_layer.buffer=1;
   map.addLayer(base_layer);

   // Set the initial map view, and add controls for the layer switcher (key)
   // and scale bar
   map.setCenter( new OpenLayers.LonLat(0.0, 0.0), 1 );
   map.addControl( new OpenLayers.Control.LayerSwitcher() );

   scalebar = new OpenLayers.Control.ScaleBar( { minWidth:100, maxWidth:300 } );
   map.addControl(scalebar);
}

	  
// Call back from the GeoRSS button form
// This adds a new data layer using a GeoRSS feed
// The feed's URL was entered by the user into the url_georss edit box

function addGeoRSS()
{
  // Extract the URL and a name for the layer (the file's name)
  var urlObj = OpenLayers.Util.getElement('url_georss');
  var value = urlObj.value;
  var parts = value.split("/");

  // Define the size and offset for the icon image
  var size = new OpenLayers.Size(21,25);
  var offset = new OpenLayers.Pixel(-(size.w/2), -size.h);

  // Icon color is chosen according to the layer number
  nRSSLayers = (nRSSLayers+1) % 4;

  var this_icon = new OpenLayers.Icon( icon_list[nRSSLayers], size, offset);

  // Create the layer from the URL and display it
  var newl = new OpenLayers.Layer.GeoRSS( parts[parts.length-1], value, 		                        { icon: this_icon,
                          projection: new OpenLayers.Projection("EPSG:4326")
                        }  );

  map.addLayer(newl);
  urlObj.value = "";
}


// Call back from the KML button form
// This adds a new data layer using a KML feed
// KML files include style information, so we do not loop through a series 
// of different colored pins. Otherwise this callback is identical to the GeoRSS

function addKML()
{
  var urlObj = OpenLayers.Util.getElement('url_kml');
  var value = urlObj.value;
  var parts = value.split("/");

  var newl = new OpenLayers.Layer.GML( parts[parts.length-1], value, 
                 {  format: OpenLayers.Format.KML,
                    formatOptions: {
                       extractStyles: true,
                       extractAttributes: true
                      },
                    projection: new OpenLayers.Projection("EPSG:4326")
                 }  );

  map.addLayer( newl );
  urlObj.value = "";		

  selectControl = new OpenLayers.Control.SelectFeature( newl, 
                {onSelect: onFeatureSelect, onUnselect: onFeatureUnselect} );
  map.addControl(selectControl);
  selectControl.activate();   
}


	  
// Popup Callbacks for the latest KML layer
function onPopupClose(evt)
{
  selectControl.unselect(selectedFeature);
}
 
function onFeatureSelect(feature)
{
  selectedFeature = feature;
  popup = new OpenLayers.Popup.FramedCloud("chicken", 
                                 feature.geometry.getBounds().getCenterLonLat(),
                                 new OpenLayers.Size(100,100),
                                 "<h2>"+feature.attributes.name + "</h2>" +                                         feature.attributes.description,
                                 null, true, onPopupClose
                                 );
   feature.popup = popup;
   map.addPopup(popup);
}

function onFeatureUnselect(feature)
{
   map.removePopup(feature.popup);
   feature.popup.destroy();
   feature.popup = null;
}

The first section of this code, defines the parameters for the different map projections in Proj4JS. These are standard Proj.4 definitions. We only actually need the EPSG:54009 definition in this example, but the others are included for reference. EPSG:102012 is our own invention, required due to the lack of a an EPSG code for the Craster Parabolic projection that uses WGS84. Inventing codes like this was mentioned in Part 1.

Note also that Proj4JS does not ship with all of these projection types. “moll” (Mollweide) is implemented as standard, but most of the others are not. This does not matter if you are only plotting WMS data using the missing projection type, but it must be implemented if you are to use OpenLayers to transform between coordinate systems (eg. to plot KML data). The old Equal-Area-Maps.com website side-stepped the issue by only implementing the overlay maps for three of these projections. Type “cea” (Cylindrical Equal Area – used by the Behrmann projection) was considered too important, so I had to write my own projection transformation code. This is an excellent example of the value of open source: It is possible to implement your own missing features! I have submitted the “cea” code to the Proj4JS source tree, and it should be included in the next distribution.

The init() function creates the map and the WMS base map. This is a standard OpenLayers initialization routine, except the bounds are defined in metres for the EPSG:54009 coordinate system (Mollweide, WGS84).

addGeoRSS() and addKML() are button callback functions. These add data layers using data feeds (GeoRSS or KML, respectively) entered by the user into an edit box. Again, these are fairly standard “add layer” OpenLayer implementations. Note that they explicitly state the coordinate system used by the input data (EPSG: 4326 – geographic WGS84). This ensures OpenLayers knows the coordinates need to be transformed to the map’s coordinate system (EPSG:54009 Mollweide). The GeoRSS callback also has some code which uses different colored marker icons for each successive layer – cycling through four different icons. KML usually has styling information and does not need this marker system.

Note that both the GeoRSS and KML data feeds must be on the same domain as your client webpage. This is due to a security feature in JavaScript that prohibits a JavaScript program from loading an XML file from a different domain. There are ways around this, but they are beyond the scope of this article.

The remaining callbacks are standard OpenLayers callbacks for icon popup balloon handling.

For completeness, here are the essential parts of the HTML:

<body onload="init()" >

<!-- Map Object -->
<div id="map" class="map"></div>



<p style="clear:left" ></p>

<form onsubmit="return false;">
<table>
<tr><td style="wdith:90px"><b>Add Data</b></td></tr>
<tr><td style="width:90px">GeoRSS URL: </td><td><input type="text" id="url_georss" style="width:100%" value="" />
</td>
<td style="width:100px">
  <input type="submit" onclick="addGeoRSS(); return false;" style="width:100px" value="Load GeoRSS" onsubmit="addGeoRSS(); return false;" />
</td></tr>

<tr><td style="width:90px">KML URL: </td><td><input type="text" id="url_kml" style="width:100%" value="" />
</td>
<td  style="width:100px">
 <input type="submit" onclick="addKML(); return false;" style="width:100px" value="Load KML" onsubmit="addKML(); return false;" />
</td></tr>
</table>
</form>


</body>

This defines the body onInit callback to init(), the map div object, and the form that allows a user to enter the GeoRSS and KML data feed URLs.

And that is it! You have the core of a working map application that uses an equal area projection that is not provided by any of the main map providers. The basemap uses the equal area projection, and the OpenLayers client re-projects input data to a matching coordinate system.