Table of Contents for
Mastering OpenLayers 3

Version ebook / Retour

Cover image for bash Cookbook, 2nd Edition Mastering OpenLayers 3 by Gábor Farkas Published by Packt Publishing, 2016
  1. Cover
  2. Table of Contents
  3. Mastering OpenLayers 3
  4. Mastering OpenLayers 3
  5. Credits
  6. About the Author
  7. About the Reviewer
  8. www.PacktPub.com
  9. Preface
  10. What you need for this book
  11. Who this book is for
  12. Conventions
  13. Reader feedback
  14. Customer support
  15. 1. Creating Simple Maps with OpenLayers 3
  16. Structure of OpenLayers 3
  17. Building the layout
  18. Using the API documentation
  19. Debugging the code
  20. Summary
  21. 2. Applying Custom Styles
  22. Customizing the default appearance
  23. Styling vector layers
  24. Customizing the appearance with JavaScript
  25. Creating a WebGIS client layout
  26. Summary
  27. 3. Working with Layers
  28. Building a layer tree
  29. Adding layers dynamically
  30. Adding vector layers with the File API
  31. Adding vector layers with a library
  32. Removing layers dynamically
  33. Changing layer attributes
  34. Changing the layer order with the Drag and Drop API
  35. Clearing the message bar
  36. Summary
  37. 4. Using Vector Data
  38. Accessing attributes
  39. Setting attributes
  40. Validating attributes
  41. Creating thematic layers
  42. Saving vector data
  43. Saving with WFS-T
  44. Modifying the geometry
  45. Summary
  46. 5. Creating Responsive Applications with Interactions and Controls
  47. Building the toolbar
  48. Mapping interactions to controls
  49. Building a set of feature selection controls
  50. Adding new vector layers
  51. Building a set of drawing tools
  52. Modifying and snapping to features
  53. Creating new interactions
  54. Building a measuring control
  55. Summary
  56. 6. Controlling the Map – View and Projection
  57. Customizing a view
  58. Constraining a view
  59. Creating a navigation history
  60. Working with extents
  61. Rotating a view
  62. Changing the map's projection
  63. Creating custom animations
  64. Summary
  65. 7. Mastering Renderers
  66. Using different renderers
  67. Creating a WebGL map
  68. Drawing lines and polygons with WebGL
  69. Blending layers
  70. Clipping layers
  71. Exporting a map
  72. Creating a raster calculator
  73. Creating a convolution matrix
  74. Clipping a layer with WebGL
  75. Summary
  76. 8. OpenLayers 3 for Mobile
  77. Responsive styling with CSS
  78. Generating geocaches
  79. Adding device-dependent controls
  80. Vectorizing the mobile version
  81. Making the mobile application interactive
  82. Summary
  83. 9. Tools of the Trade – Integrating Third-Party Applications
  84. Exporting a QGIS project
  85. Importing shapefiles
  86. Spatial analysis with Turf
  87. Spatial analysis with JSTS
  88. 3D rendering with Cesium
  89. Summary
  90. 10. Compiling Custom Builds with Closure
  91. Configuring Node JS
  92. Compiling OpenLayers 3
  93. Bundling an application with OpenLayers 3
  94. Extending OpenLayers 3
  95. Creating rich documentation with JSDoc
  96. Summary
  97. Index

Spatial analysis with Turf

In the next example, called ch09_turf, we will extend our application's topological capabilities with Turf. Turf is a well-documented, simple, but capable, library for geospatial analysis. All the basic topological functions are in the library along with some more advanced and scarce functions. Turf uses GeoJSON not only as its exchange format but also as its internal format. This enables it to skip the overhead caused by data conversion.

Tip

You can grab the most recent version of Turf from the GitHub repository at https://github.com/Turfjs/turf/releases.

First, we open the HTML file and resolve the dependencies. We have an easy job with Turf as we only have to provide one extra JavaScript file:

<head>
    […]
    <script type="text/javascript" src="../../js/turf-2.0.0/turf.min.js"></script>
</head>

Preparing an example

Before jumping into the implementation of different spatial operations, we need to tweak our example a little bit. First, as we wouldn't want to involve our layer tree, we simply hack it in order to save the selected layer in the map object:

layerTree.prototype.addSelectEvent = function (node, isChild) {
    […]
        _this.map.set('selectedLayer', _this.getLayerById(targetNode.id));
        […]
};

Next, we rewrite our map constructor a little bit so that we can add one additional vector layer. As we will implement different kinds of spatial operations, we will need different types of layers to test them on. Additionally, you can add a further line layer, too:

var map = new ol.Map({
    […]
        new ol.layer.Vector({
            source: new ol.source.Vector({
                format: new ol.format.GeoJSON({
                    defaultDataProjection: 'EPSG:4326'
                }),
                url: '../../res/world_capitals.geojson'
            }),
            name: 'World Capitals'
        }),
        new ol.layer.Vector({
            source: new ol.source.Vector({
                format: new ol.format.GeoJSON({
                    defaultDataProjection: 'EPSG:4326'
                }),
                url: '../../res/world_countries.geojson'
            }),
            name: 'World Countries'
        })
    ],
    […]
});

We also add every layer created in this way to the layer tree manually, and then register an interaction with the countries' layer as we would like to be able to modify it:

var tree = new layerTree({map: map, target: 'layertree', messages: 'messageBar'})
    .createRegistry(map.getLayers().item(0))
    .createRegistry(map.getLayers().item(1))
    .createRegistry(map.getLayers().item(2));

map.getLayers().item(2).getSource().once('change', function (evt) {
    if (this.getState() === 'ready') {
        map.addInteraction(new ol.interaction.Modify({
            features: new ol.Collection(evt.target.getFeatures())
        }));
    }
});

Implementing a buffer operation

In this example, we will create a single control for three spatial operations. The first one creates a buffer around the input elements, the second one merges an entire layer, while the third one checks for self-intersections in the input polygons.

Tip

The operations presented in this example are just a little subset of the true capabilities of Turf. If you want to get introduced to the library, you can browse its API page at http://turfjs.org/static/docs/.

First, we only create a control for buffering, then we proceed and extend this control. This way, you can check every functionality before we go on to the next. First, we create the required DOM elements for our control:

ol.control.Turf = function (opt_options) {
    var options = opt_options || {};
    var _this = this;
    var controlDiv = document.createElement('div');
    controlDiv.className = options.class || 'ol-turf ol-unselectable ol-control';
    var bufferButton = document.createElement('button');
    bufferButton.textContent = 'B';
    bufferButton.title = 'Buffer selected layer';

Next, we define what will happen if we click on the buffer button. As we can access the selected layer without getting the layer tree object, this makes our job easier. The Turf API documentation says that we can provide a feature or feature collection to the buffer method; thus, we won't make any checks, just pass the content of an entire layer to the method in GeoJSON. As we need a GeoJSON object, rather than the string representation of the GeoJSON, we can use the format object's writeFeaturesObject method instead of the more commonly used writeFeatures method:

    bufferButton.addEventListener('click', function (evt) {
        var layer = _this.getMap().get('selectedLayer');
        var units = _this.getMap().getView().getProjection().getUnits();
        if (layer instanceof ol.layer.Vector) {
            var parser = new ol.format.GeoJSON();
            var geojson = parser.writeFeaturesObject(layer.getSource().getFeatures());
            var buffered = turf.buffer(geojson, 10000, units);
            var bufferedLayer = new ol.layer.Vector({
                source: new ol.source.Vector({
                    features: parser.readFeatures(buffered)
                }),
                name: 'Buffer result'
            });
            _this.getMap().addLayer(bufferedLayer);
        }
    });
    controlDiv.appendChild(bufferButton);

After the buffering is done, we load the result in a new layer. In this example, we will work with a fixed radius of 10,000 meters.

Note

The buffer method requires three parameters: the feature(s) in GeoJSON, a buffer radius, and the units that the radius is in. We can easily get the current map units from the map's projection object.

Finally, we close our control with the ordinary method, and add it to our map:

    controlDiv.appendChild(selfIntersectButton);
    ol.control.Control.call(this, {
        element: controlDiv,
        target: options.target
    });
};
ol.inherits(ol.control.Turf, ol.control.Control);
[…]
    var map = new ol.Map({
        […]
        controls: [
            […]
            new ol.control.Turf({
                target: 'toolbar'
            })
        ],
        […]

If you test our example in this state (in the World Capitals layer first), you can try out our new buffering capability. Note that this is an expensive operation; therefore, if you provide a big dataset as input, you have to wait for it to finish for quite a bit of time:

Implementing a buffer operation

Tip

As you can see, there are some points that were not processed by the algorithm. To test it further, use the world countries' layer as an input. What do you observe? Turf returned an error and did not create the buffers. The algorithm cannot process multipart geometries correctly. Fear not! We will learn how to split up these geometries in the third operation.

Implementing a merge operation

Next, we implement an additional operation to merge entire polygon layers. Let's take a look at the Turf API documentation's merge operation. It requires a set of polygons in a geometry collection and returns a simple polygon or a multipart polygon. First, we create the DOM elements of the new control button:

ol.control.Turf = function (opt_options) {
    […]
    var mergeButton = document.createElement('button');
    mergeButton.textContent = 'M';
    mergeButton.title = 'Merge selected layer';

Next, we register a listener on the button's click event as usual. As we can pass a geometry collection to this method and the OpenLayers 3 GeoJSON's format returns a geometry collection when its writeFeaturesObject method is called, we don't check anything; we just pass the GeoJSON object to Turf:

    mergeButton.addEventListener('click', function (evt) {
        var layer = _this.getMap().get('selectedLayer');
        if (layer instanceof ol.layer.Vector) {
            var parser = new ol.format.GeoJSON();
            var geojson = parser.writeFeaturesObject(layer.getSource().getFeatures());
            var merged = turf.merge(geojson);
            var mergedLayer = new ol.layer.Vector({
                source: new ol.source.Vector({
                    features: parser.readFeatures(merged)
                }),
                name: 'Merge result'
            });
            _this.getMap().addLayer(mergedLayer);
        }
    });
    controlDiv.appendChild(mergeButton);

If you save the example and try out our new functionality, you will see that we can execute the merge operation on both of the vector layers. You can also observe that we do not get a simple continuous polygon for every continent but multipart polygons with some inner borders, instead. This can easily mean that we have some topology errors inside our dataset:

Implementing a merge operation

Tip

Once the API documentation is stated, the merge method expects a set of simple polygons as input. However, we could manage to use it in point features and multipart polygons, too. As the API documentation does not cover every use case correctly, you should always experiment with it before deploying an application.

Implementing the self-intersect operation

Now that we've discovered an algorithm to detect topological integrity in Turf, we will further implement an operation that's dedicated to such a purpose. The kinks method checks one simple polygon at a time, according to the API documentation, and returns a collection of points at self-intersections. First, let's create the DOM elements for the last control button:

    var selfIntersectButton = document.createElement('button');
    selfIntersectButton.textContent = 'S';
    selfIntersectButton.title = 'Check self intersections';

Next, as we would like this method to work correctly, we limit the operation to two types of features: simple polygons and multipart polygons. We get the selected layer's features if it is a vector:

    selfIntersectButton.addEventListener('click', function (evt) {
        var layer = _this.getMap().get('selectedLayer');
        if (layer instanceof ol.layer.Vector) {
            var parser = new ol.format.GeoJSON();
            var selfIntersectLayer = new ol.layer.Vector({
                source: new ol.source.Vector(),
                name: 'Self intersects'
            });
            _this.getMap().addLayer(selfIntersectLayer);
            var features = layer.getSource().getFeatures();

Next, we iterate through the features, convert them to GeoJSON one by one, and perform additional checks on them. If we meet a simple polygon, we just call the kinks method on it, and add occasional results to a new layer. If we have to deal with a multipart polygon, we make a second loop and get the coordinates of every part. From the coordinates, we create a simple GeoJSON polygon with Turf's polygon method and call kinks on it:

            for (var i = 0; i < features.length; i += 1) {
                var geojson = parser.writeFeatureObject(features[i]);
                if (geojson.geometry.type === 'MultiPolygon') {
                    for (var j = 0; j < geojson.geometry.coordinates.length; j += 1) {
                        var selfIntersect = turf.kinks(turf.polygon(geojson.geometry.coordinates[j]));
                        if (selfIntersect.intersections.features.length > 0) {
                            selfIntersectLayer.getSource().addFeatures(parser.readFeatures(selfIntersect.intersections));
                        }
                    }
                } else if (geojson.geometry.type === 'Polygon') {
                    var selfIntersect = turf.kinks(geojson);
                    if (selfIntersect.intersections.features.length > 0) {
                        selfIntersectLayer.getSource().addFeatures(parser.readFeatures(selfIntersect.intersections));
                    }
                }
            }
        }
    });
    controlDiv.appendChild(selfIntersectButton);

Tip

According to the API documentation, kinks returns a collection of GeoJSON points. However, it returns an object that occasionally has a set of GeoJSON points in its intersections attribute. Always use the API documentation and the rich examples hand in hand for quick and satisfying results.

If you load the final code in your browser, you can test all of our geospatial operations implemented with Turf. For the last operation, use the modify interaction, which we added to our map at the beginning of the example, and mess up one or more polygons horribly (create a lot of self-intersections):

Implementing the self-intersect operation

Note

You will notice that the asterisk remains next to the new layer. This does not mean that it is still processing; we just did not bother to create a hack that removes it. If you can see the points on the places of self-intersections, the analysis has succeeded.