21 min read

This article by Peter J Langley, author of the book OpenLayers 3.x Cookbook, sheds some light on three of the most important and talked about features of the OpenLayers library.

(For more resources related to this topic, see here.)

Introduction

This article shows us the basics and the important things that we need to know when we start creating our first web-mapping application with OpenLayers. As we will see in this and the following recipes, OpenLayers is a big and complex framework, but at the same time, it is also very powerful and flexible.

Although we’re now spoilt for choice when it comes to picking a JavaScript mapping library (as we are with most JavaScript libraries and frameworks), OpenLayers is a mature, fully-featured, and well-supported library.

In contrast to other libraries, such as Leaflet (http://leafletjs.com), which focus on a smaller download size in order to provide only the most common functionality as standard, OpenLayers tries to implement all the required things that a developer could need to create a web Geographic Information System (GIS) application.

One aspect of OpenLayers 3 that immediately differentiates itself from OpenLayers 2, is that it’s been built with the Google Closure library (https://developers.google.com/closure). Google Closure provides an extensive range of modular cross-browser JavaScript utility methods that OpenLayers 3 selectively includes.

In GIS, a real-world phenomenon is represented by the concept of a feature. It can be a place, such as a city or a village, it can be a road or a railway, it can be a region, a lake, the border of a country, or something entirely arbitrary.

Features can have a set of attributes, such as population, length, and so on. These can be represented visually through the use of points, lines, polygons, and so on, using some visual style: color, radius, width, and so on.

OpenLayers offers us a great degree of flexibility when styling features. We can use static styles or dynamic styles influenced by feature attributes. Styles can be created through various methods, such as from style functions (ol.style.StyleFunction), or by applying new style instances (ol.style.Style) directly to a feature or layer.

Let’s take a look at all of this in the following recipes.

Adding WMS layers

Web Map Service (WMS) is a standard developed by the Open Geospatial Consortium (OGC), which is implemented by many geospatial servers, among which we can find the free and open source projects, GeoServer (http://geoserver.org) and MapServer (http://mapserver.org). More information on WMS can be found at http://en.wikipedia.org/wiki/Web_Map_Service.

As a basic summary, a WMS server is a normal HTTP web server that accepts requests with some GIS-related parameters (such as projection, bounding box, and so on) and returns map tiles forming a mosaic that covers the requested bounding box. Here’s the finished recipe’s outcome using a WMS layer that covers the extent of the USA:

We are going to work with remote WMS servers, so it is not necessary that you have one installed yourself. Note that we are not responsible for these servers, and that they may have problems, or they may not be available any longer when you read this section.

Any other WMS server can be used, but the URL and layer name must be known.

How to do it…

We will add two WMS layers to work with. To do this, perform the following steps:

  1. Create an HTML file and add the OpenLayers dependencies. In particular, create the HTML to hold the map and the layer panel:
    <div id="js-map" class="map"></div>
    <div class="pane">
    <h1>WMS layers</h1>
    <p>Select the WMS layer you wish to view:</p>
    <select id="js-layers" class="layers">
       <option value="-10527519,3160212,4">Temperature "   (USA)</option>
       <option value="-408479,7213209,6">Bedrock (UK)</option>
    </select>
    </div>
  2. Create the map instance with the default OpenStreetMap layer:
    var map = new ol.Map({
    view: new ol.View({
       zoom: 4,
       center: [-10527519, 3160212]
    }),
    target: 'js-map',
    layers: [
       new ol.layer.Tile({
         source: new ol.source.OSM()
       })
    ]
    });
  3. Add the first WMS layer to the map:
    map.addLayer(new ol.layer.Tile({
    source: new ol.source.TileWMS({
       url: 'http://gis.srh.noaa.gov/arcgis/services/' +
         'NDFDTemps/MapServer/WMSServer',
         params: {
           LAYERS: 16,
           FORMAT: 'image/png',
           TRANSPARENT: true
         },
         attributions: [
           new ol.Attribution({
             html: 'Data provided by the ' +
             '<a href="http://noaa.gov">NOAA</a>.'
           })
         ]
    }),
    opacity: 0.50
    }));
  4. Add the second WMS layer to the map:
    map.addLayer(new ol.layer.Tile({
    source: new ol.source.TileWMS({
       url: 'http://ogc.bgs.ac.uk/cgi-bin/' +
           'BGS_Bedrock_and_Superficial_Geology/wms',
           params: {
             LAYERS: 'BGS_EN_Bedrock_and_Superficial_Geology'
       },
       attributions: [
         new ol.Attribution({
           html: 'Contains <a href="http://bgs.ac.uk">' +
           'British Geological Survey</a> ' +
            'materials &copy; NERC 2015'
         })
       ]
    }),
    opacity: 0.85
    }));
  5. Finally, add the layer-switching logic:
    document.getElementById('js-layers')
    .addEventListener('change', function() {
       var values = this.value.split(',');
       var view = map.getView();
       view.setCenter([
         parseFloat(values[0]),
         parseFloat(values[1])
       ]);
       view.setZoom(values[2]);
    });

How it works…

The HTML and CSS divide the page into two sections: one for the map, and the other for the layer-switching panel. The top part of our custom JavaScript file creates a new map instance with a single OpenStreetMap layer; this layer will become the background for the WMS layers in order to provide some context.

Let’s spend the rest of our time concentrating on how the WMS layers are created.

WMS layers are encapsulated within the ol.layer.Tile layer type. The source is an instance of ol.source.TileWMS, which is a subclass of ol.source.TileImage. The ol.source.TileImage class is behind many source types, such as Bing Maps, and custom OpenStreetMap layers that are based on XYZ format.

When using ol.source.TileWMS, we must at least pass in the URL of the WMS server and a layers parameter. Let’s breakdown the first WMS layer as follows:

map.addLayer(new ol.layer.Tile({
source: new ol.source.TileWMS({
   url: 'http://gis.srh.noaa.gov/arcgis/services/NDFDTemps/' +
   'MapServer/WMSServer',
   params: {
     LAYERS: 16,
     FORMAT: 'image/png',
     TRANSPARENT: true
   },
   attributions: [
     new ol.Attribution({
       html: 'Data provided by the ' +
       '<a href="http://noaa.gov">NOAA</a>.'
     })
   ]
}),
opacity: 0.50
}));

For the url property of the source, we provide the URL of the WMS server from NOAA (http://www.noaa.gov).

The params property expects an object of key/value pairs. The content of this is appended to the previous URL as query string parameters, for example, http://gis.srh.noaa.gov/arcgis/services/NDFDTemps/MapServer/WMSServer?LAYERS=16.

As mentioned earlier, at minimum, this object requires the LAYERS property with a value. We request for the layer by the name of 16. Along with this parameter, we also explicitly ask for the tile images to be in the .PNG format (FORMAT: ‘image/png’) and that the background of the tiles be transparent (TRANSPARENT: true) rather than white, which would undesirably block out the background map layer.

The default values for format and transparency are already image or PNG and false, respectively. This means you don’t need to pass them in as parameters, OpenLayers will do it for you. We’ve shown you this for learning purposes, but this isn’t strictly necessary.

There are also other parameters that OpenLayers fills in for you if not specified, such as service (WMS), version (1.3.0), request (GetMap), and so on.

For the attributions property, we created a new attribution instance to cover our usage of the WMS service, which simply contains a string of HTML linking back to the NOAA website.

Lastly, we set the opacity property of the layer to 50% (0.50), which suitably overlays the OpenStreetMap layer underneath:

map.addLayer(new ol.layer.Tile({
source: new ol.source.TileWMS({
   url: 'http://ogc.bgs.ac.uk/cgi-bin/' +
   'BGS_Bedrock_and_Superficial_Geology/wms',
     params: {
       LAYERS: 'BGS_EN_Bedrock_and_Superficial_Geology'
},
   attributions: [
     new ol.Attribution({
       html: 'Contains <a href="http://bgs.ac.uk">' +
             'British Geological Survey</a> ' +
             'materials &copy; NERC 2015'
     })
   ]
}),
opacity: 0.85
}));

Check the WMS standard to know which parameters you can use within the params property.

The use of layers is mandatory, so you always need to specify this value.

This layer from the British Geological Survey (http://bgs.ac.uk) follows the same structure as the previous WMS layer. Similarly, we provided a source URL and a layers parameter for the HTTP request. The layer name is a string rather than a number this time, which is delimited by underscores. The naming convention is at the discretion of the WMS service itself.

Like earlier, an attribution instance has been added to the layer, which contains a string of HTML linking back to the BGS website, covering our usage of the WMS service.

The opacity property of this layer is a little less transparent than the last one, at 85% (0.85):

document.getElementById('js-layers')
.addEventListener('change', function() {
   var values = this.value.split(',');
   var view = map.getView();
   view.setCenter([
     parseFloat(values[0]),
     parseFloat(values[1])
   ]);
   view.setZoom(values[2]);
});

Finally, we added a change-event listener and handler to the select menu containing both the WMS layers. If you recall from the HTML, an option’s value contains a comma-delimited string. For example, the Bedrock WMS layer option looks like this:

<option value="-408479,7213209,6">Bedrock (UK)</option>

This translates to x coordinate, y coordinate, and zoom level.

With this in mind when the change event fires, we store the value of the newly-selected option in a variable named values. The split JavaScript method creates a three-item array from the string. The array now contains the xy coordinates and the zoom level, respectively.

We store a reference to the view into a variable, namely view, as it’s accessed more than once within the event handler.

The map view is then centered to the new location with the setCenter method. We’ve made sure to convert the string values into float types for OpenLayers, via the parseFloat JavaScript method. The zoom level is then set via the setZoom method.

Continuing with the Bedrock example, it will recenter at -408479, 7213209 with zoom level 6.

Integrating with custom WMS services plays an essential role in many web-mapping applications. Learning how we did this in this recipe should give you a good idea of how to integrate with any other WMS services that you may use.

There’s more…

It’s worth mentioning that WMS services do not necessarily cover a global extent, and they will more likely cover only subset extents of the world. Case in point, the NOAA WMS layer covers only USA, and the BGS WMS layer only covers the UK.

During this topic, we only looked at the request type of GetMap, but there’s also a request type called GetCapabilities. Using the GetCapabilities request parameter on the same URL endpoint returns the capabilities (such as extent) that a WMS server supports. This is discussed in much more detail later in this book.

If you don’t specify the type of projection, the view default projection will be used. In our case, this will be EPSG:3857, which is passed up in a parameter named CRS (it’s named SRS for the GetMap version requests less than 1.3.0). If you want to retrieve WMS tiles in different projections, you need to ensure that the WMS server supports that particular format.

WMS servers return images no matter whether there is information in the bounding box that we are requesting or not. Taking this recipe as an example, if the viewable extent of the map is only the UK, blank images will get returned for WMS layer requests made for USA (via the NOAA tile requests). You can prevent these unnecessary HTTP requests by setting the visibility of any layers that do not cover the extent of the area being viewed to false.

There are some useful methods of the ol.source.TileWMS class that are worth being aware of, such as updateParams, which can be used to set parameters for the WMS request, and getUrls, which return the URLs used for the WMS source.

Creating features programmatically

Loading data from an external source is not the only way to work with vector layers. Imagine a web-mapping application where users can create new features on the fly: landing zones, perimeters, areas of interest, and so on, and add them to a vector layer with some style. This scenario requires the ability to create and add the features programmatically.

In this recipe, we will take a look at some of the ways to create a selection of features programmatically.

How to do it…

Here, we’ll create some features programmatically without any file importing. The following instructions show you how this is done:

  1. Start by creating a new HTML file with the required OpenLayers dependencies. In particular, add the div element to hold the map:
    <div id="js-map"></div>
  2. Create an empty JavaScript file and instantiate a map with a background raster layer:
    var map = new ol.Map({
    view: new ol.View({
       zoom: 3,
       center: [-2719935, 3385243]
    }),
    target: 'js-map',
    layers: [
       new ol.layer.Tile({
         source: new ol.source.MapQuest({layer: 'osm'})
       })
    ]
    });
  3. Create the point and circle features:
    var point = new ol.Feature({
    geometry: new ol.geom.Point([-606604, 3228700])
    });
    
    var circle = new ol.Feature(
    new ol.geom.Circle([-391357, 4774562], 9e5)
    );
  4. Create the line and polygon features:
    var line = new ol.Feature(
    new ol.geom.LineString([
       [-371789, 6711782], [1624133, 4539747]
    ])
    );
    
    var polygon = new ol.Feature(
    new ol.geom.Polygon([[
     [606604, 4285365], [1506726, 3933143],
       [1252344, 3248267], [195678, 3248267]
    ]])
    );
  5. Create the vector layer and add features to the layer:
    map.addLayer(new ol.layer.Vector({
    source: new ol.source.Vector({
       features: [point, circle, line, polygon]
    })
    }));

How it works…

Although we’ve created some random features for this recipe, features in mapping applications would normally represent some phenomenon of the real world with an appropriate geometry and a style associated with it.

Let’s go over the programmatic feature creation and how they are added to a vector:

layervar point = new ol.Feature({
geometry: new ol.geom.Point([-606604, 3228700])
});

Features are instances of ol.Feature. This constructor contains many useful methods, such as clone, setGeometry, getStyle, and others. When creating an instance of ol.Feature, we must either pass in a geometry of type ol.geom.Geometry or an object containing properties. We demonstrate both variations throughout this recipe.

For the point feature, we pass in a configuration object. The only property that we supply is geometry. There are other properties available, such as style, and the use of custom properties to set the feature attributes ourselves, which come with getters and setters.

The geometry is an instance of ol.geom.Point. The ol.geom class provides a variety of other feature types that we don’t get to see in this recipe, such as MultiLineString and MultiPoint. The pointgeometry type simply requires an ol.Coordinate type array (xy coordinates):

var circle = new ol.Feature(
new ol.geom.Circle([-391357, 4774562], 9e5)
);

Remember to express the coordinates in the appropriate projection, such as the one used by the view, or translate the coordinates yourself. So, for now, all features will be rendered with the default OpenLayers styling.

The circle feature follows almost the same structure as the point feature. This time, however, we don’t pass in a configuration object to ol.Feature, but instead, we directly instantiate an ol.geom.Geometry type of Circle. The circle geometry takes an array of coordinates and a second parameter for the radius. 9e5 or 9e+5 is exponential notation for 900,000.

The circle geometry also has useful methods, such as getCenter and setRadius:

var line = new ol.Feature(
new ol.geom.LineString([
   [-371789, 6711782], [1624133, 4539747]
])
);

The only noticeable difference with the LineString feature is that ol.geom.LineString expects an array of coordinate arrays. For more advanced line strings, use the ol.geom.MultiLineString geometry type (more information about them can be found on the OpenLayers API documentation: http://openlayers.org/en/v3.13.0/apidoc/):

The LineString feature also has useful methods, such as getLength:

var polygon = new ol.Feature(
new ol.geom.Polygon([[
   [606604, 4285365], [1506726, 3933143],
   [1252344, 3248267], [195678, 3248267]
]])
);

The final feature, a Polygon geometry type differs slightly from the LineString feature as it expects an ol.Coordinate type array within an array within another wrapping array. This is because the constructor (ol.geom.Polygon) expects an array of rings with each ring representing an array of coordinates. Ideally, each ring should be closed.

The polygon feature also has useful methods, such as getArea and getLinearRing:

map.addLayer(new ol.layer.Vector({
source: new ol.source.Vector({
   features: [point, circle, line, polygon]
})
}));

The OGC’s Simple Feature Access specification (http://www.opengeospatial.org/standards/sfa) contains an in-depth description of the standard. It also contains a UML class diagram where you can see all the geometry classes and hierarchy.

Finally, we create the vector layer, with a vector source instance and then add all four features into an array and pass it to the features property.

All the features we’ve created are subclasses of ol.geom.SimpleGeometry. This class provides useful base methods, such as getExtent and getFirstCoordinate.

All features have a getType method that can be used to identify the type of feature, for example, ‘Point‘ or ‘LineString‘.

There’s more…

Sometimes, the polygon features may represent a region with a hole in it. To create the hollow part of a polygon, we use the LinearRing geometry. The outcome is best explained with the following screenshot:

You can see that the polygon has a section cut out of it. To achieve this geometry, we must create the polygon in a slightly different way. Here are the steps:

  1. Create the polygon geometry:
    var polygon = new ol.geom.Polygon([[
    [606604, 4285365], [1506726, 3933143],
    [1252344, 3248267], [195678, 3248267]
    ]]);
  2. Create and add the linear ring to the polygon geometry:
    polygon.appendLinearRing(
    new ol.geom.LinearRing([
       [645740, 3766816], [1017529, 3786384],
       [1017529, 3532002], [626172, 3532002]
    ])
    );
  3. Create the completed feature:
    var polygonFeature = new ol.Feature(polygon);
  4. Finish off by adding the polygon feature to the vector layer:
    vectorLayer.getSource().addFeature(polygonFeature);
  5. We won’t break this logic down any further, as it’s quite self explanatory. Now, we’re comfortable with geometry creation.

    The ol.geom.LinearRing feature can only be used in conjunction with a polygon geometry, not as a standalone feature.

Styling features based on geometry type

We can summarize that there are two ways to style a feature. The first is by applying the style to the layer so that every feature inherits the styling. The second is to apply the styling options directly to the feature, which we’ll see with this recipe.

This recipe shows you how we can choose which flavor of styling to apply to a feature depending on the geometry type. We will apply the style directly to the feature using the ol.Feature method, setStyle.

When a point geometry type is detected, we will actually style the representing geometry as a star, rather than the default circle shape. Other styling will be applied when a geometry type of line string is detected and here’s what the output of the recipe will look like:

How to do it…

To customize the feature styling based on the geometry type, follow these steps:

  1. Create the HTML file with OpenLayers dependencies, the jQuery library, and a div element that will hold the map instance.
  2. Create a custom JavaScript file and initialize a new map instance:
    var map = new ol.Map({
    view: new ol.View({
       zoom: 4, center: [-10732981, 4676723]
    }),
    target: 'js-map',
    layers: [
       new ol.layer.Tile({
         source: new ol.source.MapQuest({layer: 'osm'})
       })
    ]
  3. Create a new vector layer and add it to the map. Have the source loader function retrieve the GeoJSON file, format the response, then pass it through our custom modifyFeatures method (which we’ll implement next) before adding the features to the vector source:
    var vectorLayer = new ol.layer.Vector({
    source: new ol.source.Vector({
       loader: function() {
         $.ajax({
           type: 'GET',
           url: 'features.geojson',
           context: this
         }).done(function(data) {
           var format = new ol.format.GeoJSON();
           var features = format.readFeatures(data);
           this.addFeatures(modifyFeatures(features));
         });
       }
    })
    });
    map.addLayer(vectorLayer);
  4. Finish off by implementing the modifyFeatures function so that it transforms the projection of the geometry and styles the feature that are based on the geometry type:
    function modifyFeatures(features) {
    features.forEach(function(feature) {
       var geometry = feature.getGeometry();
       geometry.transform('EPSG:4326', 'EPSG:3857');
    
       if (geometry.getType() === 'Point') {
         feature.setStyle(
           new ol.style.Style({
             image: new ol.style.RegularShape({
               fill: new ol.style.Fill({
                 color: [255, 0, 0, 0.6]
               }),
               stroke: new ol.style.Stroke({
                 width: 2, color: 'blue'
              }),
               points: 5, radius1: 25, radius2: 12.5
             })
           })
         );
       }
       if (geometry.getType() === 'LineString') {
         feature.setStyle(
           new ol.style.Style({
             stroke: new ol.style.Stroke({
               color: [255, 255, 255, 1],
               width: 3, lineDash: [8, 6]
             })
           })
         );
       }
    });
    return features;
    }

How it works…

Let’s briefly look over the loader function of the vector source before we take a closer examination of the logic behind the styling:

loader: function() {
$.ajax({
   type: 'GET',
   url: 'features.geojson',
   context: this
}).done(function(data) {
   var format = new ol.format.GeoJSON();
   var features = format.readFeatures(data);
   this.addFeatures(modifyFeatures(features));
});
}

Our external resource contains points and line strings in the format of GeoJSON. So we must create a new instance of ol.format.GeoJSON so that we can read in the data (format.readFeatures(data)) of the AJAX response to build out a collection of OpenLayers features.

Before adding the group of features straight into the vector source (this refers to the vector source here), we pass the array of features through our modifyFeatures method. This method will apply all the necessary styling to each feature, then return the modified features in place, and feed the result into the addFeatures method. Let’s break down the contents our modifyFeatures method:

function modifyFeatures(features) {
features.forEach(function(feature) {
   var geometry = feature.getGeometry();
   geometry.transform('EPSG:4326', 'EPSG:3857');

The logic begins by looping over each feature in the array using the JavaScript array method, forEach. The first argument passed into the anonymous iterator function is the (feature) feature.

Within the loop iteration, we store the feature’s geometry into a variable, namely geometry, as it’s accessed more than once during the loop iteration.

Unbeknown to you, the projection of coordinates within the GeoJSON file are in longitude/latitude, the EPSG:4326 projection code. The map’s view, however, is in the EPSG:3857 projection. To ensure they appear where intended on the map, we use the transform geometry method, which takes the source and the destination projections as arguments and converts the coordinates of the geometry in place:

if (geometry.getType() === 'Point') {
feature.setStyle(
   new ol.style.Style({
     image: new ol.style.RegularShape({
       fill: new ol.style.Fill({
         color: [255, 0, 0, 0.6]
        }),
       stroke: new ol.style.Stroke({
         width: 2, color: 'blue'
       }),
       points: 5, radius1: 25, radius2: 12.5
     })
   })
);
}

Next up is a conditional check on whether or not the geometry is a type of Point. The geometry instance has the getType method for this kind of purpose.

Inline of the setStyle method of the feature instance, we create a new style object from the ol.style.Style constructor. The only direct property that we’re interested in is the image property.

By default, point geometries are styled as a circle. Instead, we want to style the point as a star. We can achieve this through the use of the ol.style.RegularShape constructor. We set up a fill style with color and a stroke style with width and color.

The points property specifies the number of points for the star. In the case of a polygon shape, it represents the number of sides.

The radius1 and radius2 properties are specifically to design star shapes for the configuration of the inner and outer radius, respectively:

if (geometry.getType() === 'LineString') {
feature.setStyle(
   new ol.style.Style({
     stroke: new ol.style.Stroke({
       color: [255, 255, 255, 1],
       width: 3, lineDash: [8, 6]
     })
   })
);
}

The final piece of the method has a conditional check on the geometry type of LineString. If this is the case, we style this geometry type differently to the point geometry type. We provide a stroke style with a color, width,property and a custom lineDash. The lineDash array declares a line length of 8 followed by a gap length of 6.

Summary

In this article we looked at how to integrate WMS layers to our map from a basic HTTP web server by passing in some GIS related parameters. We also saw how to create and add features to our vector layer with some styling the idea behind this particular recipe was to enable the user to create the features programmatically without any file importing. We also saw how to style these features by applying styling option to the features individually based on their geometry type rather than styling the layer.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here