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

Building a measuring control

We have come to our last example in this chapter. In this example, called ch05_measure, we will harness the full power of interactions and build a completely custom one in which we manually handle every event type. This example has three parts, so stay sharp. Firstly, as usual, we create a CSS rule for our new control button:

.toolbar .ol-measure button {
    background-image: url(../../res/button_measure.png);
}

One button is enough for this control, as we will implement two functionalities (length and area measurement) into a single interaction.

Creating the interaction

In the interaction's constructor, we accept two properties in an object literal: a reference to our map object and an optional style object or style function. As interactions do not have the exposed setMap and getMap methods, we need a reference to our map; thus, if it is not there, we return an error. If the style is not defined, we simply assign a default style, which is a simplified version of the OpenLayers 3 default editing style:

ol.interaction.Measure = function (opt_options) {
    var options = opt_options || {};
    if (!(options.map instanceof ol.Map)) {
        throw new Error('Please provide a valid OpenLayers 3 map');
    }
    var style = opt_options.style || new ol.style.Style({
        image: new ol.style.Circle({
            radius: 6,
            fill: new ol.style.Fill({
                color: [0, 153, 255, 1]
            }),
            stroke: new ol.style.Stroke({
                color: [255, 255, 255, 1],
                width: 1.5
            })
        }),
        stroke: new ol.style.Stroke({
            color: [0, 153, 255, 1],
            width: 3
        }),
        fill: new ol.style.Fill({
            color: [255, 255, 255, 0.5]
        })
    });
    var cursorFeature = new ol.Feature();
    var lineFeature = new ol.Feature();
    var polygonFeature = new ol.Feature();

We also create some empty sketch features. Next, we call the interaction's factory constructor with our custom handleEvent handler function:

    ol.interaction.Interaction.call(this, {
        handleEvent: function (evt) {
            switch (evt.type) {
                case 'pointermove':
                    cursorFeature.setGeometry(new ol.geom.Point(evt.coordinate));
                    var coordinates = this.get('coordinates');
                    coordinates[coordinates.length - 1] = evt.coordinate;
                    if (this.get('session') === 'area') {
                        if (coordinates.length < 3) {
                            lineFeature.getGeometry().setCoordinates(coordinates);
                        } else {
                            polygonFeature.getGeometry().setCoordinates([coordinates]);
                        }
                    }
                    else if (this.get('session') === 'length') {
                        lineFeature.getGeometry().setCoordinates(coordinates);
                    }
                    break;

Note

The handleEvent's return value defines whether the event needs to be propagated further. If it returns true, other interactions will also get the event; thus, we can pan or zoom the map while our interaction is active.

Firstly, we handle the move events. If we move our cursor while the interaction is active, a point feature is displayed on our cursor, which is similar to the draw interaction. Furthermore, it grabs a reference to our stored coordinate array representing the sketch feature. It switches the array's last pair of coordinates to the pointer's current location, pulling the last vertex of the sketch with the cursor.

If we are in a drawing session, it checks whether we are drawing a line or a polygon. If it is a polygon, it draws a line until our polygon is displayable (has a minimum of three vertices). After this, it draws a polygon. If it is a line, it simply draws a line.

Note

Note that we do not have to erase the line when we switch to a polygon, as they share the same coordinates. If we leave the existing line segment there, it will not make any kind of visual noise.

Next, we handle click events. Basically, this is a simple function that starts a drawing session or appends new coordinates to the coordinate array when we are in a session:

                case 'click':
                    if (!this.get('session')) {
                        if (evt.originalEvent.shiftKey) {
                            this.set('session', 'area');
                            polygonFeature.setGeometry(new ol.geom.Polygon([[[0, 0]]]));
                        } else {
                            this.set('session', 'length');
                        }
                        lineFeature.setGeometry(new ol.geom.LineString([[0, 0]]));
                        this.set('coordinates', [evt.coordinate]);
                    }
                    this.get('coordinates').push(evt.coordinate);
                    return false;

If we are not in a session, we check the original browser event. If the Shift key is pressed, we start an area measurement, otherwise we measure a length. This is the part where we set up the geometries of the features. Note that, at this point, we only define some dummy geometries so they can pass the library's error checks. We will dynamically overwrite them with our stored coordinate array later.

Tip

It seems to be a habit of using Alt and the Platform key as modifiers in applications. Be cautious with those keys. The Platform key opens up the Start menu in Windows, while the Alt key drags the active window in Linux.

If we start a new drawing session, as you can see, we add two pairs of coordinates to our array. This way, we can store the last clicked coordinate safe and sound, while we also prepare a new pair of coordinates to be modified by the move handler. Finally, we return false, as we don't want our interaction to be hooked up with some other interaction when we click on the map.

Note

As the last pair of coordinates only indicate the place that the move handler should modify, its original form won't influence the shape of our geometry; therefore, the results in the accuracy of our measurement. You can replace the penultimate line with this.get('coordinates').push(['chicken', 'nuggets']); and the interaction will still work as intended.

Next, we handle the double-click events. We stop the drawing, calculate the results, and restore every significant property to their default values:

                case 'dblclick':
                    var unit;
                    if (this.get('session') === 'area') {
                        var area = polygonFeature.getGeometry().getArea();
                        if (area > 1000000) {
                            area = area / 1000000;
                            unit = 'km²';
                        } else {
                            unit = 'm²';
                        }
                        this.set('result', {
                            type: 'area',
                            measurement: area,
                            unit: unit
                        });

If we measure an area, we calculate the polygon's area, convert the result if necessary, and then update the result property:

                    } else {
                        var length = lineFeature.getGeometry().getLength();
                        if (length > 1000) {
                            length = length / 1000;
                            unit = 'km';
                        } else {
                            unit = 'm';
                        }
                        this.set('result', {
                            type: 'length',
                            measurement: length,
                            unit: unit
                        });
                    }

If there is any other case, we must be dealing with a length measurement. We calculate the line feature's length and follow our previous logic. In this handler, we also return false as we do not want to propagate the event. Otherwise, in a default setting, the event would be propagated to ol.interaction.DoubleClickZoom, and we would unintentionally zoom into the map. If we have any other events, we just return true propagating the event to other interactions:

Note

With regard to unit notations, we currently only deal with metric coordinate systems. As the getLength and getArea methods return measurements in the projected plane, the interaction in its current form returns correct results in any coordinate system (until it converts them), but wrong units in non-metric systems.

                    cursorFeature.setGeometry(null);
                    lineFeature.setGeometry(null);
                    polygonFeature.setGeometry(null);
                    this.setProperties({
                        session: null,
                        coordinates: []
                        
                    });
                    return false;
            }
            return true;
        }
    });

Finally, we remove the overlay when the interaction is inactive, and add it to the map when it is active. We also set up the default properties and the inheritance. It is important to set the line's and the polygon's geometry to null when we deactivate the interaction. This way, if we disable the interaction during a drawing session, it won't get stuck:

    this.on('change:active', function (evt) {
        if (this.getActive()) {
            this.get('overlay').setMap(this.get('map'));
        } else {
            this.get('overlay').setMap(null);
            this.set('session', null);
            lineFeature.setGeometry(null);
            polygonFeature.setGeometry(null);
        }
    });
    this.setProperties({
        overlay: new ol.layer.Vector({
            source: new ol.source.Vector({
                features: [cursorFeature, lineFeature, polygonFeature]
            }),
            style: style
        }),
        map: options.map,
        session: null,
        coordinates: [],
        result: null
    });
};
ol.inherits(ol.interaction.Measure, ol.interaction.Interaction);

Tip

You can add a layer with the map object's addLayer method while you can also add it with the layer object's setMap method. The latter adds the layer to the map silently, skipping it from the map's layer collection, but still drawing its content to the canvas.

Only one thing is left to do: we add a measurement control to our map. We do it in our init function as this control is not part of the editing toolbar. We also assign an event listener to it. If the result property changes, we annotate the results. As we saved the overlay layer as a property too, you can add the newly created features to a permanent layer in the same event before they become null:

var measureControl = new ol.control.Interaction({
    label: ' ',
    tipLabel: 'Measure distances and areas',
    className: 'ol-measure ol-unselectable ol-control',
    interaction: new ol.interaction.Measure({
        map: map
    })
});
measureControl.get('interaction').on('change:result', function (evt) {
    var result = evt.target.get('result');
    tree.messages.textContent = result.measurement + ' ' + result.unit;
});

tools.addControl(measureControl);

If you save the code and load it up, you can see our measurement control in action:

Creating the interaction

Tip

You can keep the geometry of the last measurement with some modifications in the code. For additional help, you can see the official example at http://www.openlayers.org/en/master/examples/measure.html. It also provides some great styling tips.

Doing geodesic measurements

One thing I know for sure, Hungary's exact size is 93030 km2 and it does not correspond with 207186 km2 measured with our tool. Of course, the measurement wasn't precise, but the difference is by orders of magnitude. As you must have known, projections distort spatial data in more ways. There are better and worse projections from this aspect, and Web Mercator is one of the worst. In some applications, measuring in the projected plane is sufficient, or it is the expected behavior, but in others, more precise measurements are required. The good news is: you can calculate lengths and areas accurately in basically any projection.

The next example is in a separate JavaScript file, called ch05_measure_geodesic.js. You can link it to the example's HTML file (ch05_measure.html) or modify our interaction in place. In this example, we will modify the interaction in such a way that it will always calculate lengths and areas of a sphere. Our sphere will have a radius of the semi-major axis of the WGS 84 ellipsoid used by the EPSG:4326 projection. Firstly, we create the sphere inside our interaction:

ol.interaction.Measure = function (opt_options) {
    […]
    var sphere = new ol.Sphere(6378137);
    […]

Next, we modify our double-click handler where the calculations are done:

                case 'dblclick':
                    var unit;
                    var mapProjection = this.get('map').getView().getProjection();
                    if (this.get('session') === 'area') {
                        var lonLatPolygon = polygonFeature.getGeometry().transform(mapProjection, 'EPSG:4326');
                        var area = Math.abs(sphere.geodesicArea(lonLatPolygon.getCoordinates()[0]));
                        […]
                    } else {
                        var lonLatLine = lineFeature.getGeometry().transform(mapProjection, 'EPSG:4326');
                        var lineCoordinates = lonLatLine.getCoordinates();
                        var length = 0;
                        for (var i = 0; i < lineCoordinates.length - 1; i += 1) {
                            length += sphere.haversineDistance(lineCoordinates[i], lineCoordinates[i + 1]);
                        }
                        […]

When we measure a sphere, we have to provide the coordinates in a longitude/latitude format. Therefore, we have to transform the coordinates first from whatever projection we use to EPSG:4326. For calculating areas, we have an easy job. We simply have to pass a polygon's coordinates to our sphere's geodesicArea method.

Note

The sign of the result depends on the orientation of the passed polygon's coordinates. If the coordinates have a clockwise orientation, the result will be positive, while in the other case, it will be negative. Calculating the result's absolute value is recommended.

For calculating lengths, we can use the sphere's haversineDistance method. As it calculates length between two given pairs of coordinates, we have to iterate through the transformed line feature's coordinates.

If you update your interaction with these modifications, you will see that now you can measure quite accurately. The new result for Hungary's area is 93051 km2, which is quite good.

Calculating lengths even more precisely

The Haversine formula can be considered as quite accurate; however, in a few use cases, millimeters to a 10th of a meter precision is required. In such cases, using spherical geometry is not accurate enough.

An ellipsoidal approach, however, can provide the required accuracy at the expense of performance. There is a third JavaScript file for this example, called ch05_measure_vincenty.js. In this example, we will use a WGS 84 ellipsoid to define the shape of the Earth and measure distances on it with the Vincenty formula. As the ol.Ellipsoid class is not exposed in the compiled library, we have to use the ol-debug.js file. As the ellipsoid class was removed from OpenLayers 3.9.0, we use the debug file from version 3.8.2.

Tip

If you have an error, double check the link in the HTML file. It must connect the debug version of OpenLayers 3.8.2 to our application.

Firstly, we define an ellipsoid in our interaction. The constructor needs the length of the ellipsoid's semi-major axis in meters and also its flattening as arguments:

ol.interaction.Measure = function (opt_options) {
    […]
    var ellipsoid = new ol.Ellipsoid(6378137, 1 / 298.257223563);
    […]

Next, we only have to modify the calculations of the length measurement:

                case 'dblclick':
                    […]
                    } else {
                        […]
                        for (var i = 0; i < lineCoordinates.length - 1; i += 1) {
                            length += ellipsoid.vincentyDistance(lineCoordinates[i], lineCoordinates[i + 1]);
                        […]
                    }
                    […]

Now, our application can calculate distances and areas as accurately as OpenLayers 3 allows it to.