Peter Robins, his website

Using OpenLayers: Editing Vectors

So far we've been looking at reading vectors only. To edit them, you need a control for each edit function and, as there's little point editing them if you don't write them back to the server, you need a means to send the appropriate transactions. This is analagous to the read process, but in the other direction. As stated on the vectors page, the format includes a write() method which converts the OL features into the appropriate format, the HTTP protocol creates the POST/PUT/DELETE transactions, and the save strategy coordinates the commit to the server.

Of course you also need a program on your server that knows what to do with the transactions sent by the protocol/strategy. The server side has nothing to do with OL, but you can't demonstrate functionality without it, so I'll use a workspace I play around with (which uses FeatureServer). This is only for small-scale test purposes, and any features entered in it may disappear at any time.

So, let's look at an example. This time we'll use OpenStreetMap as the baselayer. We'll use the default worldwide maxExtent and zoomlevels provided by OL:


        map.projection = "EPSG:3857";
        map.displayProjection = new OpenLayers.Projection("EPSG:4326");
        map.addLayer(new OpenLayers.Layer.OSM());

The only difference from what we've seen before is a slight alteration in styling, plus the use of Strategy.Save. This has 2 events registered, for success and fail:


        var defStyle = {strokeColor: "blue", strokeOpacity: "0.7", strokeWidth: 2, fillColor: "blue", pointRadius: 3, cursor: "pointer"};
        var sty = OpenLayers.Util.applyDefaults(defStyle, OpenLayers.Feature.Vector.style["default"]);
        var sm = new OpenLayers.StyleMap({
            'default': sty,
            'select': {strokeColor: "red", fillColor: "red"}
        });
        var saveStrategy = new OpenLayers.Strategy.Save();
        saveStrategy.events.on({
            'success': function(event) {
                 alert('Changes saved');
            },
            'fail': function(event) {
                 alert('Error! Changes not saved');
            },
            scope: this
        });
        var vectorLayer = new OpenLayers.Layer.Vector("Line Vectors", {
            styleMap: sm,
            eventListeners: {
                "featuresadded": function(event) {
                    // 'this' is layer
                    this.map.zoomToExtent(this.getDataExtent());
                }
            },
            projection: wgs84,
            strategies: [
                new OpenLayers.Strategy.Fixed(),
                saveStrategy
            ],
            protocol: new OpenLayers.Protocol.HTTP({
                url: ...,
                format: new OpenLayers.Format.GeoJSON({
                    ignoreExtraDims: true
                })
            })
        });
        map.addLayer(vectorLayer);

The bigger new thing is the use of a separate control panel. For the purposes of this demo, we're using 2 DrawFeature controls, one for drawing points and one for drawing lines; ModifyFeature, for moving points or line vertices; DeleteFeature (for some reason, OL doesn't provide one of these, so I copied one from one of the examples); and Split, which splits lines into different segments. Finally, there is a button for saving, which calls save() on the save strategy, and the navigation control, which is the normal panning of the map. Each of these has a displayClass, which provides the link with CSS. As before, these controls are all added to a panel, called editPanel, and this is then added to the map. Unlike the previous button panel, these are toggle buttons, so only one control can be active at one time.


        var navControl = new OpenLayers.Control.Navigation({title: 'Pan/Zoom'});
        var editPanel = new OpenLayers.Control.Panel({displayClass: 'editPanel'});
        editPanel.addControls([
            new OpenLayers.Control.DrawFeature(vectorLayer, OpenLayers.Handler.Point, {displayClass: 'pointButton', title: 'Add point', handlerOptions: {style: sty}}),
            new OpenLayers.Control.DrawFeature(vectorLayer, OpenLayers.Handler.Path, {displayClass: 'lineButton', title: 'Draw line', handlerOptions: {style: sty}}),
            new OpenLayers.Control.ModifyFeature(vectorLayer, {title: 'Edit feature'}),
            new DeleteFeature(vectorLayer, {title: 'Delete Feature'}),
            new OpenLayers.Control.Split({ layer: vectorLayer, deferDelete: true, title: 'Split line' }),
            new OpenLayers.Control.Button({displayClass: 'saveButton', trigger: function() {saveStrategy.save()}, title: 'Save changes' }),
            navControl
        ]);
        editPanel.defaultControl = navControl;
        map.addControl(editPanel);

The DeleteFeature control looks like this:


DeleteFeature = OpenLayers.Class(OpenLayers.Control, {
    initialize: function(layer, options) {
        OpenLayers.Control.prototype.initialize.apply(this, [options]);
        this.layer = layer;
        this.handler = new OpenLayers.Handler.Feature(
            this, layer, {click: this.clickFeature}
        );
    },
    clickFeature: function(feature) {
        // if feature doesn't have a fid, destroy it
        if(feature.fid == undefined) {
            this.layer.destroyFeatures([feature]);
        } else {
            feature.state = OpenLayers.State.DELETE;
            this.layer.events.triggerEvent("afterfeaturemodified", {feature: feature});
            feature.renderIntent = "select";
            this.layer.drawFeature(feature);
        }
    },
    setMap: function(map) {
        this.handler.setMap(map);
        OpenLayers.Control.prototype.setMap.apply(this, arguments);
    },
    CLASS_NAME: "OpenLayers.Control.DeleteFeature"
})

So it inherits from OpenLayers.Control, and instantiation sets up a feature handler which associates a click with the function clickFeature(). This sets feature.state to DELETE and redraws the feature. Because feature.state is DELETE, drawFeature() will set the renderintent to 'delete'. By default, this sets 'display:none' on the feature, so it will not be displayed even though it is still present in layer.features; it will be finally deleted with successful return code from the server. You can of course set the delete renderintent in your stylemap if you wish some specific styling.

The additional styling for these controls looks like this. I use a sprite for a small improvement in download speed:


div.editPanel {
    top: 100px;
    left: 50px;
    position: absolute;
}
    .editPanel div {
      background-image: url("edit_sprite.png");
      background-repeat: no-repeat;
      width:  22px;
      height: 22px;
      border: thin solid black;
      margin-top: 10px;
    }
.olControlNavigationItemActive {background-position: 0px -207px; }
.olControlNavigationItemInactive {background-position: 0px -184px; }
.lineButtonItemActive {background-position: 0px -69px; }
.lineButtonItemInactive {background-position: 0px -46px; }
.pointButtonItemActive {background-position: 0px -23px; }
.pointButtonItemInactive {background-position: 0px 0px; }
.olControlModifyFeatureItemActive {background-position: 0px -161px; }
.olControlModifyFeatureItemInactive {background-position: 0px -138px; }
.olControlDeleteFeatureItemActive { background-position: 0px -253px; }
.olControlDeleteFeatureItemInactive { background-position: 0px -230px; }
.olControlSplitItemActive {background-position: 0px -347px; }
.olControlSplitItemInactive {background-position: 0px -322px; }
.saveButtonItemActive {background-position: 0px -299px; }
.saveButtonItemInactive {background-position: 0px -276px;}

So each button has an ItemActive and ItemInactive CSS class. When a button is clicked the active image for that button is displayed, and any previously active button is set to inactive so the inactive image for that button is displayed.

And that's it. The user loads the page, and makes the changes with the various controls; each change sets feature.state to 'INSERT', 'UPDATE', or 'DELETE'. When finished, the user presses the 'save' button. This calls save() on the strategy, which calls commit() on the protocol. This goes through each feature, and for each one where state is set it sends the appropriate HTTP transaction to the server.

A couple of additional points:

  • using the controls: double-click when entering a line indicates last point on line; with the split control, you draw a line over the point(s) where you want a line (or several lines) to be split
  • at the moment, feedback from the server is a simple success/fail response. The save strategy still needs some work on it; ideally, if there is a failure of some sort, there ought to be a detailed list of which transactions worked and which didn't.

View/download page with this logic

April 2010, updated January 2013