Initial commit

This commit is contained in:
Yohan Boniface
2014-07-31 13:16:03 +02:00
commit 4b9e5232e9
13 changed files with 1668 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
npm-debug.log
node_modules/*

4
Makefile Normal file
View File

@ -0,0 +1,4 @@
test:
firefox test/index.html
.PHONY: test

23
README.md Normal file
View File

@ -0,0 +1,23 @@
# Leaflet.Editable
Make geometries editable in Leaflet.
This is not a plug and play UI, and will not. This is a minimal, lightweight,
and fully extendable API to control editing of geometries. So you can easily
build your own UI with your own needs and choices.
See the [demo UI](http://yohanboniface.github.io/Leaflet.Editable/example/index.html).
Design keys:
- only the core needs
- no UI, but hooks everywhere needed
- everything programatically controlable
- MultiPolygon/MultiPolyline support
- Polygons' holes support
- touch support
- tests
**Considered pre-alpha. Under active developpement, many things can change
before a proprer release.**

131
example/example.js Normal file
View File

@ -0,0 +1,131 @@
L.NewLineControl = L.Control.extend({
options: {
position: 'topleft'
},
onAdd: function (map) {
var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar'),
link = L.DomUtil.create('a', '', container);
link.href = '#';
link.title = 'Create a new line';
link.innerHTML = '/\\/';
L.DomEvent.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', function () {
map.editable.startNewLine();
});
return container;
}
});
L.NewPolygonControl = L.Control.extend({
options: {
position: 'topleft'
},
onAdd: function (map) {
var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar'),
link = L.DomUtil.create('a', '', container);
link.href = '#';
link.title = 'Create a new polygon';
link.innerHTML = '▱';
L.DomEvent.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', function () {
map.editable.startNewPolygon();
});
return container;
}
});
L.NewHoleControl = L.Control.extend({
options: {
position: 'topleft'
},
onAdd: function (map) {
var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar'),
link = L.DomUtil.create('a', '', container);
link.href = '#';
link.title = 'Create a new hole';
link.innerHTML = '▣';
L.DomEvent.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', function () {
map.editable.startNewHole();
});
var toggle = function (e) {
if (e && e.editor && e.editor instanceof L.Editable.PolygonEditor) {
container.style.display = 'block';
} else {
container.style.display = 'none';
}
};
toggle();
map.editable.on('editable:editorchanged', toggle);
return container;
}
});
L.NewMarkerControl = L.Control.extend({
options: {
position: 'topleft'
},
onAdd: function (map) {
var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar'),
link = L.DomUtil.create('a', '', container);
link.href = '#';
link.title = 'Add a new marker';
link.innerHTML = '⚫';
L.DomEvent.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', function () {
map.editable.startNewMarker();
});
return container;
}
});
L.ExtendMultiControl = L.Control.extend({
options: {
position: 'topleft'
},
onAdd: function (map) {
var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar'),
link = L.DomUtil.create('a', '', container);
link.href = '#';
link.title = 'Add a new polygon to multi';
link.innerHTML = '⧉';
L.DomEvent.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', function () {
map.editable.extendMultiPolygon();
});
var toggle = function (e) {
if (e && e.editor && e.editor.feature.multi) {
container.style.display = 'block';
} else {
container.style.display = 'none';
}
};
toggle();
map.editable.on('editable:editorchanged', toggle);
return container;
}
});

85
example/index.html Normal file
View File

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Leaflet.Editable demo</title>
<link rel="stylesheet" href="../node_modules/leaflet/dist/leaflet.css" />
<script src="../node_modules/leaflet/dist/leaflet-src.js"></script>
<script src="../src/Leaflet.Editable.js"></script>
<script src="example.js"></script>
<style type='text/css'>
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; right: 0; left: 0; width:100%; }
</style>
</head>
<body>
<div id='map'></div>
<script type="text/javascript">
var startPoint = [43.1249, 1.254];
var map = L.map('map', {attributionControl: false, loadingControl: true}).setView(startPoint, 16),
tilelayer = L.tileLayer('http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {maxZoom: 20, attribution: 'Data \u00a9 <a href="http://www.openstreetmap.org/copyright"> OpenStreetMap Contributors </a> Tiles \u00a9 HOT'}).addTo(map);
map.addControl(new L.NewMarkerControl());
map.addControl(new L.NewLineControl());
map.addControl(new L.NewPolygonControl());
map.addControl(new L.NewHoleControl());
map.addControl(new L.ExtendMultiControl());
var line = L.polyline([
[43.1292, 1.256],
[43.1295, 1.259],
[43.1291, 1.261],
]).addTo(map);
line.on('dblclick', L.DomEvent.stop).on('dblclick', line.toggleEdit);
var multi = L.multiPolygon([
[
[
[43.1239, 1.244],
[43.123, 1.253],
[43.1252, 1.255],
[43.1250, 1.251],
[43.1239, 1.244]
],
[
[43.124, 1.246],
[43.1236, 1.248],
[43.12475, 1.250]
],
[
[43.124, 1.251],
[43.1236, 1.253],
[43.12475, 1.254]
],
],
[
[
[43.1269, 1.246],
[43.126, 1.252],
[43.1282, 1.255],
[43.1280, 1.245],
]
]
]).addTo(map);
multi.on('dblclick', L.DomEvent.stop).on('dblclick', multi.toggleEdit);
var poly = L.polygon([
[
[43.1239, 1.259],
[43.123, 1.263],
[43.1252, 1.265],
[43.1250, 1.261]
],
[
[43.124, 1.263],
[43.1236, 1.261],
[43.12475, 1.262]
]
]).addTo(map);
poly.on('dblclick', L.DomEvent.stop).on('dblclick', poly.toggleEdit);
</script>
</body>
</html>

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "leaflet-editable",
"version": "0.0.0",
"description": "Make geometries editable in Leaflet",
"main": "src/Leaflet.Editable.js",
"scripts": {
"test": "make test"
},
"keywords": [
"leaflet",
"map"
],
"author": "Yohan Boniface",
"license": "WTFPL",
"devDependencies": {
"chai": "^1.9.1",
"happen": "^0.1.3",
"mocha": "^1.21.3"
},
"dependencies": {
"leaflet": "^0.7.3"
}
}

950
src/Leaflet.Editable.js Normal file
View File

@ -0,0 +1,950 @@
L.Editable = L.Class.extend({
statics: {
FORWARD: 1,
BACKWARD: -1
},
includes: L.Mixin.Events,
options: {
zIndex: 10000,
polygonClass: L.Polygon,
polylineClass: L.Polyline,
markerClass: L.Marker
},
initialize: function (map) {
this.map = map;
this.editLayer = new L.LayerGroup().addTo(map);
this.newClickHandler = L.marker(this.map.getCenter(), {
icon: L.Browser.touch ? new L.Editable.TouchDivIcon() : new L.Editable.DivIcon(),
opacity: 0,
// zIndexOffset: this.options.zIndex
});
this.forwardLineGuide = this.createLineGuide();
this.backwardLineGuide = this.createLineGuide();
var activeEditor = null,
self = this;
try {
Object.defineProperty(this, 'activeEditor', {
get: function () {
return activeEditor;
},
set: function (editor) {
var oldEditor = activeEditor; // prevent looping with editor.disable()
activeEditor = editor;
if (oldEditor && oldEditor != editor) {
if (editor && editor.feature.multi && oldEditor.feature.multi === editor.feature.multi) {
oldEditor.setSecondary();
} else {
oldEditor.disable();
}
if (oldEditor.feature.multi && (!editor || oldEditor.feature.multi !== editor.feature.multi)) {
oldEditor.feature.multi.endEdit();
}
}
self.fire('editable:editorchanged', {editor: editor});
}
});
}
catch (e) {
// Certainly IE8, which has a limited version of defineProperty
}
},
createLineGuide: function () {
return L.polyline([], {dashArray: '5,10', weight: 1});
},
moveForwardLineGuide: function (latlng) {
if (this.forwardLineGuide._latlngs.length) {
this.forwardLineGuide._latlngs[1] = latlng;
this.forwardLineGuide.redraw();
}
},
moveBackwardLineGuide: function (latlng) {
if (this.backwardLineGuide._latlngs.length) {
this.backwardLineGuide._latlngs[1] = latlng;
this.backwardLineGuide.redraw();
}
},
newPointForward: function (latlng) {
this.activeEditor.addLatLng(latlng);
this.anchorForwardLineGuide(latlng);
},
newPointBackward: function (latlng) {
this.activeEditor.addLatLng(latlng);
this.anchorBackwardLineGuide(latlng);
},
anchorForwardLineGuide: function (latlng) {
this.forwardLineGuide._latlngs[0] = latlng;
this.forwardLineGuide.redraw();
},
anchorBackwardLineGuide: function (latlng) {
this.backwardLineGuide._latlngs[0] = latlng;
this.backwardLineGuide.redraw();
},
attachForwardLineGuide: function () {
this.editLayer.addLayer(this.forwardLineGuide);
},
attachBackwardLineGuide: function () {
this.editLayer.addLayer(this.backwardLineGuide);
},
detachForwardLineGuide: function () {
this.forwardLineGuide._latlngs = [];
this.editLayer.removeLayer(this.forwardLineGuide);
},
detachBackwardLineGuide: function () {
this.backwardLineGuide._latlngs = [];
this.editLayer.removeLayer(this.backwardLineGuide);
},
onMouseMove: function (e) {
if (this.activeEditor) {
this.map.editable.newClickHandler.setLatLng(e.latlng);
this.activeEditor.onMouseMove(e);
}
},
onTouch: function (e) {
this.onMouseMove(e);
if (this.activeEditor && this.activeEditor.drawing) {
this.newClickHandler._fireMouseEvent(e);
}
},
startNewLine: function () {
var line = this.createLine([]).connectToMap(this.map),
editor = line.edit();
editor.startDrawingForward();
return line;
},
startNewPolygon: function () {
var polygon = this.createPolygon([]).connectToMap(this.map),
editor = polygon.edit();
editor.startDrawingForward();
return polygon;
},
startNewHole: function () {
if (!this.activeEditor || !this.activeEditor instanceof L.Editable.PolygonEditor) return;
this.activeEditor.newHole();
},
extendMultiPolygon: function () {
if (!this.multi) return;
var polygon = this.createPolygon([]);
this.multi.addLayer(polygon);
polygon.multi = this.multi;
var editor = polygon.edit();
this.multi.setPrimary(polygon);
editor.startDrawingForward();
return polygon;
},
startNewMarker: function (latlng) {
latlng = latlng || this.map.getCenter();
var marker = this.createMarker(latlng).connectToMap(this.map),
editor = marker.edit();
editor.startDrawing();
return marker;
},
createLine: function (latlngs) {
return new this.options.polylineClass(latlngs);
},
createPolygon: function (latlngs) {
return new this.options.polygonClass(latlngs);
},
createMarker: function (latlng) {
return new this.options.markerClass(latlng);
},
addNewClickHandler: function () {
if (!this.activeEditor) return;
this.editLayer.addLayer(this.newClickHandler);
this.newClickHandler.on('click', function (e) {
this.onNewClickHandlerClicked(e);
}, this.activeEditor);
if (L.Browser.touch) this.map.on('click', this.onTouch, this);
},
removeNewClickHandler: function () {
this.editLayer.removeLayer(this.newClickHandler);
this.newClickHandler.off();
if (L.Browser.touch) this.map.off('click', this.onTouch, this);
}
});
L.Map.addInitHook(function () {
this.whenReady(function () {
this.editable = new L.Editable(this);
this.on('mousemove touchmove', this.editable.onMouseMove, this.editable);
});
});
L.Editable.DivIcon = L.DivIcon.extend({
options: {
iconSize: new L.Point(8, 8),
className: 'leaflet-div-icon leaflet-editing-icon'
}
});
L.Editable.TouchDivIcon = L.Editable.DivIcon.extend({
options: {
iconSize: new L.Point(20, 20)
}
});
L.Editable.VertexMarker = L.Marker.extend({
options: {
draggable: true,
riseOnOver: true,
icon: L.Browser.touch ? new L.Editable.TouchDivIcon() : new L.Editable.DivIcon(),
zIndex: 10001
},
initialize: function (latlng, latlngs, editor, options) {
this.latlng = latlng;
this.latlngs = latlngs;
this.editor = editor;
L.setOptions(this, options);
L.Marker.prototype.initialize.call(this, latlng);
if (this.editor.secondary) this.setSecondary();
this.latlng.__vertex = this;
this.editor.editLayer.addLayer(this);
},
setSecondary: function () {
this.setOpacity(0.3);
},
setPrimary: function () {
this.setOpacity(1);
},
onAdd: function (map) {
L.Marker.prototype.onAdd.call(this, map);
L.DomEvent.on(this.dragging._draggable, 'drag', this.onDrag, this);
this.on('click', this.onClick);
this.on('mousedown touchstart', this.onMouseDown);
this.addMiddleMarkers();
},
onDrag: function (e) {
var iconPos = L.DomUtil.getPosition(this._icon),
latlng = this._map.layerPointToLatLng(iconPos);
this.latlng.lat = latlng.lat;
this.latlng.lng = latlng.lng;
this.editor.feature.redraw();
if (this.middleMarker) {
this.middleMarker.updateLatLng();
}
var next = this.getNext();
if (next && next.middleMarker) {
next.middleMarker.updateLatLng();
}
},
onClick: function (e) {
this.editor.onVertexMarkerClick(e, this);
},
onMouseDown: function (e) {
if (this.editor.secondary) {
this.editor.setPrimary();
}
},
remove: function () {
var next = this.getNext();
if (this.middleMarker) this.middleMarker.remove();
delete this.latlng.__vertex;
this.latlngs.splice(this.latlngs.indexOf(this.latlng), 1);
if (next) next.resetMiddleMarker();
},
getPosition: function () {
return this.latlngs.indexOf(this.latlng);
},
getLastIndex: function () {
return this.latlngs.length - 1;
},
getPrevious: function () {
if (this.latlngs.length < 2) return;
var position = this.getPosition(),
previousPosition = position - 1;
if (position === 0 && this.editor.CLOSED) previousPosition = this.getLastIndex();
var previous = this.latlngs[previousPosition];
if (previous) return previous.__vertex;
},
getNext: function () {
if (this.latlngs.length < 2) return;
var position = this.getPosition(),
nextPosition = position + 1;
if (position === this.getLastIndex() && this.editor.CLOSED) nextPosition = 0;
var next = this.latlngs[nextPosition];
if (next) return next.__vertex;
},
addMiddleMarker: function (previous) {
previous = previous || this.getPrevious();
if (previous && !this.middleMarker) this.middleMarker = this.editor.addMiddleMarker(previous, this, this.latlngs, this.editor);
},
addMiddleMarkers: function () {
var previous = this.getPrevious();
if (previous) {
this.addMiddleMarker(previous);
}
var next = this.getNext();
if (next) {
next.resetMiddleMarker();
}
},
resetMiddleMarker: function () {
if (this.middleMarker) this.middleMarker.remove();
this.addMiddleMarker();
},
_initInteraction: function () {
L.Marker.prototype._initInteraction.call(this);
L.DomEvent.on(this._icon, 'touchstart', function (e) {this._fireMouseEvent(e);}, this);
}
});
L.Editable.mergeOptions({
vertexMarkerClass: L.Editable.VertexMarker
});
L.Editable.MiddleMarker = L.Marker.extend({
options: {
icon: L.Browser.touch ? new L.Editable.TouchDivIcon() : new L.Editable.DivIcon(),
zIndex: 10000,
opacity: 0.5
},
initialize: function (left, right, latlngs, editor, options) {
this.left = left;
this.right = right;
this.editor = editor;
this.latlngs = latlngs;
L.Marker.prototype.initialize.call(this, this.computeLatLng());
if (this.editor.secondary) this.setSecondary();
this.editor.editLayer.addLayer(this);
},
setSecondary: function () {
this.setOpacity(0.2);
},
setPrimary: function () {
this.setOpacity(this.options.opacity);
},
updateLatLng: function () {
this.setLatLng(this.computeLatLng());
},
computeLatLng: function () {
var lat = (this.left.latlng.lat + this.right.latlng.lat) / 2,
lng = (this.left.latlng.lng + this.right.latlng.lng) / 2;
return [lat, lng];
},
onAdd: function (map) {
L.Marker.prototype.onAdd.call(this, map);
this.on('mousedown touchstart', this.onMouseDown);
},
onMouseDown: function (e) {
this.latlngs.splice(this.index(), 0, e.latlng);
this.editor.feature.redraw();
this.editor.setPrimary();
this.remove();
var marker = this.editor.addVertexMarker(e.latlng, this.latlngs);
marker.dragging._draggable._onDown(e.originalEvent); // Transfer ongoing dragging to real marker
},
remove: function () {
this.editor.editLayer.removeLayer(this);
delete this.right.middleMarker;
},
index: function () {
return this.latlngs.indexOf(this.right.latlng);
},
_initInteraction: function () {
L.Marker.prototype._initInteraction.call(this);
L.DomEvent.on(this._icon, 'touchstart', function (e) {this._fireMouseEvent(e);}, this);
}
});
L.Editable.mergeOptions({
middleMarkerClass: L.Editable.MiddleMarker
});
L.Editable.BaseEditor = L.Class.extend({
initialize: function (map, feature, options) {
this.map = map;
this.feature = feature;
this.feature.editor = this;
this.editLayer = new L.LayerGroup();
var self = this;
try {
Object.defineProperty(this, 'active', {
get: function () {
return self.map.editable.activeEditor === self;
},
set: function (status) {
if (status && self.map.editable.activeEditor !== self) {
self.map.editable.activeEditor = self;
}
if (!status && self.map.editable.activeEditor === self) {
self.map.editable.activeEditor = null;
}
}
});
}
catch (e) {
// Certainly IE8, which has a limited version of defineProperty
}
},
enable: function () {
this.map.editable.editLayer.addLayer(this.editLayer);
this.onEnable();
return this;
},
disable: function () {
this.editLayer.clearLayers();
this.map.editable.editLayer.removeLayer(this.editLayer);
this.onDisable();
return this;
},
onEnable: function () {
this.feature.fire('editable:enable', {layer: this.feature});
},
onDisable: function () {
this.feature.fire('editable:disable', {layer: this.feature});
},
onEditing: function () {
this.feature.fire('editable:editing', {layer: this.feature});
},
onEdited: function () {
this.feature.fire('editable:edited', {layer: this.feature});
},
startDrawing: function () {
if (!this.drawing) this.drawing = L.Editable.FORWARD;
this.map.editable.addNewClickHandler();
this.onEditing();
},
finishDrawing: function () {
this.onEdited();
this.drawing = false;
this.map.editable.removeNewClickHandler();
}
});
L.Editable.MarkerEditor = L.Editable.BaseEditor.extend({
enable: function () {
L.Editable.BaseEditor.prototype.enable.call(this);
this.active = true;
this.feature.dragging.enable();
this.feature.on('dragstart', this.onEditing, this);
return this;
},
disable: function () {
L.Editable.BaseEditor.prototype.disable.call(this);
this.active = false;
this.feature.dragging.disable();
this.feature.off('dragstart', this.onEditing, this);
return this;
},
onMouseMove: function (e) {
if (this.drawing) {
this.feature.setLatLng(e.latlng);
this.map.editable.newClickHandler._bringToFront();
}
},
onNewClickHandlerClicked: function (e) {
if (this.checkAddConstraints && !this.checkAddConstraints(e.latlng)) {
return;
}
this.finishDrawing();
}
});
L.Editable.PathEditor = L.Editable.BaseEditor.extend({
CLOSED: false,
enable: function (secondary) {
L.Editable.BaseEditor.prototype.enable.call(this);
this.secondary = secondary;
if (this.feature) {
this.initVertexMarkers();
}
if (!this.secondary) this.active = true;
return this;
},
disable: function () {
L.Editable.BaseEditor.prototype.disable.call(this);
if (!this.secondary) this.active = false;
},
setPrimary: function () {
if (this.feature.multi) {
this.feature.multi.setSecondary(this.feature);
}
delete this.secondary;
this.active = true;
this.editLayer.eachLayer(function (layer) {
layer.setPrimary();
});
},
setSecondary: function () {
this.secondary = true;
this.editLayer.eachLayer(function (layer) {
if (layer.setSecondary) layer.setSecondary();
});
},
initVertexMarkers: function () {
// groups can be only latlngs (for polyline or symple polygon,
// or latlngs plus many holes, in case of a complex polygon)
var latLngGroups = this.getLatLngsGroups();
for (var i = 0; i < latLngGroups.length; i++) {
this.addVertexMarkers(latLngGroups[i]);
}
},
addVertexMarker: function (latlng, latlngs) {
return new this.map.editable.options.vertexMarkerClass(latlng, latlngs, this);
},
addVertexMarkers: function (latlngs) {
for (var i = 0; i < latlngs.length; i++) {
this.addVertexMarker(latlngs[i], latlngs);
}
},
onVertexMarkerClick: function (e, vertex) {
var position = vertex.getPosition();
if (e.originalEvent.ctrlKey) {
this.onVertexMarkerCtrlClick(e, vertex, position);
} else if (e.originalEvent.altKey) {
this.onVertexMarkerAltClick(e, vertex, position);
} else if (e.originalEvent.shiftKey) {
this.onVertexMarkerShiftClick(e, vertex, position);
} else if (position >= 1 && position === vertex.getLastIndex() && this.drawing === L.Editable.FORWARD) {
this.finishDrawing();
} else if (position === 0 && this.drawing === L.Editable.BACKWARD && this.activeLatLngs.length >= 2) {
this.finishDrawing();
} else {
this.onVertexRawMarkerClick(e, vertex, position);
}
},
onVertexRawMarkerClick: function (e, vertex, position) {
vertex.remove();
this.editLayer.removeLayer(vertex);
this.feature.redraw();
},
onVertexMarkerCtrlClick: function (e, vertex, position) {
this.feature.fire('editable:vertexctrlclick', {
originalEvent: e.originalEvent,
latlng: e.latlng,
vertex: vertex,
position: position
});
},
onVertexMarkerShiftClick: function (e, vertex, position) {
this.feature.fire('editable:vertexshiftclick', {
originalEvent: e.originalEvent,
latlng: e.latlng,
vertex: vertex,
position: position
});
},
onVertexMarkerAltClick: function (e, vertex, position) {
this.feature.fire('editable:vertexaltclick', {
originalEvent: e.originalEvent,
latlng: e.latlng,
vertex: vertex,
position: position
});
},
addMiddleMarker: function (left, right, latlngs) {
return new this.map.editable.options.middleMarkerClass(left, right, latlngs, this);
},
startDrawingForward: function () {
this.startDrawing();
this.map.editable.attachForwardLineGuide();
},
finishDrawing: function () {
L.Editable.BaseEditor.prototype.finishDrawing.call(this);
this.map.editable.detachForwardLineGuide();
this.map.editable.detachBackwardLineGuide();
this.unsetActiveLatLngs();
delete this.checkConstraints;
},
addLatLng: function (latlng) {
this.setActiveLatLngs(latlng);
if (this.drawing === L.Editable.FORWARD) this.activeLatLngs.push(latlng);
else this.activeLatLngs.unshift(latlng);
this.feature.redraw();
this.addVertexMarker(latlng, this.activeLatLngs);
},
onNewClickHandlerClicked: function (e) {
if (this.checkAddConstraints && !this.checkAddConstraints(e.latlng)) {
return;
}
if (this.drawing === L.Editable.FORWARD) this.map.editable.newPointForward(e.latlng);
else this.map.editable.newPointBackward(e.latlng);
if (!this.map.editable.backwardLineGuide._latlngs[0]) {
this.map.editable.anchorBackwardLineGuide(e.latlng);
}
this.feature.fire('editable:newclick', e);
},
setActiveLatLngs: function (latlng) {
if (!this.activeLatLngs) {
this.activeLatLngs = this.getLatLngs(latlng);
}
},
unsetActiveLatLngs: function () {
delete this.activeLatLngs;
},
onMouseMove: function (e) {
if (this.drawing) {
this.map.editable.moveForwardLineGuide(e.latlng);
this.map.editable.moveBackwardLineGuide(e.latlng);
}
}
});
L.Editable.PolylineEditor = L.Editable.PathEditor.extend({
getLatLngsGroups: function () {
return [this.getLatLngs()];
},
getLatLngs: function () {
return this.feature.getLatLngs();
},
startDrawingBackward: function () {
this.drawing = L.Editable.BACKWARD;
this.startDrawing();
this.map.editable.attachBackwardLineGuide();
},
continueBackward: function () {
this.map.editable.anchorBackwardLineGuide(this.getFirstLatLng());
this.startDrawingBackward();
},
continueForward: function () {
this.map.editable.anchorForwardLineGuide(this.getLastLatLng());
this.startDrawingForward();
},
getLastLatLng: function () {
return this.getLatLngs()[this.getLatLngs().length - 1];
},
getFirstLatLng: function () {
return this.getLatLngs()[0];
}
});
L.Editable.PolygonEditor = L.Editable.PathEditor.extend({
CLOSED: true,
getLatLngsGroups: function () {
var groups = [this.feature._latlngs];
if (this.feature._holes) {
for (var i = 0; i < this.feature._holes.length; i++) {
groups.push(this.feature._holes[i]);
}
}
return groups;
},
startDrawingForward: function () {
L.Editable.PathEditor.prototype.startDrawingForward.call(this);
this.map.editable.attachBackwardLineGuide();
},
finishDrawing: function () {
L.Editable.PathEditor.prototype.finishDrawing.call(this);
this.map.editable.detachBackwardLineGuide();
},
getLatLngs: function (latlng) {
if (latlng) {
var p = this.map.latLngToLayerPoint(latlng);
if (this.feature._latlngs && this.feature._holes && this.feature._containsPoint(p)) {
return this.addNewEmptyHole();
}
}
return this.feature._latlngs;
},
addNewEmptyHole: function () {
var holes = Array();
if (!this.feature._holes) {
this.feature._holes = [];
}
this.feature._holes.push(holes);
return holes;
},
prepareForNewHole: function () {
this.activeLatLngs = this.addNewEmptyHole();
this.checkAddConstraints = this.checkContains;
},
newHole: function () {
this.prepareForNewHole();
this.startDrawingForward();
},
checkContains: function (latlng) {
return this.feature._containsPoint(this.map.latLngToLayerPoint(latlng));
}
});
L.Editable.mergeOptions({
polylineEditorClass: L.Editable.PolylineEditor
});
L.Editable.mergeOptions({
polygonEditorClass: L.Editable.PolygonEditor
});
L.Editable.mergeOptions({
markerEditorClass: L.Editable.MarkerEditor
});
var EditableMixin = {
createEditor: function (map) {
map = map || this._map;
var Klass = this.options.editorClass || this.getEditorClass(map);
return new Klass(map, this);
},
edit: function (secondary) {
return this.createEditor().enable(secondary);
},
endEdit: function () {
if (this.editor) {
this.editor.disable();
delete this.editor;
}
},
toggleEdit: function () {
if (this.editor) {
this.endEdit();
} else {
this.edit();
}
},
connectToMap: function (map) {
return this.addTo(map);
}
};
L.Polyline.include(EditableMixin);
L.Polygon.include(EditableMixin);
L.Marker.include(EditableMixin);
L.Polyline.include({
_containsPoint: function (p, closed) { // Copy-pasted from Leaflet
var i, j, k, len, len2, dist, part,
w = this.options.weight / 2;
if (L.Browser.touch) {
w += 10; // polyline click tolerance on touch devices
}
for (i = 0, len = this._parts.length; i < len; i++) {
part = this._parts[i];
for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
if (!closed && (j === 0)) {
continue;
}
dist = L.LineUtil.pointToSegmentDistance(p, part[k], part[j]);
if (dist <= w) {
return true;
}
}
}
return false;
},
getEditorClass: function (map) {
return map.editable.options.polylineEditorClass;
}
});
L.Polygon.include({
_containsPoint: function (p) { // Copy-pasted from Leaflet
var inside = false,
part, p1, p2,
i, j, k,
len, len2;
// TODO optimization: check if within bounds first
if (L.Polyline.prototype._containsPoint.call(this, p, true)) {
// click on polygon border
return true;
}
// ray casting algorithm for detecting if point is in polygon
for (i = 0, len = this._parts.length; i < len; i++) {
part = this._parts[i];
for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
p1 = part[j];
p2 = part[k];
if (((p1.y > p.y) !== (p2.y > p.y)) &&
(p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) {
inside = !inside;
}
}
}
return inside;
},
getEditorClass: function (map) {
return map.editable.options.polygonEditorClass;
}
});
L.Marker.include({
getEditorClass: function (map) {
return map.editable.options.markerEditorClass;
}
});
var MultiEditableMixin = {
edit: function (e) {
this.eachLayer(function(layer) {
layer.multi = this;
layer.endEdit();
layer.edit(e.layer !== layer);
}, this);
this._map.editable.multi = this;
},
endEdit: function () {
this.eachLayer(function(layer) {
layer.endEdit();
});
},
toggleEdit: function (e) {
if (!e.layer.editor || e.layer.editor.secondary) {
this.edit(e);
} else {
this.endEdit();
}
},
setPrimary: function (primary) {
this.eachLayer(function (layer) {
if (layer === primary) layer.editor.setPrimary();
else layer.editor.setSecondary();
});
},
setSecondary: function (except) {
this.eachLayer(function (layer) {
if (layer !== except) layer.editor.setSecondary();
});
}
};
L.MultiPolygon.include(MultiEditableMixin);
L.MultiPolyline.include(MultiEditableMixin);

17
test/Editable.js Normal file
View File

@ -0,0 +1,17 @@
describe('L.Editable', function() {
before(function () {
this.map = map;
});
after(function () {
});
describe('#init', function() {
it('should be initialized', function() {
assert.ok(this.map.editable);
});
});
});

67
test/MarkerEditor.js Normal file
View File

@ -0,0 +1,67 @@
describe('L.MarkerEditor', function() {
var mouse, marker;
before(function () {
this.map = map;
});
after(function () {
marker.endEdit();
this.map.removeLayer(marker);
});
describe('#startNewPolygon()', function() {
it('should create feature and editor', function() {
marker = this.map.editable.startNewMarker();
assert.ok(this.map.editable.activeEditor);
assert.ok(marker);
});
it('should update marker position on mousemove', function () {
happen.at('mousemove', 200, 200);
var before = marker._latlng;
happen.at('mousemove', 300, 300);
assert.notEqual(before, marker._latlng);
});
it('should set latlng on first click', function () {
happen.at('click', 300, 300);
var before = marker._latlng;
happen.at('mousemove', 400, 400);
assert.equal(before, marker._latlng);
});
});
describe('#disable()', function () {
it('should stop editing on disable() call', function () {
marker.endEdit();
assert.notOk(this.map.editable.activeEditor);
});
});
describe('#enable()', function () {
it('should start editing on enable() call', function () {
marker.edit();
assert.ok(this.map.editable.activeEditor);
});
});
describe('#drag()', function () {
it('should update latlng on marker drag', function (done) {
var before = marker._latlng.lat,
self = this;
happen.drag(300, 299, 350, 350, function () {
assert.notEqual(before, marker._latlng.lat);
done();
});
});
});
});

137
test/PolygonEditor.js Normal file
View File

@ -0,0 +1,137 @@
describe('L.PolygonEditor', function() {
var mouse, polygon;
before(function () {
this.map = map;
});
after(function () {
polygon.editor.disable();
this.map.removeLayer(polygon);
});
describe('#startNewPolygon()', function() {
it('should create feature and editor', function() {
this.map.editable.startNewPolygon();
assert.ok(this.map.editable.activeEditor);
polygon = this.map.editable.activeEditor.feature;
assert.ok(polygon);
assert.notOk(this.map.editable.activeEditor.feature._latlngs.length);
});
it('should create latlng on click', function () {
happen.at('mousemove', 100, 150);
happen.at('click', 100, 150);
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 1);
happen.at('mousemove', 200, 350);
happen.at('click', 200, 350);
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 2);
happen.at('mousemove', 300, 250);
happen.at('click', 300, 250);
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 3);
happen.at('mousemove', 300, 150);
happen.at('click', 300, 150);
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 4);
});
it('should finish shape on last point click', function () {
happen.at('click', 300, 150);
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 4);
});
});
describe('#disable()', function () {
it('should stop editing on disable() call', function () {
polygon.endEdit();
assert.notOk(this.map.editable.activeEditor);
});
});
describe('#enable()', function () {
it('should start editing on enable() call', function () {
polygon.edit();
assert.ok(this.map.editable.activeEditor);
});
});
describe('#dragVertex()', function () {
it('should update latlng on vertex drag', function (done) {
var before = this.map.editable.activeEditor.feature._latlngs[2].lat,
self = this;
happen.drag(300, 250, 310, 260, function () {
assert.notEqual(before, self.map.editable.activeEditor.feature._latlngs[2].lat);
done();
});
});
});
describe('#deleteVertex()', function () {
it('should delete latlng on vertex click', function () {
happen.at('click', 200, 350);
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 3);
});
});
describe('#dragMiddleMarker()', function (done) {
it('should insert new latlng on middle marker click', function (done) {
var first = this.map.editable.activeEditor.feature._latlngs[0],
second = this.map.editable.activeEditor.feature._latlngs[1],
self = this,
fromX = (100 + 310) / 2,
fromY = (150 + 260) / 2;
happen.drag(fromX, fromY, 150, 300, function () {
assert.equal(self.map.editable.activeEditor.feature._latlngs.length, 4);
// New should have been inserted between first and second latlng,
// so second should equal third, and first should not have changed
assert.equal(first, self.map.editable.activeEditor.feature._latlngs[0]);
assert.equal(second, self.map.editable.activeEditor.feature._latlngs[2]);
done();
});
});
});
describe('#newHole', function () {
it('should create new hole on click', function () {
assert.notOk(polygon._holes);
polygon.editor.newHole();
happen.at('mousemove', 150, 170);
happen.at('click', 150, 170);
assert.equal(polygon._holes.length, 1);
assert.equal(polygon._holes[0].length, 1);
happen.at('mousemove', 200, 250);
happen.at('click', 200, 250);
assert.equal(polygon._holes[0].length, 2);
happen.at('mousemove', 250, 250);
happen.at('click', 250, 250);
assert.equal(polygon._holes[0].length, 3);
happen.at('mousemove', 250, 200);
happen.at('click', 250, 200);
assert.equal(polygon._holes[0].length, 4);
});
it('should not create new point when clicking outside', function () {
happen.at('click', 400, 400);
assert.equal(polygon._holes[0].length, 4);
});
it('should finish shape on last point click', function () {
happen.at('click', 250, 200);
happen.at('click', 260, 210);
assert.equal(polygon._holes[0].length, 4);
});
});
});

129
test/PolylineEditor.js Normal file
View File

@ -0,0 +1,129 @@
describe('L.PolylineEditor', function() {
var mouse, polyline;
before(function () {
this.map = map;
});
after(function () {
polyline.editor.disable();
this.map.removeLayer(polyline);
});
describe('#startNewLine()', function() {
it('should create feature and editor', function() {
this.map.editable.startNewLine();
assert.ok(this.map.editable.activeEditor);
polyline = this.map.editable.activeEditor.feature;
assert.ok(polyline);
assert.notOk(this.map.editable.activeEditor.feature._latlngs.length);
});
it('should create latlng on click', function () {
happen.at('mousemove', 100, 150);
happen.at('click', 100, 150);
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 1);
happen.at('mousemove', 200, 350);
happen.at('click', 200, 350);
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 2);
happen.at('mousemove', 300, 250);
happen.at('click', 300, 250);
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 3);
});
it('should finish shape on last point click', function () {
happen.at('click', 300, 250);
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 3);
});
});
describe('#disable()', function () {
it('should stop editing on disable() call', function () {
polyline.endEdit();
assert.notOk(this.map.editable.activeEditor);
});
});
describe('#enable()', function () {
it('should start editing on enable() call', function () {
polyline.edit();
assert.ok(this.map.editable.activeEditor);
});
});
describe('#dragVertex()', function () {
it('should update latlng on vertex drag', function (done) {
var before = this.map.editable.activeEditor.feature._latlngs[1].lat,
self = this;
happen.drag(200, 350, 220, 360, function () {
assert.notEqual(before, self.map.editable.activeEditor.feature._latlngs[1].lat);
done();
});
});
});
describe('#deleteVertex()', function () {
it('should delete latlng on vertex click', function () {
happen.at('click', 300, 250);
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 2);
});
});
describe('#continueForward()', function () {
it('should add new latlng on map click', function () {
this.map.editable.activeEditor.continueForward();
happen.at('mousemove', 400, 400);
happen.at('click', 400, 400);
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 3);
happen.at('click', 400, 400); // Finish shape
happen.at('click', 450, 450); // Click elsewhere on the map
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 3);
});
});
describe('#continueBackward()', function () {
it('should add new latlng on map click', function () {
this.map.editable.activeEditor.continueBackward();
happen.at('mousemove', 400, 100);
happen.at('click', 400, 100);
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 4);
happen.at('click', 400, 100); // Finish shape
happen.at('click', 450, 450); // Click elsewhere on the map
assert.equal(this.map.editable.activeEditor.feature._latlngs.length, 4);
});
});
describe('#dragMiddleMarker()', function (done) {
it('should insert new latlng on middle marker click', function (done) {
var last = this.map.editable.activeEditor.feature._latlngs[3],
third = this.map.editable.activeEditor.feature._latlngs[2],
self = this,
fromX = (400 + 220) / 2,
fromY = (400 + 360) / 2;
happen.drag(fromX, fromY, 300, 440, function () {
assert.equal(self.map.editable.activeEditor.feature._latlngs.length, 5);
// New should have been inserted between third and last latlng,
// so third and last should not have changed
assert.equal(last, self.map.editable.activeEditor.feature._latlngs[4]);
assert.equal(third, self.map.editable.activeEditor.feature._latlngs[2]);
done();
});
});
});
});

38
test/_pre.js Normal file
View File

@ -0,0 +1,38 @@
var qs = function (selector) {return document.querySelector(selector);};
var qsa = function (selector) {return document.querySelectorAll(selector);};
happen.at = function (what, x, y, props) {
this.once(document.elementFromPoint(x, y), L.Util.extend({
type: what,
clientX: x,
clientY: y,
screenX: x,
screenY: y,
which: 1,
button: 1
}, props || {}));
};
happen.drag = function (fromX, fromY, toX, toY, then) {
happen.at('mousemove', fromX, fromY);
happen.at('mousedown', fromX, fromY);
while (fromX <= toX) {
happen.at('mousemove', fromX++, fromY);
}
var moveX = function () {
if (fromX <= toX) {
happen.at('mousemove', fromX++, fromY);
window.setTimeout(moveX, 5);
}
};
moveX();
var moveY = function () {
if (fromY <= toY) {
happen.at('mousemove', fromX, fromY++);
window.setTimeout(moveY, 5);
}
};
moveY();
window.setTimeout(function () {
happen.at('mouseup', toX, toY);
if (then) then();
}, 1000);
};

62
test/index.html Normal file
View File

@ -0,0 +1,62 @@
<html>
<head>
<title>Leaflet.Editable Tests</title>
<meta charset="utf-8">
<script src="../node_modules/leaflet/dist/leaflet-src.js"></script>
<script src="../src/Leaflet.Editable.js"></script>
<link rel="stylesheet" href="../node_modules/leaflet/dist/leaflet.css" />
<script src="../node_modules/mocha/mocha.js"></script>
<script src="../node_modules/happen/happen.js"></script>
<script src="../node_modules/chai/chai.js"></script>
<script src="../node_modules/effroi/dist/effroi.js"></script>
<link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
<script>mocha.setup({ui: 'bdd', bail: true})</script>
<script type="text/javascript">
expect = chai.expect;
assert = chai.assert;
</script>
<script src="./_pre.js"></script>
<script src="./Editable.js"></script>
<script src="./PolylineEditor.js"></script>
<script src="./PolygonEditor.js"></script>
<script src="./MarkerEditor.js"></script>
<style type="text/css">
#mocha {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 10000;
background-color: white;
box-shadow: 0px 0px 8px 0px black;
overflow-y: auto;
display: none;
}
#mocha-stats {
position: absolute;
}
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; right: 0; left: 0; width:100%; }
</style>
</head>
<body>
<div id="mocha"></div>
<div id="map"></div>
<script>
var startPoint = [43.1249, 1.254],
map = L.map('map').setView(startPoint, 16),
tilelayer = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom: 19, attribution: 'Data \u00a9 <a href="http://www.openstreetmap.org/copyright"> OpenStreetMap Contributors </a>'}).addTo(map);
var runner = mocha.run(function (failures) {
if (window.location.hash !== '#debug') qs('#mocha').style.display = 'block';
console.log(failures);
});
runner.on('fail', function(test, err) {
console.log(test.title, test.err);
console.log(test.err.expected, test.err.actual);
console.log(test.err.stack);
});
</script>
</body>
</html>