So far we've been looking at reading vectors only. To edit them, you need a control for each edit function, plus a save strategy.
This time we'll use OpenStreetMap as the baselayer:
map.projection = "EPSG:3857";
map.displayProjection = new OpenLayers.Projection("EPSG:4326");
map.numZoomLevels = 18;
map.maxExtent = new OpenLayers.Bounds(-20037508.3427892, -20037508.3427892, 20037508.3427892, 20037508.3427892);
map.addLayer(new OpenLayers.Layer.OSM());
The server side of course has nothing to do with OL, but you can't demonstrate functionality without it, so I'll use a workspace I play around with. This is only for small-scale test purposes, and any features entered in it may disappear at any time. The only difference here 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.register('success', null, saveSuccess);
saveStrategy.events.register('fail', null, saveFail);
var vectorLayer = new OpenLayers.Layer.Vector("Line Vectors", {
styleMap: sm,
eventListeners: {
"featuresadded": dataLoaded
},
projection: wgs84,
strategies: [
new OpenLayers.Strategy.Fixed(),
saveStrategy
],
protocol: new OpenLayers.Protocol.HTTP({
url: ...,
format: new OpenLayers.Format.GeoJSON({
ignoreExtraDims: true
})
})
});
map.addLayer(vectorLayer);
And here are the success/fail callback functions:
function saveSuccess(event) {
alert('Changes saved');
}
function saveFail(event) {
alert('Error! Changes not saved');
}
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 POST 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. This 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.

