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; + } });