Table of Contents for
OpenLayers 3.x Cookbook - Second Edition

Version ebook / Retour

Cover image for bash Cookbook, 2nd Edition OpenLayers 3.x Cookbook - Second Edition by Antonio Santiago Perez Published by Packt Publishing, 2016
  1. Cover
  2. Table of Contents
  3. OpenLayers 3.x Cookbook Second Edition
  4. OpenLayers 3.x Cookbook Second Edition
  5. Credits
  6. About the Authors
  7. About the Reviewer
  8. www.PacktPub.com
  9. Preface
  10. What you need for this book
  11. Who this book is for
  12. Sections
  13. Conventions
  14. Reader feedback
  15. Customer support
  16. 1. Web Mapping Basics
  17. Creating a simple fullscreen map
  18. Playing with the map's options
  19. Managing the map's stack layers
  20. Managing the map's controls
  21. Moving around the map view
  22. Restricting the map's extent
  23. 2. Adding Raster Layers
  24. Using Bing imagery
  25. Using OpenStreetMap imagery
  26. Adding WMS layers
  27. Changing the zoom effect
  28. Changing layer opacity
  29. Buffering the layer data to improve map navigation
  30. Creating an image layer
  31. Setting the tile size in WMS layers
  32. 3. Working with Vector Layers
  33. Adding a GML layer
  34. Adding a KML layer
  35. Creating features programmatically
  36. Exporting features as GeoJSON
  37. Reading and creating features from a WKT
  38. Using point features as markers
  39. Removing or cloning features using overlays
  40. Zooming to the extent of a layer
  41. Adding text labels to geometry points
  42. Adding features from a WFS server
  43. Using the cluster strategy
  44. Reading features directly using AJAX
  45. Creating a heat map
  46. 4. Working with Events
  47. Creating a side-by-side map comparator
  48. Implementing a work-in-progress indicator for map layers
  49. Listening for the vector layer features' events
  50. Listening for mouse or touch events
  51. Using the keyboard to pan or zoom
  52. 5. Adding Controls
  53. Adding and removing controls
  54. Working with geolocation
  55. Placing controls outside the map
  56. Drawing features across multiple vector layers
  57. Modifying features
  58. Measuring distances and areas
  59. Getting feature information from a data source
  60. Getting information from a WMS server
  61. 6. Styling Features
  62. Styling layers
  63. Styling features based on geometry type
  64. Styling based on feature attributes
  65. Styling interaction render intents
  66. Styling clustered features
  67. 7. Beyond the Basics
  68. Working with projections
  69. Creating a custom control
  70. Selecting features by dragging out a selection area
  71. Transitioning between weather forecast imagery
  72. Using the custom OpenLayers library build
  73. Drawing in freehand mode
  74. Modifying layer appearance
  75. Adding features to the vector layer by dragging and dropping them
  76. Making use of map permalinks
  77. Index

Modifying layer appearance

We already saw how to perform some basic layer appearance modifications in the Changing layer opacity recipe in Chapter 2, Adding Raster Layers. We also saw how to reorder the layer stack and show or hide layers. This kind of layer modification may be sufficient for the majority of web-mapping applications, but we're going to take a look at some other more advanced techniques for this recipe.

During this recipe, we will modify the appearance of the raster tiles that are returned from the tile service. As we know, raster tiles are returned as images, which means that we can manipulate the color of the images when they're applied through the canvas renderer.

Color manipulation can also be performed on the server (perhaps a proxy server) before returning the modified tiles to the browser. Alternatively, if you're in control of the tile service itself, you could offer the raster tiles in a different color scheme.

We, of course, will look at how this can be achieved in the JavaScript code on the client's side. We will be using the ol.source.Raster layer source for the first time in this book. It offers a way to transform input pixel values into different pixel values for output, as specified by us.

We are going to offer the ability to switch the layer between black, white, and other colors. We will also allow the user to adjust the lightness of the layer. The source code can be found in ch07/ch07-modifying-layer-appearance. The screenshot of the finished application is displayed next, where the user has switched the layer to black and white mode.

Be mindful of the fact that we are going to be performing operations at a low level for every pixel of a raster tile. This does consume computing power, and performance issues may be noticeable (that is, the layer may take some time to transform from one color scheme to another). Consider the users that you are supporting in your application and appropriately pick the best technology and capabilities for this type of task.

Modifying layer appearance

Getting ready

For some of the color manipulations, we are going to convert them between different color spaces (RGB to HCL and vice versa). We are also going to modify the brightness of the colors. We will lean on the great D3 color library (https://github.com/d3/d3-color) to take care of the grunt work for us so that we can focus on the application code. Download the D3 color library, as we'll be referencing it within the HTML.

How to do it…

  1. Create a new HTML file and include the OpenLayers dependencies and a div element to hold the map. As mentioned in the Getting ready section of this recipe, also include the D3 color library. In particular, we have the following markup for the controls inside the side panel:
    <form>
      <select id="js-colour">
        <option value="colour">Colour</option>
        <option value="blackAndWhite">Black &amp; White</option>
      </select>
      <button id="js-darker"></button>
      <button id="js-lighter"></button>
    </form>
  2. Create a custom JavaScript file and subscribe to the color select change event, forcing the source to rerender:
    var selectElem = document.getElementById('js-colour');
    selectElem.addEventListener('change', function() {
      raster.changed();
    });
  3. Set up the default darker and lighter properties, subscribe to the click event on each button, update the relevant brightness properties, and force the source to rerender:
    var goDarker = { enable: false, level: 0.1 };
    document.getElementById('js-darker')
        .addEventListener('click', function() {
            goDarker.enable = true;
            raster.changed();
        });
    
    var goLighter = { enable: false, level: 0.1 };
    document.getElementById('js-lighter')
        .addEventListener('click', function() {
            goLighter.enable = true;
            raster.changed();
        });
  4. Create the raster source with the pixel operations to modify the layer appearance:
    var raster = new ol.source.Raster({
      sources: [
        new ol.source.Stamen({
          layer: 'watercolor'
        })
      ],
      threads: 0,
      operation: function(pixels, data) {
        if (pixels[0][0] == 0 && pixels[0][1] == 0 && pixels[0][2] == 0) {
          return [0, 0, 0, 0];
        }
    
        var rgb = d3_color.rgb(
          pixels[0][0],
          pixels[0][1],
          pixels[0][2]
        );
    
        if (data.blackAndWhite) {
          var hcl = d3_color.hcl(rgb);
          hcl.c = 0;
          rgb = d3_color.rgb(hcl);
        }
    
        if (data.goDarker) {
          rgb = rgb.darker(data.level);
        }
        else if (data.goLighter) {
          rgb = rgb.brighter(data.level);
        }
    
        return [rgb.r, rgb.g, rgb.b, 255];
      }
    });
  5. Set the correct attributions for the raster source, as follows:
    raster.setAttributions(ol.source.Stamen.ATTRIBUTIONS);
  6. Prepare some configuration for the layer manipulation before the pixel operations actually occur:
    raster.on(ol.source.RasterEventType.BEFOREOPERATIONS, function(event) {
      var data = event.data;
      data.blackAndWhite = selectElem.value === 'blackAndWhite';
    
      if (goDarker.enable) {
        data.goDarker = true;
        data.level = goDarker.level;
        goDarker.enable = false;
        goDarker.level += 0.1;
        goLighter.level -= 0.1;
      }
      else if (goLighter.enable) {
        data.goLighter = true;
        data.level = goLighter.level;
        goLighter.enable = false;
        goLighter.level += 0.1;
        goDarker.level -= 0.1;
      }
    });
  7. Instantiate a new instance of map:
    var map = new ol.Map({
      layers: [
        new ol.layer.Image({source: raster})
      ], target: 'js-map',
      view: new ol.View({
        center: [-4383204, 6985732], zoom: 3
      })
    });

How it works…

We've used the Bootstrap CSS framework to style and organize the HTML, which has been omitted for brevity. Please view the accompanying source code for the full details.

In the UI, there are controls to manipulate the layer appearance. The select menu contains two options to transform the tiles between colors or black and white. We subscribe to the change event and force the raster source to rerender by calling raster.changed() from within our handler:

selectElem.addEventListener('change', function() {
  raster.changed();
});

By kicking off a render, the pixel operations are performed, which is explained, as follows:

var goDarker = { enable: false, level: 0.1 };
document.getElementById('js-darker')
    .addEventListener('click', function() {
        goDarker.enable = true;
        raster.changed();
    });

We set up a goDarker object, specifying some current state. It tracks whether or not it's enabled and also the current level. Before we perform the pixel operations, we need to know what user action has taken place to trigger the rerender of the layer. In order to achieve this, we keep this object up-to-date accordingly.

If the user presses the darker (minus) button for the first time, the tiles are darkened by an amount of 0.1 of the original, and the level gets incremented to 0.2. If the user wants to go darker again, the level of increment will be 0.2 this time, making the layer a total of 0.2 darker than the original color, and so on. We can see how the level is incremented in later explanations.

We subscribe to the click event on the button, set the enable property on the goDarker object to true, and trigger the raster source to rerender.

We apply the same setup and logic for the lighter button, so we won't go into any more details here, as it should be self-explanatory from the previous discussion.

The raster source setup spans over many lines, so let's break it down into smaller pieces for explanation:

var raster = new ol.source.Raster({
  sources: [
    new ol.source.Stamen({
      layer: 'watercolor'
    })
  ],

A raster source allows us to perform arbitrary manipulations on pixel values, which is exactly what we want to do. The first property, sources, expects an array of sources that will provide the input for manipulation. We pass in a source from the Stamen tile provider.

threads: 0,

The next property is threads, which we set to zero. By default, OpenLayers performs the pixel operations in a worker thread for performance gains. In doing so, it introduces some complexities around scope, where functions and values must be passed to the thread context; otherwise, they'll be unavailable. For demonstration purposes, we avoided this complexity. For a good introduction on worker threads, visit http://www.html5rocks.com/en/tutorials/workers/basics/. Remember to check for browser support when implementing these advanced capabilities (http://caniuse.com/#search=workers).

operation: function(pixels, data) {
    if (pixels[0][0] == 0 && pixels[0][1] == 0 && pixels[0][2] == 0) {
      return [0, 0, 0, 0];
    }

The operation property is provided with the function that'll process the input pixel data and then return the modified pixel data, which will be assigned to the raster source.

The pixels argument is an array. The first item of the array contains the RGB values within an array of their own. This is the data that we're most interested in.

If each pixel in the array has a zero value for red, green, and blue, then we return early to avoid performing any transforms that may trigger JavaScript NaN (not a number) errors. We return an array of zeros, which is the same as the data that was delivered to this operation.

    var rgb = d3_color.rgb(
      pixels[0][0],
      pixels[0][1],
      pixels[0][2]
    );

In order to perform some pixel color manipulation with the D3 color library, we first take the pixel input and convert it to a D3 RGB object using the d3_color.rgb method and store the result in a variable, namely rgb. The pixels[0][0] input corresponds to red, pixels[0][1] corresponds to green, and so on.

    if (data.blackAndWhite) {
      var hcl = d3_color.hcl(rgb);
      hcl.c = 0;
      rgb = d3_color.rgb(hcl);
    }

If the data.blackAndWhite property is true, this means that the user has the select menu value as Black & White. To transform the color to grayscale, we must set the chroma attribute of the HCL color space to zero. In order to do this, we first convert the color from the RGB color space to the HCL color space using the d3_color.hcl method. Once we have the color in HCL, we update the chroma property (hcl.c) to zero. We then return back to the RGB color space that OpenLayers expects (rgb = d3_color.rgb(hcl)).

    if (data.goDarker) {
      rgb = rgb.darker(data.level);
    }
    else if (data.goLighter) {
      rgb = rgb.brighter(data.level);
    }

Note

The values are assigned to the data object from within the before operations event handler, which we will explain later on.

If the user has requested to go darker or lighter, we assign a new RGB color to the rgb variable by calling either the method darker or brighter on the D3 RGB object and supplying the current brightness level (data.level).

return [rgb.r, rgb.g, rgb.b, 255];

Finally, we return an array, with indexes 0-2 forming the manipulated RGB content, retrieved off the D3 RGB object; that is, rgb.r is for red.

This completes the setup of our raster source, but we also need to perform some tasks prior to the raster source operations. For this, we subscribe to the before operations event on the raster source:

raster.on(ol.source.RasterEventType.BEFOREOPERATIONS,function(event) {
  var data = event.data;
  data.blackAndWhite = selectElem.value === 'blackAndWhite';

When the before operations event is published, it provides us with an opportunity to extend the event.data object, which is passed to the operations method of the raster source (it's called data there too, as we've seen).

We attach the blackAndWhite property to the data object, which is assigned a value of true or false, depending on the evaluation of the condition (whether or not the user has selected the Black & White option from the select menu).

  if (goDarker.enable) {
    data.goDarker = true;
    data.level = goDarker.level;
    goDarker.enable = false;
    goDarker.level += 0.1;
    goLighter.level -= 0.1;
  }

If the user has clicked on the minus button to go darker, the goDarker property of data is assigned true. We also retrieve the current level of the darkness that is to be applied (goDarker.level) and assign this value to data.level for use inside the operations function.

As these changes (to go darker) will be applied to the next operation but not necessarily to the operation following that one, we must update the goDarker object accordingly. We mark it as disabled (goDarker.enable = false) and increment the darkness level, which is used for the next step of darkness (goDarker.level += 0.1).

Importantly, we also decrement the level of the goLighter object. The lightness must be relative to the darkness, so if the darkness has increased, the level of brightness will decrease. This ensures a smooth consistency when adjusting between brightness levels.

We perform similar (but reversed) logic if the user has made the layer lighter, rather than darker. We hope the preceding explanation will cover the details of this as well.

We used the verbose lookup for the event type of before operations: ol.source.RasterEventType.BEFOREOPERATIONS. The result is a string of 'beforeoperations', which we could have used instead. However, it's interesting to understand where these events are registered in the OpenLayers code.

There is also an after operations event (ol.source.RasterEventType.AFTEROPERATIONS). As an idea, if you think your operations could be long running and affect performance, then you could consider displaying a loading graphic on before operations, then hiding the loading graphic once the after operations event is published.

See also

  • The Changing layer opacity recipe in Chapter 2, Adding Raster Layers
  • The Creating an image layer recipe in Chapter 2, Adding Raster Layers