Peter Robins, his website

Using OpenLayers: Adding Vectors

Vectors vs markers

Ok, so we've now seen the basics of creating a map, adding baselayers and controls, and how to transform coordinates. Now we can move on to adding vector data. Before we do that though, let's consider markers a moment. Anyone who's used Google's API will be familiar with markers, which mark a location on the map with an icon. The equivalent of this exists in OL, in Markers.js; these markers are added to a Markers layer, Layer/Markers.js. These have the advantage of being relatively simple, but they can only be used for points, not for linestrings or polygons. They are also nowhere near as versatile as vectors, which can easily be edited and moved about. Against that, only a couple of classes are needed to support markers, whereas large numbers are needed for vectors, not least because different browsers use different renderers. A few years ago, there were still browsers which did not support vector rendering, but this has now diminished, and all the main browsers can support it - at varying speeds! The development effort with OL is largely going into vectors, and I believe the intention is eventually to remove markers.

I started off using markers, but have largely changed over to vectors, so won't deal with markers here.

Vector basics

  • Vectors go in vector layers, Layer/Vector.js, which are added to the map just like the baselayers we've seen. By default, they are not baselayers but overlays, and you can have as many as you like. For example, you might like to have a different layer for different categories or different years; then users can use the layerswitcher to show or hide whichever they please
  • each vector layer has a features property, which contains an array of Feature/Vector objects
  • each feature consists of a Geometry together with an attributes property
  • there are various different types of geometry, of which the most common are Point, LineString and Polygon
  • attributes describe a feature

So, for example, if you have some geographic features in a KML file you want to read in to OL, each placemark in the file is converted to a Feature, the Point, LineString etc converted to the appropriate OL Geometry, and any name and description elements added as attributes.

Formats

OL is oriented towards open standards, so supports all the main open-standard file formats, such as GeoJSON (particularly suited for JS software like OL), GML, KML or WFS. It does not support proprietory formats, so if, for example, you have features in an ESRI shapefile, this will first have to be converted to one of the formats supported by OL (I use ogr2ogr for such conversions).

Each Format object contains read() and write() methods, which do the actual conversion to/from OL features, as in the KML example above.

Protocols

To simplify the communication with the server containing the features you want to read/write, you can add a protocol to the vector layer for the type of communication you are using. A common one is HTTP, which handles all the GET/POST/PUT/DELETE transactions with the server. There is also a WFS protocol, and a read-only JSONP-style Script protocol for cross-origin data.

The HTTP protocol needs a format and a url option. The format is the Format object as above. If the url option does not contain 'http', it is treated as a url on the server serving the page; if it does, the protocol assumes this is on another server, in which case it tries to use a proxy on the page-serving server, either supplied as an option, or as defined in OpenLayers.ProxyHost. Any proxy is of course not part of OL but part of the server logic; I describe the one I use in my Symfony tutorial. Increasingly, servers are able to handle cross-origin requests using CORS; to do this, simply ensure OpenLayers.ProxyHost is set to null.

Strategies

Strategies coordinate feature management for a layer, and take much of the grunt-work out of common use-cases. For example, a Fixed strategy can be used to read in a fixed source of features, such as the KML file above. A BBOX strategy on the other hand reads in only those features contained within the current viewport, and then reads in a new lot whenever the user pans the map. Cluster can be used to cluster points within a defined pixel distance (default is 20), and Save used to coordinate the commit of changes to the server. Any number of strategies can be added to a layer, and when the layer is added to the map, the strategies on that layer are activated (activate()).

An example

Here's an example putting all this together. It uses a Fixed strategy, HTTP protocol and the GeoJSON format to read in LineString features, which are then displayed in a vector overlay on top of the Spanish topo maps we saw before. As the 4326 projection is something we need in later examples, I create a global variable that can be used in future. It just so happens that this particular file has the 3rd dimension, so we set up the format to ignore it. Unlike the previous example, I do not set the map's centre specifically, but monitor the featuresadded event so that it can zoom to the data extent once read in. As the default style does not stand out very well, I set the style - I'll go into more details on this on the styling page.


        var wgs84 = new OpenLayers.Projection("EPSG:4326");
        map.addLayer(new OpenLayers.Layer.Vector("Line Vectors", {
            style: {
                strokeColor: "blue",
                strokeWidth: 3,
                cursor: "pointer"
            },
            eventListeners: {
                "featuresadded": function(event) {
                    // 'this' is layer
                    this.map.zoomToExtent(this.getDataExtent());
                }
            },
            projection: wgs84,
            strategies: [new OpenLayers.Strategy.Fixed()],
            protocol: new OpenLayers.Protocol.HTTP({
                url: "ingles.json",
                format: new OpenLayers.Format.GeoJSON({
                    ignoreExtraDims: true
                })
            })
        }));

When the strategy is activated (by map.addLayer()), it does layer.protocol.read(), which does format.read(). We set the projection on the layer to the projection of the data source and, because this differs from the map projection, the strategy knows it has to transform the geometries. When this is complete, the strategy does layer.addFeatures() which, when complete, triggers the 'featuresadded' event which is captured by the eventListener, which zooms to the dataExtent of the features.

Note: I have changed the transform logic in this example. Before, I was using internalProjection and externalProjection on the format. This works with the Fixed strategy (see next page for an example), but does not work with the BBOX strategy. Doing it this way means layer.projection says 4326, whereas the feature coordinates are actually in the map projection, which is rather confusing. However, it is logical if you think of layer.projection as the external projection, which may not be the same as the projection of the map. In version 3, it's planned to be able to reproject raster layers as well as vector layers, and in this case having the layer projection as that of the source server in all cases makes sense.

Selecting features

Once you have features displayed on the screen, you generally then want to select them, which you do with a SelectFeature control. This has an onSelect property that you set to a function. In this sample, this just sends an alert message if the feature does not have a 'clickable' attribute set to 'off'; in real life you would want something more useful. The SelectFeature control also has a hover property for mouseover, but this is an either/or, whereas I want both select and a tooltip message on mouseover. Fortunately, there are 'over' and 'out' callbacks in there for the hover option which you can use. In this example, my featureOver function displays the name/title/id of the feature, and calculates the length if it is a linestring. This calculation uses the getGeodesicLength() method, so the value should be accurate regardless of which projection the map is in. To decide where to display the tooltip, I use the latest value in the MousePosition control, the x and y properties of which give the pixel values for this. Unfortunately, SVG and the other renderers do not have a 'title' property (at least, not on the individual geometries) which browsers could use to display a tooltip, so the showTooltip() and getViewport() functions I use are general functions for displaying a div in a particular position whilst ensuring that the div does not go beyond the viewport; they have nothing to do with OL as such. The display uses an html element called 'tooltip':


<div class="popup" id="tooltip" style="visibility: hidden"></div>

and the CSS looks like:


.popup {
    position: absolute;
    padding: 5px;
    font-size: 8pt;
    z-index: 1000;
    background-color: white
}

A restriction in older versions of OL was that you could only select vectors in the topmost vector layer, but this was solved in 2.8. So, now if you want SelectFeature to apply to more than one layer, you pass it an array of layers; in this case, we pass all vector layers.


        var layers = map.getLayersByClass('OpenLayers.Layer.Vector');
        var selectControl = new OpenLayers.Control.SelectFeature(layers, {
            callbacks: {
                over: featureOver,
                out: hideTooltip
            }
        });
        selectControl.onSelect = function(feature) {
            if (feature.attributes.clickable != 'off') alert('Feature!');
        }
        map.addControl(selectControl);
        selectControl.activate();
...
        function featureOver(feature) {
            // 'this' is selectFeature control
            var fname = feature.attributes.name || feature.attributes.title || feature.attributes.id || feature.fid;
            if (feature.geometry.CLASS_NAME == "OpenLayers.Geometry.LineString") {
                fname += ' '+ Math.round(feature.geometry.getGeodesicLength(feature.layer.map.baseLayer.projection) * 0.1) / 100 + 'km';
            }
            var xy = this.map.getControl('ll_mouse').lastXy || { x: 0, y: 0 };
            showTooltip(fname, xy.x, xy.y);
        }

        function getViewport() {
            var e = window, a = 'inner';
            if ( !( 'innerWidth' in window ) ) {
                a = 'client';
                e = document.documentElement || document.body;
            }
            return { width : e[ a+'Width' ], height : e[ a+'Height' ] }
        }
        function showTooltip(ttText, x, y) {
            var windowWidth = getViewport().width;
            var o = document.getElementById('tooltip');
            o.innerHTML = ttText;
            if(o.offsetWidth) {
                var ew = o.offsetWidth;
            } else if(o.clip.width) {
                var ew = o.clip.width;
            }
            y = y + 16;
            x = x - (ew / 4);
            if (x < 2) {
                x = 2;
            } else if(x + ew > windowWidth) {
                x = windowWidth - ew - 4;
            }
            o.style.left = x + 'px';
            o.style.top = y + 'px';
            o.style.visibility = 'visible';
        }
        function hideTooltip() {
            document.getElementById('tooltip').style.visibility = 'hidden';
        }

OL does provide popup classes for displaying popups when users click on a feature; personally, I dislike these and use a movable/resizable window display instead. As this is not part of OL, it is not discussed in these pages.

View/download page with this logic

April 2010, updated January 2013