Managing the map's stack layers
An OpenLayers map allows us to visualize information from different kinds of layers, and it brings us methods to manage the layers that are attached to it.
In this recipe, we'll learn some techniques on how to control the layers: adding, grouping, managing the stack order, and other layer manipulation. Learning these very common operations is important because these types of tasks will be required on almost every web-mapping application.
The application will display a map on the left and a control panel on the right with a list of layers, which can be dragged, that you'll be able to sort. Here's what we'll end up with:
You can find the source code for this recipe in ch01/ch01-map-layers/
.
Note
When creating widgets such as a sortable list in this recipe, we're going to use the jQuery UI library (https://jqueryui.com), which has a single dependency on jQuery (https://jquery.com). Doing so will help us focus our attention towards the OpenLayers code, rather than the general JavaScript code that is used to create advanced UI components.
How to do it…
- We start by creating an HTML file to organize the application layout and link to resources:
<!doctype html> <html> <head> <meta charset="utf-8"> <title>Managing map's stack layers | Chapter 1</title> <link rel="stylesheet" href="ol.css"> <link rel="stylesheet" href="style.css"> </head> <body> <div id="js-map" class="map"></div> <div class="pane"> <h1>Layers</h1> <p>Drag the layer you wish to view over the satellite imagery into the box.</p> <ul id="js-layers" class="layers"></ul> </div> <script src="ol.js"></script> <script src="jquery.js"></script> <script src="jquery-ui.js"></script> <script src="script.js"></script> </body> </html>
- Create the CSS file,
style.css
, and add the following content in it:.map { position: absolute; top: 0; bottom: 0; left: 0; right: 20%; } .pane { position: absolute; top: 0; bottom: 0; right: 0; width: 20%; background: ghostwhite; border-left: 5px solid lightsteelblue; box-sizing: border-box; padding: 0 20px; } .layers { cursor: move; list-style: none; padding: 0; position: relative; } .layers::before { content: ''; display: block; position: absolute; top: 0; height: 30px; width: 100%; border: 4px solid lightsteelblue; z-index: 0; } .layers li { z-index: 1; position: relative; line-height: 38px; display: block; height: 38px; padding: 0 10px; }
- Create the
script.js
JavaScript file and add the following in it:var map = new ol.Map({ layers: [ new ol.layer.Tile({ source: new ol.source.MapQuest({ layer: 'sat' }), opacity: 0.5, zIndex: 1 }) ], view: new ol.View({ zoom: 4, center: [2120000, 0] }), target: 'js-map' }); var layerGroup = new ol.layer.Group({ layers: [ new ol.layer.Tile({ source: new ol.source.MapQuest({ layer: 'osm' }), title: 'MapQuest OSM' }), new ol.layer.Tile({ source: new ol.source.MapQuest({ layer: 'hyb' }), title: 'MapQuest Hybrid', visible: false }), new ol.layer.Tile({ source: new ol.source.OSM(), title: 'OpenStreetMap', visible: false }) ], zIndex: 0 }); map.addLayer(layerGroup); var $layersList = $('#js-layers'); layerGroup.getLayers().forEach(function(element, index, array) { var $li = $('<li />'); $li.text(element.get('title')); $layersList.append($li); }); $layersList.sortable({ update: function() { var topLayer = $layersList.find('li:first-child').text(); layerGroup.getLayers().forEach(function(element) { element.setVisible(element.get('title') === topLayer); }); } });
How it works…
The HTML contains the markup for the map and the control panel. As mentioned earlier in this recipe, we've linked to local copies of jQuery UI and jQuery. If you're not using the provided source code, you'll need to download these libraries yourself in order to follow along.
The CSS organizes the layout so that the map takes up 80% width of the screen with 20% left over for the control panel. It also provides the styling for the list of layers so that the first item in the list is outlined to represent the layer that is currently in view. We won't go into any more detail about the CSS, as we'd like to spend more of our time taking a closer look at the OpenLayers code instead.
Let's begin by breaking down the code in our custom JavaScript file:
var map = new ol.Map({ layers: [ new ol.layer.Tile({ source: new ol.source.MapQuest({ layer: 'sat' }), opacity: 0.5, zIndex: 1 }) ], view: new ol.View({ zoom: 4, center: [2120000, 0] }), target: 'js-map' });
We've introduced a new layer source here, ol.source.MapQuest
. OpenLayers provides easy access to this tile service that offers multiple types of layers
, from which we've chosen type sat
, which is an abbreviation of satellite. We're going to use this layer as our always-visible backdrop. In order to produce this desired effect, we've passed in some properties to ol.layer.Tile
to set opacity
to 50% (0.5
) and zIndex
to 1
.
The reason why we set zIndex
to 1
is to ensure that this layer is not hidden by the layer group that's added on top of this layer. This will be better explained when we continue looking through the next piece of code, as follows:
var layerGroup = new ol.layer.Group({ layers: [ new ol.layer.Tile({ source: new ol.source.MapQuest({ layer: 'osm' }), title: 'MapQuest OSM' }), new ol.layer.Tile({ source: new ol.source.MapQuest({ layer: 'hyb' }), title: 'MapQuest Hybrid', visible: false }), new ol.layer.Tile({ source: new ol.source.OSM(), title: 'OpenStreetMap', visible: false }) ], zIndex: 0 });
We instantiate a new instance of ol.layer.Group
, which expects a layers collection. One useful benefit of creating a layer group is when you want to apply the same actions against many layers at once, such as setting a property.
We instantiate three new instances of ol.layer.Tile
, two of which are different layer types offered from ol.source.MapQuest
(osm
and hyb
). The other tile service source is the familiar ol.source.OSM
layer source (OpenStreetMap) from previous recipes.
We have set the visible
property on two of the three tile layers to false
. When the page loads, the MapQuest
osm
layer will be the only visible layer from this layer group.
Optionally, we could have set the opacity
to 0 for the layers that we didn't want to display. However, there's a performance benefit from setting the visibility to false
, as OpenLayers doesn't make any unnecessary HTTP requests for the tiles of layers that aren't visible.
The title
property that we set on each layer isn't actually part of the OpenLayers API. This is a custom property, and we could have named it almost anything. This allows us to create arbitrary properties and values on the layer
objects, which we can later reference in our application. We will use the title information for some layer-switching logic and to display this text in the UI.
Lastly, a customization has been applied to all the layers inside the layer
group by setting the zIndex
property to 0
on the layer group instance. However, why have we done this?
Internally, OpenLayers stores layers
in an array, and they are rendered in the same order that they are stored in the array (so the first element is the bottom layer). You can think of the map as storing layers in a stack and they are rendered from bottom to top, so the above layers can hide beneath the below layers depending on opacity and extent.
With this in mind, when this layer group is added to the map, it'll naturally render above our first layer containing the satellite imagery. As the layers in the group are all opaque, this will result in hiding the satellite imagery layer. However, by manually manipulating the map layer stack order, we force the layer group to be at the bottom of the stack by setting zIndex
to 0,
and we force the satellite imagery layer to the top of the stack by setting zIndex
to 1
so that it'll render above this layer group.
Note
The default zIndex
property for a layer group is 0
anyway. This means that we could have just set the zIndex
property of the satellite layer to 1
, and this would leave us with the same result. We've explicitly set this here to help explain what's going on.
As we always want our satellite imagery on top, it's also worth mentioning that ol.layer.Layer
offers a setMap
method. The tile layer (ol.layer.Tile
) is a subclass of ol.layer.Layer
, so if we added the satellite imagery tile layer to the map via the setMap
method, we wouldn't need to manually adjust the zIndex
property ourselves because it would automatically appear on top. In any case, this was a good opportunity to show zIndex
ordering in action.
map.addLayer(layerGroup);
The layer group is simply added to the map instance. You'll notice that this method can be used to add a single layer or a group of layers.
var $layersList = $('#js-layers'); layerGroup.getLayers().forEach(function(element, index, array) { var $li = $('<li />'); $li.text(element.get('title')); $layersList.append($li); });
Now, we begin to take advantage of the jQuery library in order to perform some DOM operations. We store the element of the js-layers
ID into a variable, namely $layersList
. Prefixing the variable with a dollar symbol is a convention to represent the result as a jQuery object. This selector will target this HTML from earlier:
<ul id="js-layers" class="layers"></ul>
In order to populate the list of layers dynamically in the panel, we use a method from the layer group instance called getLayers
. This returns a list (ol.collection
) of all the layers for the given group, which we then chain to the forEach
method (another method available from ol.collection
).
Internally, the forEach
method calls a utility method from the Google Closure library. The available parameters within this forEach
method are element, index, and array. The element is the layer at iteration, index is the position of this layer within the group at iteration, and array is the group of layers that we're looping over. In our case, we only make use of the element parameter.
We use jQuery to create a li
element and set the text content. The text value is derived from the layer's title value—this is the custom property that we gave to each layer in the group in order to identify them. OpenLayers provides a handy get
method for the retrieval of this value. We then use jQuery to append this li
element to the ul
element.
$layersList.sortable({ update: function() { var topLayer = $layersList.find('li:first-child').text(); layerGroup.getLayers().forEach(function(element) { element.setVisible(element.get('title') === topLayer); }); } });
In order to enable list items to be reordered, we use the jQuery UI sortable widget and apply it to the list of layers in the HTML. Once an item on the list has been moved, the update event is triggered; this is where we perform some OpenLayers logic.
The text content of the topmost layer is fetched, as this is the layer the user wishes to see. The text is stored inside the topLayer
variable. This text will correspond to one of the layer titles.
We use the same getLayers
method on the layer group and the forEach
method on the ol.collection
as before. Depending on whether or not the text matches the layer title, we toggle the layer visibility accordingly with the setVisible
method.
There's more…
For this recipe, we chose to display only one other additional layer at a time. If you need to keep all layers visible and instead dynamically change the stack order of layers, you can use the layer setZIndex
method to manage which layers are above other layers.
With a collection of layers, such as what's returned with ol.Map.getLayers()
, you can use the setAt
method on the ol.collection
layers object to reorder layers, which, subsequently, alters their stacking order. This is effectively the same as changing the zIndex
property.
There are plenty of other methods to manipulate map layers. We have seen only a few in this recipe: adding, setting standard and arbitrary properties, layer stack ordering, and so on. However, you can find more methods, such as layer/layer group removal, changing the layer source, and much more.
See also
- The Managing the map's controls recipe
- The Moving around the map view recipe
- The Restricting the map extent recipe