diff --git a/README.md b/README.md
index a7cfe8d..2ad33db 100644
--- a/README.md
+++ b/README.md
@@ -111,8 +111,8 @@ Copyright (c) 2013, Turistforeningen, Hans Kristian Flaatten. All rights reserve
Copyright (c) 2013 Felix Bache; [MIT License](https://github.com/MrMufflon/Leaflet.Elevation/blob/master/LICENSE)
* [D3.js](https://github.com/mbostock/d3)
Copyright (c) 2013, Michael Bostock. All rights reserved.; [3-clause BSD License](https://github.com/mbostock/d3/blob/master/LICENSE)
-* [Leaflet.draw](https://github.com/Leaflet/Leaflet.draw)
-Copyright 2012 Jacob Toye; [MIT License](https://github.com/Leaflet/Leaflet.draw/blob/master/MIT-LICENCE.txt)
+* [Leaflet.Editable](https://github.com/Leaflet/Leaflet.Editable)
+Yohan Boniface; WTFPL licence
* [Leaflet Control Geocoder](https://github.com/perliedman/leaflet-control-geocoder)
Copyright (c) 2012 [sa3m](https://github.com/sa3m), Copyright (c) 2013 Per Liedman; [2-clause BSD License](https://github.com/perliedman/leaflet-control-geocoder/blob/master/LICENSE)
* [leaflet-plugins](https://github.com/shramov/leaflet-plugins)
diff --git a/bower.json b/bower.json
index 466c829..faab27b 100644
--- a/bower.json
+++ b/bower.json
@@ -15,7 +15,6 @@
"leaflet-routing": "nrenner/leaflet-routing#leaflet-1.0",
"async": "~0.9.2",
"d3": "~3.5.5",
- "leaflet.draw": "~0.4.9",
"bootstrap": "4.0.0-alpha.5",
"DataTables": "~1.10.13",
"leaflet.elevation": "MrMufflon/Leaflet.Elevation#master",
@@ -29,7 +28,8 @@
"font-awesome": "^4.7.0",
"bootstrap-select": "hugdx/bootstrap-select#patch-1",
"leaflet-sidebar": "^0.1.9",
- "autosize": "^3.0.20"
+ "autosize": "^3.0.20",
+ "leaflet.editable": "^1.1.0"
},
"overrides": {
"leaflet": {
@@ -53,15 +53,6 @@
"src/L.Routing.Edit.js"
]
},
- "leaflet.draw": {
- "main": [
- "dist/leaflet.draw-src.js",
- "dist/leaflet.draw.css",
- "dist/images/*.png",
- "dist/images/*.svg"
- ],
- "dependencies": null
- },
"bootstrap-select": {
"main": [
"js/bootstrap-select.js",
diff --git a/css/style.css b/css/style.css
index 7c6b033..b4c32bb 100644
--- a/css/style.css
+++ b/css/style.css
@@ -226,3 +226,40 @@ table.dataTable.display tbody tr.odd:hover,
table.dataTable.display tbody tr.even:hover {
background-color: rgba(255,255,0,0.2);
}
+
+/*
+ * No-go areas
+ */
+
+.nogo-delete-marker {
+ text-align: center;
+ line-height: 16px;
+ font-size: 11px;
+ border-radius: 8px;
+}
+.leaflet-touch .nogo-delete-marker {
+ border-radius: 12px;
+ line-height: 24px;
+}
+
+/* tooltip */
+
+.editing-tooltip,
+.editing-tooltip-create {
+ color: #fff;
+ background-color: rgba(0,0,0,0.7);
+ /* for direction arrows that inherit */
+ border-color: rgba(0,0,0,0.7);
+ /* no border but still set a color for direction arrows */
+ border-width: 0px;
+}
+.editing-tooltip-create {
+ /* no (invisible) direction arrow for cursor tooltip */
+ border-color: transparent;
+}
+.leaflet-tooltip-bottom:before {
+ border-bottom-color: inherit;
+}
+.leaflet-tooltip-right:before {
+ border-right-color: inherit;
+}
diff --git a/js/Browser.js b/js/Browser.js
index 0c71316..1d7534d 100644
--- a/js/Browser.js
+++ b/js/Browser.js
@@ -13,12 +13,13 @@
return result;
}()),
- touchScreenDetectable = touchScreen !== null;
-
-
+ touchScreenDetectable = touchScreen !== null,
+ touch = touchScreenDetectable ? touchScreen : L.Browser.touch;
+
BR.Browser = {
touchScreen: touchScreen,
- touchScreenDetectable: touchScreenDetectable
+ touchScreenDetectable: touchScreenDetectable,
+ touch: touch
};
}());
\ No newline at end of file
diff --git a/js/index.js b/js/index.js
index 1c66415..1f37894 100644
--- a/js/index.js
+++ b/js/index.js
@@ -235,6 +235,8 @@
map.addControl(sidebar);
nogos.addTo(map);
+ nogos.preventRoutePointOnCreate(routing);
+
map.addControl(new BR.OpacitySlider({
callback: L.bind(routing.setOpacity, routing)
}));
diff --git a/js/plugin/NogoAreas.js b/js/plugin/NogoAreas.js
index d7ff4a3..e3ae993 100644
--- a/js/plugin/NogoAreas.js
+++ b/js/plugin/NogoAreas.js
@@ -1,55 +1,115 @@
-L.drawLocal.draw.toolbar.buttons.circle = 'Draw no-go area (circle)';
-L.drawLocal.edit.toolbar.buttons.edit = 'Edit no-go areas';
-L.drawLocal.edit.toolbar.buttons.remove = 'Delete no-go areas';
-
-BR.NogoAreas = L.Control.Draw.extend({
- initialize: function () {
- this.drawnItems = new L.FeatureGroup();
+BR.NogoAreas = L.Control.extend({
+ statics: {
+ MSG_BUTTON: 'Draw no-go area (circle)',
+ MSG_BUTTON_CANCEL: 'Cancel drawing no-go area',
+ MSG_CREATE: 'Click and drag to draw circle',
+ MSG_DISABLED: 'Click to edit',
+ MSG_ENABLED: '□ = move / resize, = delete,
click circle to quit editing',
+ STATE_CREATE: 'no-go-create',
+ STATE_CANCEL: 'cancel-no-go-create'
+ },
- L.Control.Draw.prototype.initialize.call(this, {
- draw: {
- position: 'topleft',
- polyline: false,
- polygon: false,
- circle: true,
- rectangle: false,
- marker: false
- },
- edit: {
- featureGroup: this.drawnItems,
- //edit: false,
- edit: {
- selectedPathOptions: {
- //opacity: 0.8
- }
- },
- remove: true
- }
- });
+ style: {
+ color: '#f06eaa',
+ weight: 4,
+ opacity: 0.5,
+ fillColor: null, //same as color by default
+ fillOpacity: 0.2,
+ dashArray: null
+ },
+
+ editStyle: {
+ color: '#fe57a1',
+ opacity: 0.6,
+ dashArray: '10, 10',
+ fillOpacity: 0.1
+ },
+
+ initialize: function () {
+ this._wasRouteDrawing = false;
},
onAdd: function (map) {
- map.addLayer(this.drawnItems);
+ var self = this;
+
+ this.drawnItems = new L.FeatureGroup().addTo(map);
+ this.drawnItems.on('click', function(e) {
+ L.DomEvent.stop(e);
+ e.layer.toggleEdit();
+ });
- map.on('draw:created', function (e) {
- var layer = e.layer;
- this.drawnItems.addLayer(layer);
+ var editTools = this.editTools = map.editTools = new L.Editable(map, {
+ circleEditorClass: BR.DeletableCircleEditor,
+ // FeatureGroup instead of LayerGroup to propagate events to members
+ editLayer: new L.FeatureGroup().addTo(map),
+ featuresLayer: this.drawnItems
+ });
+
+ var button = L.easyButton({
+ states: [{
+ stateName: BR.NogoAreas.STATE_CREATE,
+ icon: 'fa-ban',
+ title: BR.NogoAreas.MSG_BUTTON,
+ onClick: function (control) {
+ // initial radius of 0 to detect click, see DeletableCircleEditor.onDrawingMouseUp
+ var opts = L.extend({radius: 0}, self.style);
+ editTools.startCircle(null, opts);
+
+ control.state('cancel-no-go-create');
+ }
+ }, {
+ stateName: BR.NogoAreas.STATE_CANCEL,
+ icon: 'fa-ban active',
+ title: BR.NogoAreas.MSG_BUTTON_CANCEL,
+ onClick: function (control) {
+ editTools.stopDrawing();
+ control.state('no-go-create');
+ }
+ }]
+ }).addTo(map);
+
+ this.editTools.on('editable:drawing:end', function (e) {
+ button.state(BR.NogoAreas.STATE_CREATE);
+
+ setTimeout(L.bind(function () {
+ // turn editing off after create; async to still fire 'editable:vertex:dragend'
+ e.layer.disableEdit();
+ }, this), 0);
+ }, this);
+
+ this.editTools.on('editable:vertex:dragend editable:deleted', function (e) {
this._fireUpdate();
}, this);
- map.on('draw:editstart', function (e) {
- this.drawnItems.eachLayer(function (layer) {
- layer.on('edit', function(e) {
- this._fireUpdate();
- }, this);
- }, this);
+ this.editTools.on('editable:enable', function (e) {
+ e.layer.setStyle(this.editStyle);
+ }, this);
+ this.editTools.on('editable:disable', function (e) {
+ e.layer.setStyle(this.style);
}, this);
- map.on('draw:deleted', function (e) {
- this._fireUpdate();
+ this.tooltip = new BR.EditingTooltip(map, editTools, button);
+ this.tooltip.enable();
+
+ // dummy, no own representation, delegating to EasyButton
+ return L.DomUtil.create('div');
+ },
+
+ // prevent route waypoint added after circle create (map click after up)
+ preventRoutePointOnCreate: function(routing) {
+ this.editTools.on('editable:drawing:start', function (e) {
+ this._wasRouteDrawing = routing.isDrawing();
+ routing.draw(false);
}, this);
- return L.Control.Draw.prototype.onAdd.call(this, map);
+ // after create
+ this.editTools.on('editable:drawing:end', function (e) {
+ if (this._wasRouteDrawing) {
+ setTimeout(function () {
+ routing.draw(true);
+ }, 0);
+ }
+ }, this);
},
getOptions: function() {
@@ -60,17 +120,292 @@ BR.NogoAreas = L.Control.Draw.extend({
setOptions: function(options) {
var nogos = options.nogos;
- this.drawnItems.clearLayers();
+ this._clear();
if (nogos) {
for (var i = 0; i < nogos.length; i++) {
+ nogos[i].setStyle(this.style);
this.drawnItems.addLayer(nogos[i]);
}
}
},
+ _clear: function () {
+ this.drawnItems.clearLayers();
+ },
+
+ clear: function () {
+ this._clear();
+ this._fireUpdate();
+ },
+
_fireUpdate: function () {
this.fire('update', {options: this.getOptions()});
+ },
+
+ getFeatureGroup: function() {
+ return this.drawnItems;
+ },
+
+ getEditGroup: function() {
+ return this.editTools.editLayer;
}
});
-BR.NogoAreas.include(L.Mixin.Events);
\ No newline at end of file
+BR.NogoAreas.include(L.Mixin.Events);
+
+
+L.Editable.prototype.createVertexIcon = function (options) {
+ return BR.Browser.touch ? new L.Editable.TouchVertexIcon(options) : new L.Editable.VertexIcon(options);
+};
+
+
+BR.EditingTooltip = L.Handler.extend({
+ options: {
+ closeTimeout: 2000
+ },
+
+ initialize: function (map, editTools, button) {
+ this.map = map;
+ this.editTools = editTools;
+ this.button = button;
+ },
+
+ addHooks: function () {
+ // hack: listen to EasyButton click (instead of editable:drawing:start),
+ // to get mouse position from event for initial tooltip location
+ L.DomEvent.addListener(this.button.button, 'click', this._addCreate, this);
+
+ this.editTools.featuresLayer.on('layeradd', this._bind, this);
+
+ this.editTools.on('editable:drawing:end', this._postCreate, this);
+ this.editTools.on('editable:enable', this._enable, this);
+ this.editTools.on('editable:disable', this._disable, this);
+ },
+
+ removeHooks: function () {
+ L.DomEvent.removeListener(this.button.button, 'click', this._addCreate, this);
+
+ this.editTools.featuresLayer.off('layeradd', this._bind, this);
+
+ this.editTools.off('editable:drawing:end', this._postCreate, this);
+ this.editTools.off('editable:enable', this._enable, this);
+ this.editTools.off('editable:disable', this._disable, this);
+ },
+
+ _bind: function (e) {
+ // Position tooltip at bottom of circle, less distracting than
+ // sticky with cursor or at center.
+
+ var layer = e.layer;
+ layer.bindTooltip(BR.NogoAreas.MSG_DISABLED, {
+ direction: 'bottom',
+ className: 'editing-tooltip'
+ });
+
+ // Override to set position to south instead of center (circle latlng);
+ // works better with zooming than updating offset to match radius
+ layer.openTooltip = function (layer, latlng) {
+ if (!latlng && layer instanceof L.Layer) {
+ latlng = L.latLng(layer.getBounds().getSouth(), layer.getLatLng().lng);
+ }
+ L.Layer.prototype.openTooltip.call(this, layer, latlng);
+ };
+ },
+
+ _addCreate: function (e) {
+ // button cancel
+ if (!this.editTools.drawing()) return;
+
+ var initialLatLng = this.map.mouseEventToLatLng(e);
+ var tooltip = L.tooltip({
+ // no effect with map tooltip
+ sticky: true,
+ // offset wrong with 'auto' when switching direction
+ direction: 'right',
+ offset: L.point(5, 28),
+ className: 'editing-tooltip-create'
+ });
+
+ // self-reference hack for _moveTooltip, as tooltip is not bound to layer
+ tooltip._tooltip = tooltip;
+
+ // simulate sticky feature (follow mouse) for map tooltip without layer
+ var onOffMove = function (e) {
+ var onOff = (e.type === 'tooltipclose') ? 'off' : 'on';
+ this._map[onOff]('mousemove', this._moveTooltip, this);
+ }
+ this.map.on('tooltipopen', onOffMove, tooltip);
+ this.map.on('tooltipclose', onOffMove, tooltip);
+
+ var onTooltipRemove = function (e) {
+ this.map.off('tooltipopen', onOffMove, e.tooltip);
+ this.map.off('tooltipclose', onOffMove, e.tooltip);
+ this.map.off('tooltipclose', onTooltipRemove, this);
+ e.tooltip._tooltip = null;
+ }
+ this.map.on('tooltipclose', onTooltipRemove, this);
+
+ tooltip.setTooltipContent(BR.NogoAreas.MSG_CREATE);
+ this.map.openTooltip(tooltip, initialLatLng);
+
+ var closeTooltip = function () {
+ this.map.closeTooltip(tooltip);
+ };
+ this.editTools.once('editable:editing editable:drawing:cancel', closeTooltip, this);
+
+ if (BR.Browser.touch) {
+ // can't move with cursor on touch devices, so show at start pos for a few seconds
+ setTimeout(L.bind(closeTooltip, this), this.options.closeTimeout);
+ }
+ },
+
+ _postCreate: function () {
+ // editing is disabled by another handler, tooltip won't stay open before
+ this.editTools.once('editable:disable', function (e) {
+
+ // show for a few seconds, as mouse often not hovering circle after create
+ e.layer.openTooltip(e.layer);
+ setTimeout(function () {
+ e.layer.closeTooltip();
+ }, this.options.closeTimeout);
+ }, this);
+ },
+
+ _enable: function (e) {
+ e.layer.setTooltipContent(BR.NogoAreas.MSG_ENABLED);
+
+ this.editTools.once('editable:editing', function(e) {
+ e.layer.closeTooltip();
+ }, this);
+ },
+
+ _disable: function (e) {
+ e.layer.setTooltipContent(BR.NogoAreas.MSG_DISABLED);
+
+ setTimeout(function () {
+ e.layer.closeTooltip();
+ }, this.options.closeTimeout);
+ }
+});
+
+
+BR.DeletableCircleEditor = L.Editable.CircleEditor.extend({
+
+ _computeDeleteLatLng: function () {
+ // While circle is not added to the map, _radius is not set.
+ var delta = (this.feature._radius || this.feature._mRadius) * Math.cos(Math.PI / 4),
+ point = this.map.project(this.feature._latlng);
+ return this.map.unproject([point.x - delta, point.y - delta]);
+ },
+
+ _updateDeleteLatLng: function () {
+ this._deleteLatLng.update(this._computeDeleteLatLng());
+ this._deleteLatLng.__vertex.update();
+ },
+
+ _addDeleteMarker: function() {
+ if (!this.enabled()) return;
+ this._deleteLatLng = this._computeDeleteLatLng();
+ return new BR.DeleteMarker(this._deleteLatLng, this);
+ },
+
+ _delete: function() {
+ this.disable();
+ this.tools.featuresLayer.removeLayer(this.feature);
+ },
+
+ delete: function() {
+ this._delete();
+ this.fireAndForward('editable:deleted');
+ },
+
+ initialize: function (map, feature, options) {
+ L.Editable.CircleEditor.prototype.initialize.call(this, map, feature, options);
+ this._deleteLatLng = this._computeDeleteLatLng();
+
+ // FeatureGroup instead of LayerGroup to propagate events to members
+ this.editLayer = new L.FeatureGroup();
+ },
+
+ addHooks: function () {
+ L.Editable.CircleEditor.prototype.addHooks.call(this);
+ if (this.feature) {
+ this._addDeleteMarker();
+ }
+ return this;
+ },
+
+ reset: function () {
+ L.Editable.CircleEditor.prototype.reset.call(this);
+ this._addDeleteMarker();
+ },
+
+ onDrawingMouseDown: function (e) {
+ this._deleteLatLng.update(e.latlng);
+ L.Editable.CircleEditor.prototype.onDrawingMouseDown.call(this, e);
+ },
+
+ // override to cancel/remove created circle when added by click instead of drag, because:
+ // - without resize, edit handles stacked on top of each other
+ // - makes event handling more complicated (editable:vertex:dragend not called)
+ onDrawingMouseUp: function (e) {
+ if (this.feature.getRadius() > 0) {
+ this.commitDrawing(e);
+ } else {
+ this.cancelDrawing(e);
+ this._delete();
+ }
+ e.originalEvent._simulated = false;
+ L.Editable.PathEditor.prototype.onDrawingMouseUp.call(this, e);
+ },
+
+ onVertexMarkerDrag: function (e) {
+ this._updateDeleteLatLng();
+ L.Editable.CircleEditor.prototype.onVertexMarkerDrag.call(this, e);
+ }
+
+});
+
+
+BR.DeleteMarker = L.Marker.extend({
+
+ options: {
+ draggable: false,
+ icon: L.divIcon({
+ iconSize: BR.Browser.touch ? new L.Point(24, 24) : new L.Point(16, 16),
+ className: 'leaflet-div-icon fa fa-trash-o nogo-delete-marker'
+ })
+ },
+
+ initialize: function (latlng, editor, options) {
+ // derived from L.Editable.VertexMarker.initialize
+
+ // We don't use this._latlng, because on drag Leaflet replace it while
+ // we want to keep reference.
+ this.latlng = latlng;
+ this.editor = editor;
+ L.Marker.prototype.initialize.call(this, latlng, options);
+
+ this.latlng.__vertex = this;
+ this.editor.editLayer.addLayer(this);
+
+ // to keep small circles editable, make sure delete button is below drag handle
+ // (not using "+ 1" to place at bottom of other vertex markers)
+ this.setZIndexOffset(editor.tools._lastZIndex);
+ },
+
+ onAdd: function (map) {
+ L.Marker.prototype.onAdd.call(this, map);
+ this.on('click', this.onClick);
+ },
+
+ onRemove: function (map) {
+ delete this.latlng.__vertex;
+ this.off('click', this.onClick);
+ L.Marker.prototype.onRemove.call(this, map);
+ },
+
+ onClick: function (e) {
+ this.editor.delete();
+ }
+});
diff --git a/js/plugin/Routing.js b/js/plugin/Routing.js
index fca3b55..4f45c29 100644
--- a/js/plugin/Routing.js
+++ b/js/plugin/Routing.js
@@ -295,4 +295,8 @@ BR.Routing = L.Routing.extend({
L.Routing.prototype._keyupListener.call(this, e);
}
}
+
+ ,isDrawing: function () {
+ return this._draw._enabled;
+ }
});