Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

Using OpenLayers with GeoNames WebServices

by
Gift

Want a free year on Tuts+ (worth $180)? Start an InMotion Hosting plan for $3.49/mo.

In this tutorial, I'll show you how to use OpenLayers, a simple to use open source JavaScript library to load, display, and render maps, with the GeoNames.org WFS to display markers on your map, just like you see on Google Maps. All it takes is some HTML, CSS, and JavaScript - that's it!


Step 1: Setting Up Your GeoNames.org Account

Before we do anything, we must first set up our GeoNames.org account. GeoNames WebServices allows you to make requests worth 30000 credit points per day, an hourly limit of 2000 credits. Different queries require different credit points, with no query 'costing' more than 4. For many small sites and simple developmental testing, this should be more than enough. They do offer Premium services at a price, but today we are going to deal with the free stuff. Free is always nice, isn't it?

To create your account, go to GeoNames.org login and set up your free account. You'll need to confirm the account in your email, but this should go fairly quickly. Once you're confirmed, you're ready to go.

"There are over 30 different types of queries you can make with GeoNames WebServices. A list of them can be found here."


Step 2: JavaScript Libraries

Next we'll need to grab the the OpenLayers source code and images. Those can be found on the OpenLayers home page. You can either download the .zip or .tar.gz. For this tutorial, all we need are the OpenLayers.js file and the img folder. For added flavor and usability, we'll be including Kelvin Luck's JScrollPane, and Brandon Aaron's jQuery mousewheel plugins, just to enhance and beautify our results div. Grab the js and css from JScrollPane. I've made some slight changes to the css, just to fit the style I wanted for this tutorial, but style it the way you would like. Grab the mousewheel plugin from GitHub. Last, but not least, grab the latest version of jQuery.

"Of course, all of the necessary files for this tutorial can be found in the Source Files download link at the top."

Today's tutorial will be addressing findNearbyPostalCodes. Now let's start writing some code!


Step 3: Directory Structure, HTML and CSS

Go ahead and create a directory structure for your application. I've named mine geonames. Inside geonames, include three additional folders: img, js and css. The images from OpenLayers will go in the img folder, the JavaScript files from OpenLayers, JScrollPane, and jQuery mousewheel, and jQuery will go in the js folder, and the stylesheet from JScrollPane will go in the css folder. Also, a few images I've created, and a couple grabbed from iconfinder can be found in the source files. Put them in the img folder as well.

  • geonames
    • img
    • js
    • css

Here we have a simple page with some HTML elements. Most of our meat will be in our JavaScript, so this part is quite short. Save this file as index.html.

<!DOCTYPE html>
<html>
  <head>
    <title>Openlayers/Geonames Tutorial</title>    
    <link type="text/css" href="css/jquery.jscrollpane.css" rel="stylesheet" media="all">
    <link type="text/css" href="css/style.css" rel="stylesheet" media="all">        
  </head>
  <body>  
    <div id="searchContainer">      
      <div id="searchHeader">Search</div>
      <div class="clear"></div>
      <div id="searchBox">
        <input type="text" id="txtSearch" name="txtSearch" size="30"><br>
        <button id="btnSearch">Search GeoNames.org</button><br>
        <button id="btnClear">Clear Markers</button>
      </div>
    </div>
    <div id="resultContainer">
      <div id="resultHeader">
        Results
      </div>
      <div class="clear"></div>      
      <div id="resultBox">
        <div id="results"></div>
      </div>
    </div>
    <div id="map"></div>
    <script src="js/jquery-1.7.2.min.js"></script>
    <script src="js/jquery.mousewheel.js"></script>
    <script src="js/jquery.jscrollpane.min.js"></script>
    <script src="js/openlayers-2.11.js"></script>
    <script src="js/geonames.js"></script>
  </body>
</html>

Here's the CSS we've created for use in this tutorial. Nothing terribly groundbreaking here, just some styling. Save this file as style.css in the css folder you created.

*  {
  font-family: Helvetica;
  color: black;
}
html {
  height: 100%;
  margin: 0;
  overflow-y: scroll;      
}
body {
  background-color: white;
  font: normal 13px arial,sans-serif;
  height: 100%;
  margin: 0;
}
#map {
  background: #ccc;
  height: 100%;
  position: absolute;
  width: 100%;
  z-index: 1;
}
#searchContainer {
  border-radius:2px;
  -moz-border-radius: 2px;
  -o-border-radius: 2px;
  -webkit-border-radius: 2px;
  background-color: rgba(247,247,247,0.5);
  border: 1px solid #ffffff;
  box-shadow: 0 0 3px #C5C5C5;
  -moz-box-shadow: 0 0 3px #C5C5C5;
  -webkit-box-shadow: 0 0 3px #C5C5C5;
  height:158px;
  width:250px;
  position:absolute;
  z-index: 2;
  top: 20px;
  right: 20px;
  padding: 4px 4px 4px 4px;
}
#searchBox {
  background-color: rgba(247,247,247,0.7);
  border-bottom-left-radius:2px;
  border-bottom-right-radius:2px;
  border: 1px solid #ffffff;
  height:136px;
  width:250px;
  text-align: center;
  line-height: 44px;
}
#resultContainer {
  border-radius:2px;
  -moz-border-radius: 2px;
  -o-border-radius: 2px;
  -webkit-border-radius: 2px;
  background-color: rgba(247,247,247,0.5);
  border: 1px solid #ffffff;
  -moz-box-shadow: 0 0 3px #C5C5C5;
  -webkit-box-shadow: 0 0 3px #C5C5C5;
  box-shadow: 0 0 3px #C5C5C5;
  width:252px;
  position:absolute;
  z-index: 2;
  top: 208px;
  right: 20px;
  padding: 4px 4px 4px 4px;
  display: none;
}
#resultHeader, #searchHeader {
  width:250px;  
  height:20px;
  border-top-left-radius:2px;
  border-top-right-radius:2px;
  border-left: 1px solid #ffffff;
  border-top: 1px solid #ffffff;
  border-right: 1px solid #ffffff;
  position: relative;
  background-repeat: repeat-x; 
  background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#C2DCFD), to(#DDECFD));
  background: -webkit-linear-gradient(top, #DDECFD, #C2DCFD);
  background: -moz-linear-gradient(top, #DDECFD, #C2DCFD);
  background: -ms-linear-gradient(top, #DDECFD, #C2DCFD);
  background: -o-linear-gradient(top, #DDECFD, #C2DCFD);
  text-align: center;  
  font-size:16px;
  text-shadow: 0px 0px 1px #96B0BB;
}
#resultBox {
  background-color: rgba(247,247,247,0.7);
  border-bottom-left-radius:2px;
  border-bottom-right-radius:2px;
  border: 1px solid #ffffff;
  max-height:418px;
  min-height:250px;
  width:250px;    
  overflow: auto;      
}
.item0,.item1 {
  float:left;
  padding: 5px 4px 5px 4px;
  width:242px;
  border-top: 1px solid #dcdcdc;
}
.item1 {      
  background-color: #FFFFFF;
}    
.clear {
  clear:both;
}      
.olPopupCloseBox { 
  background: url("../img/close.gif") no-repeat;  
  cursor: pointer; 
}
.olFramedCloudPopupContent { 
  padding: 5px;
  overflow: auto; 
}

At this point, your page should look something like this:

It isn't a whole lot to look at, so let's get into the good stuff.


Step 4: The GeoNames JavaScript

Variables

var $r = $('#results'),
    $rContainer = $('#resultContainer'),
    $rBox = $('#resultBox');

You always want to set your jQuery objects to variables. Always best practices!

Event Listener

var Observation = function (target) {
    _target = target;
    _arrObservers = [];

    var binder = function (observer) {
        _arrObservers.push(observer);
    };

    var inform = function () {
        for (var x=0; x<_arrObservers.length; x++) {
            _arrObservers[x](_target);
        }
    };

    return {
        binder: binder,
        inform: inform
    }
};

This is just a simple listener function we've created. When we create the event that we want to listen to, we pass it the object to which we want to listen; I've called this argument: target. It contains two variables: _target - a variable we set equal to our argument and _arrObservers - an empty array that we'll use to populate with listeners. Observation also contains two functions: binder and inform.

var binder = function (observer) {
    _arrObservers.push(observer);
};

Function binder adds each listener, or observer to an array of listeners. In this tutorial we're only going to be creating one custom event, but adding each listener to an array allows you to assign multiple listeners with one function.

var inform = function () {
    for (var x=0; x<_arrObservers.length; x++) {
        _arrObservers[x](_target);
    }
};

Function inform fires a message to the listener letting it know that the event is occurring. Lastly, as you see above, we'll return both of these functions so they're available for use.

GeoNames Model

var makeGeoNamesModel = function() {
  
    var _results = {},
        country = 'US',
        radius = 30,
        username = 'openlayers_tutorial',
        maxRows = 20;
    
    var notifySearchComplete = new Observation(this);   

    var search = function(val) {
        $.ajax({
            url: 'http://api.geonames.org/findNearbyPostalCodesJSON',
            data: {
              postalcode: val,
              country: country,
              radius: radius,
              username: username,
              maxRows: maxRows
            },
            dataType: 'jsonp',
            jsonpCallback: 'geoNamesResponse'
        });                
    };
  
    geoNamesResponse = function(geoData) {
        _results = geoData;                      
        notifySearchComplete.inform();                
    };
    
    var getResults = function() {
      return _results;
    };
    
    var clear = function() {
      _results = {};
    };

    return {
          notifySearchComplete: notifySearchComplete,
          search: search,
          geoNamesResponse: geoNamesResponse,
          getResults: getResults,
          clear: clear          
    };
    
};

Here we have our GeoNames model. This model will handle creating, storing, and returning the value of our GeoNames WebServices request.

    var _results = {},
        country = 'US',
        radius = 30,
        username = 'openlayers_tutorial',
        maxRows = 20;

These are just a few variables we will be using, mostly in our ajax request. For the use of our tutorial, we're only going to be searching the United States (sorry, I'm biased), but you can alter your application to accept country code input if you would like. The maximum radius we are allowed with our free account is 30 kilometers. I've also set the maximum of returned locations to 20, though you can up that value if you would like. String openlayers_tutorial is the name of the account I set up for this tutorial, so change this string to the username you created when you set up the account above. Lastly, we prep our model with an empty object called _results to be filled at a later time.

var notifySearchComplete = new Observation(this);

var search = function(val) {
    $.ajax({
        url: 'http://api.geonames.org/findNearbyPostalCodesJSON',
        data: {
          postalcode: val,
          country: country,
          radius: radius,
          username: username,
          maxRows: maxRows
        },
        dataType: 'jsonp',
        jsonpCallback: 'geoNamesResponse'
    });                
};
  
geoNamesResponse = function(geoData) {
    _results = geoData;                      
    notifySearchComplete.inform();                
};

Here we have the all important web services request: search and our event notification. Since this is a 3rd party request, we set the dataType to 'jsonp' and pass the request our variables we defined previously. Argument val will be defined later in our view. We're also going to explicitly set the callback function name - geoNamesResponse - and handle the successful request. I could have added code to handle erroneous input, but for this tutorial, we'll assume you're going to put in a correct 5 digit zip code. We're passing GeoNames the postalcode that the user has entered, but for this particular query, you could pass latitude and longitude as lat and lng if you wanted. At this point, we'll also notify our listener that this search has completed.

var getResults = function() {
    return _results;
};

var clear = function() {
    _results = {};
};

The last part of our model handle returning our results when asked for them, and also emptying our result object when the user clicks the "Clear Markers" button.

GeoNames Controller

var makeGeoNamesFormController = function() {
    return {
        handleSearch: function(txtVal,geoNamesModel) {
              geoNamesModel.search(txtVal);
        },                
        handleClear: function(geoNamesModel) {
              geoNamesModel.clear();
        },
        handleResult: function(geoNamesModel) {
              testResults = geoNamesModel.getResults();  
              return testResults;
        }
    };
};

Our controller really does nothing more than accesses functions and returns variables from our GeoNames model based on input from the user interface. We return three functions:

handleSearch - this takes the value of the user's input and the geoNamesModel as arguments, and invokes the geoNamesModel's search function, passing it the value we want to send to the GeoNames WebServices.

handleClear - this invokes the geoNamesModel's clear function so that we can clear out our result object.

handleResult - this invokes the geoNamesModel's getResults function so that we can access the results of our WFS request.

GeoNames View

var makeGeoNamesFormView = function(initGeoNamesModel, initOpenLayersMapModel, initGeoNamesFormController, initOpenLayersMapController) {

    var _geoNamesModel = initGeoNamesModel,
        _openLayersMapModel = initOpenLayersMapModel,
        _geoNamesFormController = initGeoNamesFormController,
        _openLayersMapController = initOpenLayersMapController,        
        $txtSearch = $('#txtSearch'),
        $btnSearch = $('#btnSearch'),
        $btnClear = $('#btnClear');
            
    $btnSearch.on("click",function() {
        _geoNamesFormController.handleClear(_geoNamesModel);
        _openLayersMapController.handleClear(_openLayersMapModel);
        $r.html("");
        _geoNamesFormController.handleSearch($txtSearch.val(),_geoNamesModel);
    });

    $btnClear.on("click",function() {
        _geoNamesFormController.handleClear(_geoNamesModel);
        _openLayersMapController.handleClear(_openLayersMapModel);
        $r.html("");
        $txtSearch.val("");
        $rContainer.slideUp(500);
    });
    
    $(window).on("load",function(){
        _openLayersMapController.render(_openLayersMapModel);
    });
    
    var showPoints = function() {
        var olPoints = _geoNamesFormController.handleResult(_geoNamesModel);
        var olResults = _openLayersMapController.handleMarkers(_openLayersMapModel,olPoints);
        $('#resultContainer').slideDown(500);
        $r.append(olResults.join(''));
        $rBox.jScrollPane({
            showArrows: true,
            autoReinitialise: true
        });
    };
    
    _geoNamesModel.notifySearchComplete.binder(function() {
        showPoints();
    });
      
};

The GeoNames View defines our click events and handles calling the controller functions to manipulate our view. It works closely with the controller, but leaves the model accessing and manipulating up to the controller.

var _geoNamesModel = initGeoNamesModel,
    _openLayersMapModel = initOpenLayersMapModel,
    _geoNamesFormController = initGeoNamesFormController,
    _openLayersMapController = initOpenLayersMapController,
    $txtSearch = $('#txtSearch'),
    $btnSearch = $('#btnSearch'),
    $btnClear = $('#btnClear');

All we do here is set variables equal to the respective function arguments, and as always, set your jQuery objects to variables.

$btnSearch.on("click",function() {
    _geoNamesFormController.handleClear(_geoNamesModel);
    _openLayersMapController.handleClear(_openLayersMapModel);
    $r.html("");
    _geoNamesFormController.handleSearch($txtSearch.val(),_geoNamesModel);
});
    
$btnClear.on("click",function() {
    _geoNamesFormController.handleClear(_geoNamesModel);
    _openLayersMapController.handleClear(_openLayersMapModel);
    $r.html("");
    $txtSearch.val("");
    $rContainer.slideUp(500);
});

$(window).on("load",function(){
    _openLayersMapController.render(_openLayersMapModel);
});

These are our only two click events, plus a window load event. The first binds to our "Search GeoNames.org" button and sends the value of the textbox and the model we want to deal with to our controller to handle the work. The second binds to our "Clear Markers" button that we mentioned up in the GeoNames Model section. This event calls the clearing of the results object in the GeoNames model and also the markers in the view, which we will address below. Lastly it also updates our form and the results section in our view, and hides the results as that area is now empty. The window load event handles rendering the map when the window has completely loaded.

var showPoints = function() {
    var olPoints = _geoNamesFormController.handleResult(_geoNamesModel);
    var olResults = _openLayersMapController.handleMarkers(_openLayersMapModel,olPoints);
    $('#resultContainer').slideDown(500);
    $r.append(olResults.join(''));
    $rBox.jScrollPane({
        showArrows: true,
        autoReinitialise: true
    });
};
                  
_geoNamesModel.notifySearchComplete.binder(function() {
    showPoints();
});

The final part of our GeoNames View deals with taking our results and manipulating both our results view and the map. The view knows that it must update the map and the results view because it has subscribed to the GeoNames model's notifySearchComplete event as we can see above. Upon that event completing, the view calls the showPoints function, and it handles updating the results div and displaying the markers on the map.


Step 5: The OpenLayers JavaScript

OpenLayers Model

var makeOpenLayersMapModel = function() {

    var map,
        center = new OpenLayers.LonLat(-90.3658472,38.742575),  // Centered on Lambert St Louis  International  because I am biased
        zoomLevel = 6,            
        numZoomLevels = 15,
        iconSize = 32,
        autoSizeFramedCloud = OpenLayers.Class(OpenLayers.Popup.FramedCloud, {'autoSize': true}),
        size = new OpenLayers.Size(iconSize, iconSize),
        calculateOffset = function(size) { 
          return new OpenLayers.Pixel(-size.w/2, -size.h/2); 
        },
        icon = new OpenLayers.Icon('img/redpin.png',size, null, calculateOffset);

    var renderMap = function() {
        var options={            
            controls: [
              new OpenLayers.Control.Navigation(),
              new OpenLayers.Control.PanZoomBar(),
              new OpenLayers.Control.KeyboardDefaults()
            ],          
            units: "km",
            numZoomLevels: numZoomLevels,
            maxExtent: new OpenLayers.Bounds( -170.0, 10, -60, 80),
            center: center
        };
        map = new OpenLayers.Map('map', options);
        wmslayer = new OpenLayers.Layer.WMS( "OpenLayers WMS", "http://vmap0.tiles.osgeo.org/wms/vmap0", {layers: 'basic'} );
        markers = new OpenLayers.Layer.Markers("Zip Code Markers");
        map.addLayers([wmslayer, markers]);                            
        map.zoomTo(zoomLevel);
    };

    var addMarker = function(ll, icon, popupClass, popupContentHTML) {

        var marker = new OpenLayers.Marker(ll,icon);        
        markers.addMarker(marker);      
            
        marker.events.register('mousedown', marker, function(evt) {
            for (var i=map.popups.length-1; i>=0; i--){
                map.removePopup(map.popups[i]);
            };
            var popup = new OpenLayers.Popup.FramedCloud(null, marker.lonlat, null, popupContentHTML, null, true, null);  
            popup.closeOnMove = true;
            map.addPopup(popup);
            OpenLayers.Event.stop(evt);
        });
                
    };
    
    var buildMarkers = function(pts) {
    
        var rHTML = [],
            y=0;
        $.each(pts.postalCodes, function (i, v) {
            if (i === 0) {
              newCenterLL = new OpenLayers.LonLat(v.lng,v.lat);
            }
            latit = v.lat;
            longit = v.lng;
            markerIcon = icon.clone();
            lonLatMarker = new OpenLayers.LonLat(longit,latit);
            popupClass = autoSizeFramedCloud;
            popupContentHTML = '<h3>' + v.placeName + ', ' + v.adminCode1 + ' ' + v.postalCode + '</h3>';                        
            rHTML[y++] = '<div class="item' + i%2 + '">';
            rHTML[y++] = (i+1) + ') ' + v.placeName + ', ' + v.adminCode1 + ' ' + v.postalCode + '<br />';
            rHTML[y++] = v.lat.toFixed(5) + ', ' + v.lng.toFixed(5);
            rHTML[y++] = '</div><div class="clear"></div>';
            addMarker(lonLatMarker, markerIcon, popupClass, popupContentHTML);
        });
        map.setCenter(newCenterLL,12);        
        return rHTML;
        
    };
    
    var clear = function() {
    
        for(var x=markers.markers.length-1;x>=0;x--) {                  
            markers.markers[x].destroy();
            markers.removeMarker(markers.markers[x]);
        }
        map.setCenter(center,zoomLevel);
              
    };
    
    return {
        renderMap: renderMap,
        addMarker: addMarker,
        buildMarkers: buildMarkers,
        clear: clear
    };            
}

Here we have our OpenLayers model. This model will handle creating the OpenLayers map, our map markers to depict the GeoNames WebServices result set, as well as clearing those markers from our map.

var map,
    center = new OpenLayers.LonLat(-90.3658472,38.742575),  // Centered on Lambert St Louis International because I am biased
    zoomLevel = 6,            
    numZoomLevels = 15,
    iconSize = 32,
    autoSizeFramedCloud = OpenLayers.Class(OpenLayers.Popup.FramedCloud, {'autoSize': true}),
    size = new OpenLayers.Size(iconSize, iconSize),
    calculateOffset = function(size) { 
        return new OpenLayers.Pixel(-size.w/2, -size.h/2); 
    },
    icon = new OpenLayers.Icon('img/redpin.png',size, null, calculateOffset);

We've predefined some values for our map - zoomLevel is the variable to which we will set our initial zoom. The zoom levels number increases as you get closer and closer to the Earth. As you can probably guess, numZoomLevels is the number of zoom levels that this map will allow. For our push pin markers, we must declare the size of the marker, so iconSize, while not explicitly saying so, is set to 32, and OpenLayers understands this value to be in pixels. The other items you see here are OpenLayers specific. The calculateOffset simply tells the Icon to offset the icon image so that the image is centered on the latitude and longitude of the point, not to the top left or the top right. The OpenLayers.Size constructor creates a size based on the iconSize we want. Lastly, the OpenLayers.Icon constructor defines the icon that we'll use as our markers on the map.

var renderMap = function() {
    var options={
        controls: [
          new OpenLayers.Control.Navigation(),
          new OpenLayers.Control.PanZoomBar(),
          new OpenLayers.Control.KeyboardDefaults()
        ],          
        units: "km",
        numZoomLevels: numZoomLevels,
        maxExtent: new OpenLayers.Bounds( -170.0, 10, -60, 80),
        center: center
    };
    map = new OpenLayers.Map('map', options);
    wmslayer = new OpenLayers.Layer.WMS( "OpenLayers WMS", "http://vmap0.tiles.osgeo.org/wms/vmap0", {layers: 'basic'} );
    markers = new OpenLayers.Layer.Markers("Zip Code Markers");
    map.addLayers([wmslayer, markers]);                            
    map.zoomTo(zoomLevel);
};

Here is the all important code to create our map. The OpenLayers.Map constructor takes two parameters, the DOM object that will house the map, and the options, which is an optional object with properties that the map will have. Let's take a look at the options I've included.

OpenLayers gives you the flexibility to use several different source for your map tiles.

The controls simply add basic mouse and keyboard interaction with the map. These also add the zoom bar and directional buttons above the map. The units are in kilometers, though for the purposes of this tutorial, this option is not really necessary, as we're not doing any calculations with OpenLayers, only GeoNames. The numZoomLevels sets the number of zoom levels this map will have. The center tells the map where to center itself upon rendering. The maxExtent option is set to an OpenLayers element called Bounds. You simply declare a new OpenLayers.Bounds, and we give that 4 parameters - SouthWest Longitude, SouthWest Latitude, NorthEast Longitude, and NorthEast Latitude. This gives us, what we call in the GIS world, a bounding box. Since we're only dealing with the United States in this tutorial, I set the bounds to only include North America in displaying the map. If you want to show the whole world, simply leave this option out. At this point we have our map ready. Now we can start adding layers to the map.

OpenLayers gives you the flexibility to use several different source for your map tiles. Some of those include Bing Maps, Google Maps, and OpenStreetMap. You can also use your own map tiles if you have that sort of set up. For the purposes of this tutorial, we'll be using the generic OSGeo map tiles that OpenLayers utilizes in their own examples. We do this by creating a new OpenLayers.Layer.WMS constructor. WMS stands for Web Mapping Services. We give it a title, a URL to point to the tiles and the parameters which are specific to the tile host. Next we'll create a marker layer using the OpenLayers.Layer.Markers constructor. All we have to do at this point is give it a name. Lastly, we'll add these two layers we've created to our map with the addLayers function, and we'll zoom to the appropriate zoom level we've defined.

var addMarker = function(ll, icon, popupClass, popupContentHTML) {

    var marker = new OpenLayers.Marker(ll,icon);        
    markers.addMarker(marker);      
           
    marker.events.register('mousedown', marker, function(evt) {
        for (var i=map.popups.length-1; i>=0; i--){
            map.removePopup(map.popups[i]);
        };
        var popup = new OpenLayers.Popup.FramedCloud(null, marker.lonlat, null, popupContentHTML, null, true, null);  
        popup.closeOnMove = true;
        map.addPopup(popup);
        OpenLayers.Event.stop(evt);
    });
                
};

The addMarker function takes the marker information that we'll provide in the next section and creates markers and popup clouds to be added to our map. We first make our marker with the OpenLayers.Marker constructor. All we need to do is pass it our LonLat variable and the icon we want to use. Then we simply use the addMarker function with the marker variable as its argument and the marker will be added to the map. In order to get a popup window to work if we click on the marker, we just register an event for this marker. We do this by calling the events property of this marker and use the register function to bind the event like we would in jQuery. The popup is created using the OpenLayers.Popup.FramedCloud constructor, which takes seven parameters: id, lonlat, contentSize, contentHTML, anchor, closeBox, closeBoxCallback. All we really need are the lonlat, contentHTML, and the ability to close the popup, so everything else can be null. To add the popup we just simply use the function addPopup passing the popup variable. It's as simple as that.

var buildMarkers = function(pts) {
    
    var rHTML = [],
        y=0;
    $.each(pts.postalCodes, function (i, v) {
        if (i === 0) {
          newCenterLL = new OpenLayers.LonLat(v.lng,v.lat);
        }
        latit = v.lat;
        longit = v.lng;
        markerIcon = icon.clone();
        lonLatMarker = new OpenLayers.LonLat(longit,latit);
        popupClass = autoSizeFramedCloud;
        popupContentHTML = '<h3>' + v.placeName + ', ' + v.adminCode1 + ' ' + v.postalCode + '</h3>';                        
        rHTML[y++] = '<div class="item' + i%2 + '">';
        rHTML[y++] = (i+1) + ') ' + v.placeName + ', ' + v.adminCode1 + ' ' + v.postalCode + '<br />';
        rHTML[y++] = v.lat.toFixed(5) + ', ' + v.lng.toFixed(5);
        rHTML[y++] = '</div><div class="clear"></div>';
        addMarker(lonLatMarker, markerIcon, popupClass, popupContentHTML);
    });
    map.setCenter(newCenterLL,12);        
    return rHTML;
        
};

The buildMarkers function takes the JSON and loops through the result set. For simplicity, we're assuming that the first point returned by the GeoNames WebServices request will most likely be the point you searched, so we make that our new center point, and set that to a OpenLayers.LonLat object. We've already created our OpenLayers icon, so to use it over and over again, we'll call the clone method, which simply makes a copy of that icon. The rest of the loop simply writes some HTML to an array, which we saw in the GeoNames Form View gets used to create the results div. Writing multiple lines of HTML and pushing them into an array is a quick way to dynamically create HTML without having to access the DOM over and over again. At the end of this loop, we'll invoke the addMarker function that we created above. Once we've created our markers and the loop has completed, we'll center on and zoom in to our results with the setCenter function.

var clear = function() {
    
    for(var x=markers.markers.length-1;x>=0;x--) {                  
        markers.markers[x].destroy();
        markers.removeMarker(markers.markers[x]);
    }
    map.setCenter(center,zoomLevel);
          
};

This function takes care of clearing the pushpins from the map as well as removing them from the markers layer. The destroy function removes the marker from the map. The removeMarker function removes the marker from the markers layer. Notice that we are decrementing in our for loop rather than incrementing like we normally would. We do this because as we use OpenLayer's destroy and removeMarker functions, the marker object gets updated. For example, if we had 5 markers we wanted to delete, and we incremented our loop, after the 1st destroy, we would have 4 markers left. After the 2nd destroy, we would have 3 markers left. After the 3rd destroy, we would have 2 markers left. At that point, though, our remaining markers are at positions 1 and 2, so deleting the 4th marker would have no effect because that position does not exist, therefore we remove them starting at the end and work our way forwards.

OpenLayers Controller

var makeOpenLayersMapController = function() {
    return {
        render: function(openLayersMapModel) {
              openLayersMapModel.renderMap();
        },
        handleMarkers: function(openLayersMapModel,mrkr) {
              openLayersMapModel.buildMarkers(mrkr);
        },
        handleClear: function(openLayersMapModel) {
              openLayersMapModel.clear();
        }                      
    };
};

This controller, as with the one above, does nothing more than accesses functions and returns variables from the model based on input from the user interface, only this time from our OpenLayers model. We return three functions:

  • render - this actually renders the OpenLayers map to the screen.

  • handleMarkers - this invokes the openLayersMapModel's buildMarkers function so that we can take our GeoNames WFS result and create our pushpins on the map.
  • handleClear - this invokes the openLayersMapModel's clear function so we can clear the map of our markers.

When this map code gets run, your page should look something like this:


Step 6: Instantiation

Lastly all we need to do is instantiate our models, views, and contollers.

(function() {
        
  var geoNamesModel = makeGeoNamesModel();
  var openLayersMapModel = makeOpenLayersMapModel();                
  var geoNamesFormController = makeGeoNamesFormController();
  var openLayersMapController = makeOpenLayersMapController();
  var geoNamesFormView = makeGeoNamesFormView(geoNamesModel, openLayersMapModel, geoNamesFormController, openLayersMapController);
        
})();

First we'll instantiate our models, then our controllers, and finally our view. The GeoNames view passes both models and both controllers, as it is sort of a super view, for lack of a better term. We wrap this in an anonymous function, and you're all done! Your result should look something like this once you've searched for a zip code:


Resources

OpenLayers

GeoNames


Conclusion

I hope you've all found this tutorial informative, but most importantly, easy to use and understand. GIS is a booming field, and, as I've shown you, you can do your own geospatial queries right at home with free source data like GeoNames.org. If you have any questions, please let me know in the comments, and I'll do my best to answer them!

Advertisement