diff --git a/cadasta/core/static/css/leaflet.toolbar.css b/cadasta/core/static/css/leaflet.toolbar.css
new file mode 100644
index 000000000..2846953f7
--- /dev/null
+++ b/cadasta/core/static/css/leaflet.toolbar.css
@@ -0,0 +1 @@
+.leaflet-toolbar-0{list-style:none;padding-left:0;box-shadow:0 1px 5px rgba(0,0,0,.65)}.leaflet-toolbar-0>li{position:relative}.leaflet-toolbar-0>li>.leaflet-toolbar-icon{display:block;width:26px;height:26px;line-height:26px;margin-right:0;padding-right:0;border-right:0;text-align:center;text-decoration:none;background-color:#fff}.leaflet-toolbar-0>li>.leaflet-toolbar-icon:hover{background-color:#f4f4f4}.leaflet-toolbar-0 .leaflet-toolbar-1{display:none;list-style:none}.leaflet-toolbar-tip-container{margin:0 auto;height:12px;position:relative;overflow:hidden}.leaflet-toolbar-tip{width:12px;height:12px;margin:-6px auto 0;background-color:#fff;box-shadow:0 1px 5px rgba(0,0,0,.65);-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.leaflet-control-toolbar>li>.leaflet-toolbar-icon{border-bottom:1px solid #ccc}.leaflet-control-toolbar>li:first-child>.leaflet-toolbar-icon{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-control-toolbar>li:last-child>.leaflet-toolbar-icon{border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom-width:0}.leaflet-control-toolbar .leaflet-toolbar-1{margin:0;padding:0;position:absolute;left:26px;top:0;white-space:nowrap;height:26px}.leaflet-control-toolbar .leaflet-toolbar-1>li{display:inline-block}.leaflet-control-toolbar .leaflet-toolbar-1>li>.leaflet-toolbar-icon{display:block;background-color:#919187;border-left:1px solid #aaa;color:#fff;font:11px/19px "Helvetica Neue",Arial,Helvetica,sans-serif;line-height:26px;text-decoration:none;padding-left:10px;padding-right:10px;height:26px}.leaflet-control-toolbar .leaflet-toolbar-1>li>.leaflet-toolbar-icon:hover{background-color:#a0a098}.leaflet-popup-toolbar{position:relative}.leaflet-popup-toolbar>li{float:left}.leaflet-popup-toolbar>li:first-child>.leaflet-toolbar-icon{border-top-left-radius:4px;border-bottom-left-radius:4px}.leaflet-popup-toolbar>li:last-child>.leaflet-toolbar-icon{border-top-right-radius:4px;border-bottom-right-radius:4px;border-bottom-width:0}.leaflet-popup-toolbar .leaflet-toolbar-1{position:absolute;top:26px;left:0;padding-left:0}.leaflet-popup-toolbar .leaflet-toolbar-1>li>.leaflet-toolbar-icon{position:relative;float:left;width:26px;height:26px}
\ No newline at end of file
diff --git a/cadasta/core/static/css/main.scss b/cadasta/core/static/css/main.scss
index e9c5eb3c4..e0358650b 100644
--- a/cadasta/core/static/css/main.scss
+++ b/cadasta/core/static/css/main.scss
@@ -226,7 +226,7 @@ main.container-fluid {
}
#project-single #page-content main.show-sidebar,
-#organization-single #page-content main.show-sidebar {
+#organization-single #page-content main.show-sidebar {
margin-left: 100px;
}
@@ -235,7 +235,7 @@ main.container-fluid {
padding-bottom: 0;
}
#project-single #page-content main,
- #organization-single #page-content main,
+ #organization-single #page-content main,
#project-single #page-content main.show-sidebar,
#organization-single #page-content main.show-sidebar,
#organization-single #page-content main {
@@ -244,6 +244,257 @@ main.container-fluid {
}
}
+.content-single {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ z-index: 900;
+ padding: 0;
+ overflow: hidden;
+ overflow-y: scroll;
+ height: 100%;
+ height: -webkit-calc(100% - 30px);
+ height: -moz-calc(100% - 30px);
+ height: calc(100% - 30px);
+ > .row { // for map to expand need height
+ height: 100%;
+ margin-right: 0;
+ }
+ h2 {
+ color: $brand-darkblue;
+ text-transform: uppercase;
+ margin-top: 0;
+ padding-bottom: 6px;
+ text-shadow: none;
+ &.short { // provides room for admin buttons on right
+ width: 70%;
+ }
+ a {
+ color: #0e305e;
+ text-decoration: none;
+ }
+ a:hover {
+ color: #2e51a3;
+ }
+ }
+ h3 {
+ font-family: $font-family-sans-serif-alt;
+ text-transform: uppercase;
+ font-weight: 300;
+ letter-spacing: 0;
+ color: #0e305e;
+ }
+ h4 span.small { // small entity title above link in results tables
+ font-size: 11px;
+ color: $gray-medium;
+ display: block;
+ text-transform: uppercase;
+ padding-bottom: 2px;
+ font-weight: normal;
+ letter-spacing: 0;
+ }
+ .divider-thick {
+ border-top: solid 7px $table-border-color;
+ margin: 20px auto;
+ }
+ .btn-block {
+ margin: 24px auto;
+ }
+ .page-title {
+ padding-bottom: 0;
+ padding-top: 0;
+ .top-btn{
+ margin-top: 0;
+ }
+ }
+ .map { // main panel map
+ height: 100%;
+ min-height: 100%;
+ padding-right: 0;
+ #project-map, #id_extent_map, #id_extents_extent_map {
+ height: 100%;
+ min-height: 100%;
+ }
+ }
+ #id_geometry_map {
+ height: 100%;
+ min-height: 100%;
+ }
+ .main-text {
+ padding: 30px 40px 50px 50px;
+ }
+ .detail { // right panel detail
+ height: 100%;
+ min-height: 100%;
+ position: relative;
+ background: #fff;
+ margin: 0 auto;
+ padding: 24px;
+ overflow-x: hidden;
+ overflow-y: auto;
+ -webkit-box-shadow: 0 0 6px rgba(0,0,0,.3);
+ -moz-box-shadow: 0 0 6px rgba(0,0,0,.3);
+ box-shadow: 0 0 6px rgba(0,0,0,.3);
+ z-index: 300;
+ .more-menu {
+ width: auto !important;
+ }
+ .org-logo { // large org logo
+ margin: 10px auto 20px;
+ float: none;
+ text-align: center;
+ img {
+ width: 90%;
+ max-width: 200px;
+ }
+ }
+ ul.list-divider li { // lists with lines between
+ border-bottom: 1px solid $table-border-color;
+ padding: 12px 2px;
+ &:last-child {
+ border-bottom: 0;
+ padding-bottom: 6px;
+ }
+ &.linked:hover {
+ cursor: pointer;
+ background-color: $table-bg-hover;
+ }
+ }
+ .nav-tabs {
+ li > a {
+ font-size: 13px;
+ padding: 10px;
+ }
+ li.active > a, li.active > a:hover, li.active > a:focus {
+ border-color: $table-border-color;
+ border-bottom-color: transparent;
+ }
+ }
+ section > p:first-child {
+ padding-top: 20px;
+ }
+ h4 {
+ font-family: $font-family-sans-serif-alt;
+ text-transform: uppercase;
+ opacity: 0.8;
+ letter-spacing: 0;
+ padding-top: 10px;
+ }
+ .glyphicon-globe, .glyphicon-envelope, .glyphicon-earphone { // url and contacts in overview
+ opacity: 0.7;
+ padding-right: 12px;
+ }
+ dl.contacts { // contacts in overview
+ dt {
+ border-top: dotted 1px $table-border-color;
+ padding-top: 10px;
+ padding-bottom: 6px;
+ padding-left: 30px;
+ &:first-child {
+ border-top: none;
+ padding-top: 0;
+ }
+ }
+ dd {
+ padding-bottom: 10px;
+ padding-left: 30px;
+ a {
+ display: block;
+ font-size: 13px;
+ padding-bottom: 4px;
+ }
+ }
+ }
+ }
+ .row-height .detail { // columns fixed to match heights like org overview
+ padding-top: 0;
+ }
+ .detail-edit { // edit on right
+ background: $body-bg;
+ .panel-footer {
+ background: transparent;
+ }
+ }
+ .panel { // content box in main panel
+ border: 1px solid $table-border-color;
+ clear: both;
+ .panel-heading {
+ background: transparent;
+ h3 {
+ margin: 4px auto;
+ }
+ }
+ .panel-body {
+ > h3 {
+ margin-bottom: 10px;
+ margin-top: 4px;
+ padding-bottom: 10px;
+ border-bottom: solid 1px $gray-light;
+ }
+ .top-add {
+ margin-bottom: -30px;
+ }
+ }
+ .panel-footer {
+ background: lighten($body-bg, 2%);
+ }
+ .panel-buttons { // holds buttons at bottom of panels containing forms
+ padding: 20px 15px;
+ .btn {
+ min-width: 110px;
+ margin-right: 20px;
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+ }
+ .nav-tabs {
+ margin-bottom: 20px;
+ }
+}
+
+body.map .content-single {
+ overflow-y: hidden;
+}
+
+@media (max-width: $screen-sm-max) {
+ .content-single {
+ position: relative;
+ .map {
+ height: 360px;
+ min-height: 360px;
+ }
+ .main-text {
+ padding: 15px;
+ }
+ }
+}
+
+@media (max-width: $screen-xs-max) {
+ .content-single {
+ .panel {
+ .panel-buttons .btn {
+ width: 100%;
+ margin-bottom: 10px;
+ margin-right: 0;
+ }
+ .panel-body {
+ font-size: 12px;
+ }
+ }
+ .detail {
+ .nav-tabs li > a {
+ font-size: 12px;
+ }
+ .tab-content {
+ font-size: 12px;
+ }
+ }
+ }
+}
+
/* =Table for sort and filter
-------------------------------------------------------------- */
@@ -428,7 +679,7 @@ table#select-list {
font-size: 12px;
}
.form-control {
- padding: 6px;
+ padding: 6px;
}
}
diff --git a/cadasta/core/static/css/maps.scss b/cadasta/core/static/css/maps.scss
index 9eb631cee..4497d4715 100644
--- a/cadasta/core/static/css/maps.scss
+++ b/cadasta/core/static/css/maps.scss
@@ -1,5 +1,5 @@
-#mapid {
- height: 100%;
+#mapid {
+ height: 100%;
z-index: 1;
}
@@ -116,3 +116,77 @@ label[for="id_extents-location"] {
display: none;
}
+/* =LocationEditor
+-------------------------------------------------------------- */
+
+.leaflet-toolbar-icon {
+ cursor: pointer;
+ cursor: hand;
+}
+
+.cadasta-toolbar {
+ color: #000 !important;
+ background-image: url('/static/img/spritesheet.png');
+ background-repeat: no-repeat;
+}
+
+.draw-polygon {
+ background-position: -31px -2px;
+}
+
+.draw-polyline {
+ background-position: -2px -2px;
+}
+
+.draw-rectangle {
+ background-position: -62px -2px;
+}
+
+.draw-marker {
+ background-position: -122px -2px;
+}
+
+.edit-action span, .delete-action span {
+ color: #464646 !important;
+}
+
+.leaflet-subtoolbar {
+ height: 28px !important;
+}
+
+.leaflet-subtoolbar li:last-child a {
+ border-radius: 0px 4px 4px 0px;
+}
+
+.leaflet-control-toolbar .smap-edit-disable {
+ color: #D8D8D8 !important;
+}
+
+.editor-tooltip {
+ display: inline;
+ position: absolute;
+ background: #666;
+ color: white;
+ opacity: 0.8;
+ padding: 5px;
+ border: 1px solid #999;
+ font-family: sans-serif;
+ line-height: 15px;
+ font-size: 12px;
+ z-index: 1000;
+ overflow: visible;
+ border-radius: 5px;
+ margin-left: 20px;
+ margin-top: -30px;
+ white-space: nowrap;
+}
+
+.editor-tooltip:before {
+ border-right: 7px solid #666;
+ border-top: 7px solid transparent;
+ border-bottom: 7px solid transparent;
+ content: '';
+ position: absolute;
+ top: 7px;
+ left: -6px;
+}
diff --git a/cadasta/core/static/img/spritesheet.png b/cadasta/core/static/img/spritesheet.png
new file mode 100644
index 000000000..f7035a1cb
Binary files /dev/null and b/cadasta/core/static/img/spritesheet.png differ
diff --git a/cadasta/core/static/js/smap/L.TileLayer.GeoJSON.js b/cadasta/core/static/js/smap/L.TileLayer.GeoJSON.js
index 977d916c0..4b047d7bb 100644
--- a/cadasta/core/static/js/smap/L.TileLayer.GeoJSON.js
+++ b/cadasta/core/static/js/smap/L.TileLayer.GeoJSON.js
@@ -1,6 +1,6 @@
// set up webworker object.
var WebWorker = {};
-WebWorker.addTileData = function(data, callback) {
+WebWorker.addTileData = function (data, callback) {
var features = data.geojson.features;
var new_layer = [];
if (features) {
@@ -24,7 +24,7 @@ L.TileLayer.Ajax = L.TileLayer.extend({
_tiles: {},
_ticker: 1,
_original_request_len: null,
- _addTile: function(tilePoint) {
+ _addTile: function (tilePoint) {
var tile = {
datum: null,
processed: false
@@ -41,8 +41,8 @@ L.TileLayer.Ajax = L.TileLayer.extend({
this._loadTile(tile, tilePoint);
},
// XMLHttpRequest handler; closure over the XHR object, the layer, and the tile
- _xhrHandler: function(req, layer, tile, tilePoint) {
- return function() {
+ _xhrHandler: function (req, layer, tile, tilePoint) {
+ return function () {
if (req.readyState !== 4) {
return;
}
@@ -56,7 +56,7 @@ L.TileLayer.Ajax = L.TileLayer.extend({
};
},
// Load the requested tile via AJAX
- _loadTile: function(tile, tilePoint) {
+ _loadTile: function (tile, tilePoint) {
if (tilePoint.x < 0 || tilePoint.y < 0) {
return;
}
@@ -69,7 +69,7 @@ L.TileLayer.Ajax = L.TileLayer.extend({
req.addEventListener("loadend", this._loadEnd.bind(this));
},
- _loadEnd: function() {
+ _loadEnd: function () {
this._original_request_len = this._original_request_len || this._requests.length;
if (this._ticker < this._original_request_len) {
this._ticker++;
@@ -78,14 +78,14 @@ L.TileLayer.Ajax = L.TileLayer.extend({
$('#messages #loading').addClass('hidden');
}
},
- _reset: function() {
+ _reset: function () {
L.TileLayer.prototype._reset.apply(this, arguments);
for (var i = 0; i < this._requests.length; i++) {
this._requests[i].abort();
}
this._requests = [];
},
- _update: function() {
+ _update: function () {
if (this._map && this._map._panTransition && this._map._panTransition._inProgress) {
return;
}
@@ -104,7 +104,7 @@ L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({
// Used to calculate svg path string for clip path elements
_clipPathRectangles: {},
- initialize: function(numWorkers, numLocations, url, options, geojsonOptions) {
+ initialize: function (numWorkers, numLocations, url, options, geojsonOptions) {
this.numWorkers = numWorkers;
this.numLocations = numLocations;
this._workers = new Array(this.numWorkers);
@@ -117,7 +117,7 @@ L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({
this.geojsonLayer = new L.GeoJSON(null, geojsonOptions);
},
- onAdd: function(map) {
+ onAdd: function (map) {
var parent = this;
var i = 0;
this.queue.free = [];
@@ -130,7 +130,7 @@ L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({
i++;
}
- map.on("zoomstart", function() {
+ map.on("zoomstart", function () {
this.queue.len = 0;
this.queue.tiles = [];
}, this);
@@ -142,7 +142,7 @@ L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({
map.addLayer(this.geojsonLayer);
// map.addLayer(this.features);
},
- onRemove: function(map) {
+ onRemove: function (map) {
this.messages = {};
var len = this._workers.length;
var i = 0;
@@ -156,19 +156,19 @@ L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({
map.removeLayer(this.geojsonLayer);
// map.removeLayer(this.features);
},
- _reset: function() {
+ _reset: function () {
this.geojsonLayer.clearLayers();
this._keyLayers = {};
this._removeOldClipPaths();
L.TileLayer.Ajax.prototype._reset.apply(this, arguments);
},
- _getUniqueId: function() {
+ _getUniqueId: function () {
return String(this._leaflet_id || ''); // jshint ignore:line
},
// Remove clip path elements from other earlier zoom levels
- _removeOldClipPaths: function() {
+ _removeOldClipPaths: function () {
for (var clipPathId in this._clipPathRectangles) {
var prefix = clipPathId.split('tileClipPath')[0];
if (this._getUniqueId() === prefix) {
@@ -188,7 +188,7 @@ L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({
},
// Recurse LayerGroups and call func() on L.Path layer instances
- _recurseLayerUntilPath: function(func, layer) {
+ _recurseLayerUntilPath: function (func, layer) {
if (layer instanceof L.Path) {
func(layer);
} else if (layer instanceof L.LayerGroup) {
@@ -197,7 +197,7 @@ L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({
}
},
- _clipLayerToTileBoundary: function(layer, tilePoint) {
+ _clipLayerToTileBoundary: function (layer, tilePoint) {
// Only perform SVG clipping if the browser is using SVG
if (!L.Path.SVG) {
return;
@@ -252,12 +252,12 @@ L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({
}
// Add the clip-path attribute to reference the id of the tile clipPath
- this._recurseLayerUntilPath(function(pathLayer) {
+ this._recurseLayerUntilPath(function (pathLayer) {
pathLayer._container.setAttribute('clip-path', 'url(' + window.location.href + '#' + clipPathId + ')');
}, layer);
},
- _renderTileData: function(geojson, tilePoint, workerID) {
+ _renderTileData: function (geojson, tilePoint, workerID) {
var parent = this;
var msg = {
@@ -266,7 +266,7 @@ L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({
workerID: workerID,
};
- this._workers[workerID].addTileData(msg).then(function(data) {
+ this._workers[workerID].addTileData(msg).then(function (data) {
if (data.new_layer.length !== 0) {
parent._loadedfeatures = data._loadedfeatures;
parent.geojsonLayer.addData(data.new_layer);
@@ -281,10 +281,10 @@ L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({
parent.queue.free.push(data.workerID);
}
- }, function(e) {console.log(a);});
+ }, function (e) { console.log(a); });
},
- _tileLoaded: function(tile, tilePoint) {
+ _tileLoaded: function (tile, tilePoint) {
if (tile.datum === null || this.numLocations === Object.keys(this._loadedfeatures).length) {
return null;
}
@@ -298,7 +298,7 @@ L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({
}
},
- _loadTile: function(tile, tilePoint) {
+ _loadTile: function (tile, tilePoint) {
if (!this._lazyTiles.isLoaded(tilePoint.x, tilePoint.y, tilePoint.z)) {
diff --git a/cadasta/core/static/js/smap/edit/LatLngUtil.js b/cadasta/core/static/js/smap/edit/LatLngUtil.js
new file mode 100644
index 000000000..093bb8483
--- /dev/null
+++ b/cadasta/core/static/js/smap/edit/LatLngUtil.js
@@ -0,0 +1,51 @@
+/**
+ * @class L.LatLngUtil
+ * @aka LatLngUtil
+ */
+var LatLngUtil = {
+ // Clones a LatLngs[], returns [][]
+
+ // @method cloneLatLngs(LatLngs[]): L.LatLngs[]
+ // Clone the latLng point or points or nested points and return an array with those points
+ cloneLatLngs: function (latlngs) {
+ var clone = [];
+ for (var i = 0, l = latlngs.length; i < l; i++) {
+ // Check for nested array (Polyline/Polygon)
+ if (Array.isArray(latlngs[i])) {
+ clone.push(LatLngUtil.cloneLatLngs(latlngs[i]));
+ } else {
+ clone.push(this.cloneLatLng(latlngs[i]));
+ }
+ }
+ return clone;
+ },
+
+ // @method cloneLatLng(LatLng): L.LatLng
+ // Clone the latLng and return a new LatLng object.
+ cloneLatLng: function (latlng) {
+ return L.latLng(latlng.lat, latlng.lng);
+ },
+
+ copyLayer: function (layer) {
+ if (layer instanceof L.Rectangle) {
+ var rect = L.rectangle(layer.getLatLngs(), { draggable: true });
+ rect.feature = layer.feature;
+ return rect;
+ }
+ if (layer instanceof L.Polygon) {
+ var poly = L.polygon(layer.getLatLngs(), { draggable: true });
+ poly.feature = layer.feature;
+ return poly;
+ }
+ if (layer instanceof L.Marker) {
+ var marker = L.marker(layer.getLatLng(), { draggable: true });
+ marker.feature = layer.feature;
+ return marker;
+
+ } else {
+ var line = L.polyline(layer.getLatLngs(), { draggable: true });
+ line.feature = layer.feature;
+ return line;
+ }
+ },
+};
diff --git a/cadasta/core/static/js/smap/edit/Leaflet.Editable.js b/cadasta/core/static/js/smap/edit/Leaflet.Editable.js
new file mode 100644
index 000000000..f2da9812e
--- /dev/null
+++ b/cadasta/core/static/js/smap/edit/Leaflet.Editable.js
@@ -0,0 +1,1914 @@
+'use strict';
+(function (factory, window) {
+ /*globals define, module, require*/
+
+ // define an AMD module that relies on 'leaflet'
+ if (typeof define === 'function' && define.amd) {
+ define(['leaflet'], factory);
+
+
+ // define a Common JS module that relies on 'leaflet'
+ } else if (typeof exports === 'object') {
+ module.exports = factory(require('leaflet'));
+ }
+
+ // attach your plugin to the global 'L' variable
+ if (typeof window !== 'undefined' && window.L) {
+ factory(window.L);
+ }
+
+}(function (L) {
+ // 🍂miniclass CancelableEvent (Event objects)
+ // 🍂method cancel()
+ // Cancel any subsequent action.
+
+ // 🍂miniclass VertexEvent (Event objects)
+ // 🍂property vertex: VertexMarker
+ // The vertex that fires the event.
+
+ // 🍂miniclass ShapeEvent (Event objects)
+ // 🍂property shape: Array
+ // The shape (LatLngs array) subject of the action.
+
+ // 🍂miniclass CancelableVertexEvent (Event objects)
+ // 🍂inherits VertexEvent
+ // 🍂inherits CancelableEvent
+
+ // 🍂miniclass CancelableShapeEvent (Event objects)
+ // 🍂inherits ShapeEvent
+ // 🍂inherits CancelableEvent
+
+ // 🍂miniclass LayerEvent (Event objects)
+ // 🍂property layer: object
+ // The Layer (Marker, Polyline…) subject of the action.
+
+ // 🍂namespace Editable; 🍂class Editable; 🍂aka L.Editable
+ // Main edition handler. By default, it is attached to the map
+ // as `map.editTools` property.
+ // Leaflet.Editable is made to be fully extendable. You have three ways to customize
+ // the behaviour: using options, listening to events, or extending.
+ L.Editable = L.Evented.extend({
+
+ statics: {
+ FORWARD: 1,
+ BACKWARD: -1
+ },
+
+ options: {
+
+ // You can pass them when creating a map using the `editOptions` key.
+ // 🍂option zIndex: int = 1000
+ // The default zIndex of the editing tools.
+ zIndex: 1000,
+
+ // 🍂option polygonClass: class = L.Polygon
+ // Class to be used when creating a new Polygon.
+ polygonClass: L.Polygon,
+
+ // 🍂option polylineClass: class = L.Polyline
+ // Class to be used when creating a new Polyline.
+ polylineClass: L.Polyline,
+
+ // 🍂option markerClass: class = L.Marker
+ // Class to be used when creating a new Marker.
+ markerClass: L.Marker,
+
+ // 🍂option rectangleClass: class = L.Rectangle
+ // Class to be used when creating a new Rectangle.
+ rectangleClass: L.Rectangle,
+
+ // 🍂option circleClass: class = L.Circle
+ // Class to be used when creating a new Circle.
+ circleClass: L.Circle,
+
+ // 🍂option drawingCSSClass: string = 'leaflet-editable-drawing'
+ // CSS class to be added to the map container while drawing.
+ drawingCSSClass: 'leaflet-editable-drawing',
+
+ // 🍂option drawingCursor: const = 'crosshair'
+ // Cursor mode set to the map while drawing.
+ drawingCursor: 'crosshair',
+
+ // 🍂option editLayer: Layer = new L.LayerGroup()
+ // Layer used to store edit tools (vertex, line guide…).
+ editLayer: undefined,
+
+ // 🍂option featuresLayer: Layer = new L.LayerGroup()
+ // Default layer used to store drawn features (Marker, Polyline…).
+ featuresLayer: undefined,
+
+ // 🍂option polylineEditorClass: class = PolylineEditor
+ // Class to be used as Polyline editor.
+ polylineEditorClass: undefined,
+
+ // 🍂option polygonEditorClass: class = PolygonEditor
+ // Class to be used as Polygon editor.
+ polygonEditorClass: undefined,
+
+ // 🍂option markerEditorClass: class = MarkerEditor
+ // Class to be used as Marker editor.
+ markerEditorClass: undefined,
+
+ // 🍂option rectangleEditorClass: class = RectangleEditor
+ // Class to be used as Rectangle editor.
+ rectangleEditorClass: undefined,
+
+ // 🍂option circleEditorClass: class = CircleEditor
+ // Class to be used as Circle editor.
+ circleEditorClass: undefined,
+
+ // 🍂option lineGuideOptions: hash = {}
+ // Options to be passed to the line guides.
+ lineGuideOptions: {},
+
+ // 🍂option skipMiddleMarkers: boolean = false
+ // Set this to true if you don't want middle markers.
+ skipMiddleMarkers: false
+
+ },
+
+ initialize: function (map, options) {
+ L.setOptions(this, options);
+ this._lastZIndex = this.options.zIndex;
+ this.map = map;
+ this.editLayer = this.createEditLayer();
+ this.featuresLayer = this.createFeaturesLayer();
+ this.forwardLineGuide = this.createLineGuide();
+ this.backwardLineGuide = this.createLineGuide();
+ },
+
+ fireAndForward: function (type, e) {
+ e = e || {};
+ e.editTools = this;
+ this.fire(type, e);
+ this.map.fire(type, e);
+ },
+
+ createLineGuide: function () {
+ var options = L.extend({ dashArray: '5,10', weight: 1, interactive: false }, this.options.lineGuideOptions);
+ return L.polyline([], options);
+ },
+
+ createVertexIcon: function (options) {
+ return L.Browser.touch ? new L.Editable.TouchVertexIcon(options) : new L.Editable.VertexIcon(options);
+ },
+
+ createEditLayer: function () {
+ return this.options.editLayer || new L.LayerGroup().addTo(this.map);
+ },
+
+ createFeaturesLayer: function () {
+ return this.options.featuresLayer || new L.LayerGroup().addTo(this.map);
+ },
+
+ moveForwardLineGuide: function (latlng) {
+ if (this.forwardLineGuide._latlngs.length) {
+ this.forwardLineGuide._latlngs[1] = latlng;
+ this.forwardLineGuide._bounds.extend(latlng);
+ this.forwardLineGuide.redraw();
+ }
+ },
+
+ moveBackwardLineGuide: function (latlng) {
+ if (this.backwardLineGuide._latlngs.length) {
+ this.backwardLineGuide._latlngs[1] = latlng;
+ this.backwardLineGuide._bounds.extend(latlng);
+ this.backwardLineGuide.redraw();
+ }
+ },
+
+ anchorForwardLineGuide: function (latlng) {
+ this.forwardLineGuide._latlngs[0] = latlng;
+ this.forwardLineGuide._bounds.extend(latlng);
+ this.forwardLineGuide.redraw();
+ },
+
+ anchorBackwardLineGuide: function (latlng) {
+ this.backwardLineGuide._latlngs[0] = latlng;
+ this.backwardLineGuide._bounds.extend(latlng);
+ this.backwardLineGuide.redraw();
+ },
+
+ attachForwardLineGuide: function () {
+ this.editLayer.addLayer(this.forwardLineGuide);
+ },
+
+ attachBackwardLineGuide: function () {
+ this.editLayer.addLayer(this.backwardLineGuide);
+ },
+
+ detachForwardLineGuide: function () {
+ this.forwardLineGuide.setLatLngs([]);
+ this.editLayer.removeLayer(this.forwardLineGuide);
+ },
+
+ detachBackwardLineGuide: function () {
+ this.backwardLineGuide.setLatLngs([]);
+ this.editLayer.removeLayer(this.backwardLineGuide);
+ },
+
+ blockEvents: function () {
+ // Hack: force map not to listen to other layers events while drawing.
+ if (!this._oldTargets) {
+ this._oldTargets = this.map._targets;
+ this.map._targets = {};
+ }
+ },
+
+ unblockEvents: function () {
+ if (this._oldTargets) {
+ // Reset, but keep targets created while drawing.
+ this.map._targets = L.extend(this.map._targets, this._oldTargets);
+ delete this._oldTargets;
+ }
+ },
+
+ registerForDrawing: function (editor) {
+ if (this._drawingEditor) this.unregisterForDrawing(this._drawingEditor);
+ this.blockEvents();
+ editor.reset(); // Make sure editor tools still receive events.
+ this._drawingEditor = editor;
+ this.map.on('mousemove touchmove', editor.onDrawingMouseMove, editor);
+ this.map.on('mousedown', this.onMousedown, this);
+ this.map.on('mouseup', this.onMouseup, this);
+ L.DomUtil.addClass(this.map._container, this.options.drawingCSSClass);
+ this.defaultMapCursor = this.map._container.style.cursor;
+ this.map._container.style.cursor = this.options.drawingCursor;
+ },
+
+ unregisterForDrawing: function (editor) {
+ this.unblockEvents();
+ L.DomUtil.removeClass(this.map._container, this.options.drawingCSSClass);
+ this.map._container.style.cursor = this.defaultMapCursor;
+ editor = editor || this._drawingEditor;
+ if (!editor) return;
+ this.map.off('mousemove touchmove', editor.onDrawingMouseMove, editor);
+ this.map.off('mousedown', this.onMousedown, this);
+ this.map.off('mouseup', this.onMouseup, this);
+ if (editor !== this._drawingEditor) return;
+ delete this._drawingEditor;
+ if (editor._drawing) editor.cancelDrawing();
+ },
+
+ onMousedown: function (e) {
+ this._mouseDown = e;
+ this._drawingEditor.onDrawingMouseDown(e);
+ },
+
+ onMouseup: function (e) {
+ if (this._mouseDown) {
+ var editor = this._drawingEditor,
+ mouseDown = this._mouseDown;
+ this._mouseDown = null;
+ editor.onDrawingMouseUp(e);
+ if (this._drawingEditor !== editor) return; // onDrawingMouseUp may call unregisterFromDrawing.
+ var origin = L.point(mouseDown.originalEvent.clientX, mouseDown.originalEvent.clientY);
+ var distance = L.point(e.originalEvent.clientX, e.originalEvent.clientY).distanceTo(origin);
+ if (Math.abs(distance) < 9 * (window.devicePixelRatio || 1)) this._drawingEditor.onDrawingClick(e);
+ }
+ },
+
+ // 🍂section Public methods
+ // You will generally access them by the `map.editTools`
+ // instance:
+ //
+ // `map.editTools.startPolyline();`
+
+ // 🍂method drawing(): boolean
+ // Return true if any drawing action is ongoing.
+ drawing: function () {
+ return this._drawingEditor && this._drawingEditor.drawing();
+ },
+
+ // 🍂method stopDrawing()
+ // When you need to stop any ongoing drawing, without needing to know which editor is active.
+ stopDrawing: function () {
+ this.unregisterForDrawing();
+ },
+
+ // 🍂method commitDrawing()
+ // When you need to commit any ongoing drawing, without needing to know which editor is active.
+ commitDrawing: function (e) {
+ if (!this._drawingEditor) return;
+ this._drawingEditor.commitDrawing(e);
+ },
+
+ connectCreatedToMap: function (layer) {
+ return this.featuresLayer.addLayer(layer);
+ },
+
+ // 🍂method startPolyline(latlng: L.LatLng, options: hash): L.Polyline
+ // Start drawing a Polyline. If `latlng` is given, a first point will be added. In any case, continuing on user click.
+ // If `options` is given, it will be passed to the Polyline class constructor.
+ startPolyline: function (latlng, options) {
+ var line = this.createPolyline([], options);
+ line.enableEdit(this.map).newShape(latlng);
+ return line;
+ },
+
+ // 🍂method startPolygon(latlng: L.LatLng, options: hash): L.Polygon
+ // Start drawing a Polygon. If `latlng` is given, a first point will be added. In any case, continuing on user click.
+ // If `options` is given, it will be passed to the Polygon class constructor.
+ startPolygon: function (latlng, options) {
+ var polygon = this.createPolygon([], options);
+ polygon.enableEdit(this.map).newShape(latlng);
+ return polygon;
+ },
+
+ // 🍂method startMarker(latlng: L.LatLng, options: hash): L.Marker
+ // Start adding a Marker. If `latlng` is given, the Marker will be shown first at this point.
+ // In any case, it will follow the user mouse, and will have a final `latlng` on next click (or touch).
+ // If `options` is given, it will be passed to the Marker class constructor.
+ startMarker: function (latlng, options) {
+ latlng = latlng || this.map.getCenter().clone();
+ var marker = this.createMarker(latlng, options);
+ marker.enableEdit(this.map).startDrawing();
+ return marker;
+ },
+
+ // 🍂method startRectangle(latlng: L.LatLng, options: hash): L.Rectangle
+ // Start drawing a Rectangle. If `latlng` is given, the Rectangle anchor will be added. In any case, continuing on user drag.
+ // If `options` is given, it will be passed to the Rectangle class constructor.
+ startRectangle: function (latlng, options) {
+ var corner = latlng || L.latLng([0, 0]);
+ var bounds = new L.LatLngBounds(corner, corner);
+ var rectangle = this.createRectangle(bounds, options);
+ rectangle.enableEdit(this.map).startDrawing();
+ return rectangle;
+ },
+
+ // 🍂method startCircle(latlng: L.LatLng, options: hash): L.Circle
+ // Start drawing a Circle. If `latlng` is given, the Circle anchor will be added. In any case, continuing on user drag.
+ // If `options` is given, it will be passed to the Circle class constructor.
+ startCircle: function (latlng, options) {
+ latlng = latlng || this.map.getCenter().clone();
+ var circle = this.createCircle(latlng, options);
+ circle.enableEdit(this.map).startDrawing();
+ return circle;
+ },
+
+ startHole: function (editor, latlng) {
+ editor.newHole(latlng);
+ },
+
+ createLayer: function (klass, latlngs, options) {
+ options = L.Util.extend({ editOptions: { editTools: this } }, options);
+ var layer = new klass(latlngs, options);
+ // 🍂namespace Editable
+ // 🍂event editable:created: LayerEvent
+ // Fired when a new feature (Marker, Polyline…) is created.
+ this.fireAndForward('editable:created', { layer: layer });
+ return layer;
+ },
+
+ createPolyline: function (latlngs, options) {
+ return this.createLayer(options && options.polylineClass || this.options.polylineClass, latlngs, options);
+ },
+
+ createPolygon: function (latlngs, options) {
+ return this.createLayer(options && options.polygonClass || this.options.polygonClass, latlngs, options);
+ },
+
+ createMarker: function (latlng, options) {
+ return this.createLayer(options && options.markerClass || this.options.markerClass, latlng, options);
+ },
+
+ createRectangle: function (bounds, options) {
+ return this.createLayer(options && options.rectangleClass || this.options.rectangleClass, bounds, options);
+ },
+
+ createCircle: function (latlng, options) {
+ return this.createLayer(options && options.circleClass || this.options.circleClass, latlng, options);
+ }
+
+ });
+
+ L.extend(L.Editable, {
+
+ makeCancellable: function (e) {
+ e.cancel = function () {
+ e._cancelled = true;
+ };
+ }
+
+ });
+
+ // 🍂namespace Map; 🍂class Map
+ // Leaflet.Editable add options and events to the `L.Map` object.
+ // See `Editable` events for the list of events fired on the Map.
+ // 🍂example
+ //
+ // ```js
+ // var map = L.map('map', {
+ // editable: true,
+ // editOptions: {
+ // …
+ // }
+ // });
+ // ```
+ // 🍂section Editable Map Options
+ L.Map.mergeOptions({
+
+ // 🍂namespace Map
+ // 🍂section Map Options
+ // 🍂option editToolsClass: class = L.Editable
+ // Class to be used as vertex, for path editing.
+ editToolsClass: L.Editable,
+
+ // 🍂option editable: boolean = false
+ // Whether to create a L.Editable instance at map init.
+ editable: false,
+
+ // 🍂option editOptions: hash = {}
+ // Options to pass to L.Editable when instanciating.
+ editOptions: {}
+
+ });
+
+ L.Map.addInitHook(function () {
+
+ this.whenReady(function () {
+ if (this.options.editable) {
+ this.editTools = new this.options.editToolsClass(this, this.options.editOptions);
+ }
+ });
+
+ });
+
+ L.Editable.VertexIcon = L.DivIcon.extend({
+
+ options: {
+ iconSize: new L.Point(8, 8)
+ }
+
+ });
+
+ L.Editable.TouchVertexIcon = L.Editable.VertexIcon.extend({
+
+ options: {
+ iconSize: new L.Point(20, 20)
+ }
+
+ });
+
+
+ // 🍂namespace Editable; 🍂class VertexMarker; Handler for dragging path vertices.
+ L.Editable.VertexMarker = L.Marker.extend({
+
+ options: {
+ draggable: true,
+ className: 'leaflet-div-icon leaflet-vertex-icon'
+ },
+
+
+ // 🍂section Public methods
+ // The marker used to handle path vertex. You will usually interact with a `VertexMarker`
+ // instance when listening for events like `editable:vertex:ctrlclick`.
+
+ initialize: function (latlng, latlngs, editor, options) {
+ // We don't use this._latlng, because on drag Leaflet replace it while
+ // we want to keep reference.
+ this.latlng = latlng;
+ this.latlngs = latlngs;
+ this.editor = editor;
+ L.Marker.prototype.initialize.call(this, latlng, options);
+ this.options.icon = this.editor.tools.createVertexIcon({ className: this.options.className });
+ this.latlng.__vertex = this;
+ this.editor.editLayer.addLayer(this);
+ this.setZIndexOffset(editor.tools._lastZIndex + 1);
+ },
+
+ onAdd: function (map) {
+ L.Marker.prototype.onAdd.call(this, map);
+ this.on('drag', this.onDrag);
+ this.on('dragstart', this.onDragStart);
+ this.on('dragend', this.onDragEnd);
+ this.on('mouseup', this.onMouseup);
+ this.on('click', this.onClick);
+ this.on('contextmenu', this.onContextMenu);
+ this.on('mousedown touchstart', this.onMouseDown);
+ this.addMiddleMarkers();
+ },
+
+ onRemove: function (map) {
+ if (this.middleMarker) this.middleMarker.delete();
+ delete this.latlng.__vertex;
+ this.off('drag', this.onDrag);
+ this.off('dragstart', this.onDragStart);
+ this.off('dragend', this.onDragEnd);
+ this.off('mouseup', this.onMouseup);
+ this.off('click', this.onClick);
+ this.off('contextmenu', this.onContextMenu);
+ this.off('mousedown touchstart', this.onMouseDown);
+ L.Marker.prototype.onRemove.call(this, map);
+ },
+
+ onDrag: function (e) {
+ e.vertex = this;
+ this.editor.onVertexMarkerDrag(e);
+ var iconPos = L.DomUtil.getPosition(this._icon),
+ latlng = this._map.layerPointToLatLng(iconPos);
+ this.latlng.update(latlng);
+ this._latlng = this.latlng; // Push back to Leaflet our reference.
+ this.editor.refresh();
+ if (this.middleMarker) this.middleMarker.updateLatLng();
+ var next = this.getNext();
+ if (next && next.middleMarker) next.middleMarker.updateLatLng();
+ },
+
+ onDragStart: function (e) {
+ e.vertex = this;
+ this.editor.onVertexMarkerDragStart(e);
+ },
+
+ onDragEnd: function (e) {
+ e.vertex = this;
+ this.editor.onVertexMarkerDragEnd(e);
+ },
+
+ onClick: function (e) {
+ e.vertex = this;
+ this.editor.onVertexMarkerClick(e);
+ },
+
+ onMouseup: function (e) {
+ L.DomEvent.stop(e);
+ e.vertex = this;
+ this.editor.map.fire('mouseup', e);
+ },
+
+ onContextMenu: function (e) {
+ e.vertex = this;
+ this.editor.onVertexMarkerContextMenu(e);
+ },
+
+ onMouseDown: function (e) {
+ e.vertex = this;
+ this.editor.onVertexMarkerMouseDown(e);
+ },
+
+ // 🍂method delete()
+ // Delete a vertex and the related LatLng.
+ delete: function () {
+ var next = this.getNext(); // Compute before changing latlng
+ this.latlngs.splice(this.getIndex(), 1);
+ this.editor.editLayer.removeLayer(this);
+ this.editor.onVertexDeleted({ latlng: this.latlng, vertex: this });
+ if (!this.latlngs.length) this.editor.deleteShape(this.latlngs);
+ if (next) next.resetMiddleMarker();
+ this.editor.refresh();
+ },
+
+ // 🍂method getIndex(): int
+ // Get the index of the current vertex among others of the same LatLngs group.
+ getIndex: function () {
+ return this.latlngs.indexOf(this.latlng);
+ },
+
+ // 🍂method getLastIndex(): int
+ // Get last vertex index of the LatLngs group of the current vertex.
+ getLastIndex: function () {
+ return this.latlngs.length - 1;
+ },
+
+ // 🍂method getPrevious(): VertexMarker
+ // Get the previous VertexMarker in the same LatLngs group.
+ getPrevious: function () {
+ if (this.latlngs.length < 2) return;
+ var index = this.getIndex(),
+ previousIndex = index - 1;
+ if (index === 0 && this.editor.CLOSED) previousIndex = this.getLastIndex();
+ var previous = this.latlngs[previousIndex];
+ if (previous) return previous.__vertex;
+ },
+
+ // 🍂method getNext(): VertexMarker
+ // Get the next VertexMarker in the same LatLngs group.
+ getNext: function () {
+ if (this.latlngs.length < 2) return;
+ var index = this.getIndex(),
+ nextIndex = index + 1;
+ if (index === this.getLastIndex() && this.editor.CLOSED) nextIndex = 0;
+ var next = this.latlngs[nextIndex];
+ if (next) return next.__vertex;
+ },
+
+ addMiddleMarker: function (previous) {
+ if (!this.editor.hasMiddleMarkers()) return;
+ previous = previous || this.getPrevious();
+ if (previous && !this.middleMarker) this.middleMarker = this.editor.addMiddleMarker(previous, this, this.latlngs, this.editor);
+ },
+
+ addMiddleMarkers: function () {
+ if (!this.editor.hasMiddleMarkers()) return;
+ var previous = this.getPrevious();
+ if (previous) this.addMiddleMarker(previous);
+ var next = this.getNext();
+ if (next) next.resetMiddleMarker();
+ },
+
+ resetMiddleMarker: function () {
+ if (this.middleMarker) this.middleMarker.delete();
+ this.addMiddleMarker();
+ },
+
+ // 🍂method split()
+ // Split the vertex LatLngs group at its index, if possible.
+ split: function () {
+ if (!this.editor.splitShape) return; // Only for PolylineEditor
+ this.editor.splitShape(this.latlngs, this.getIndex());
+ },
+
+ // 🍂method continue()
+ // Continue the vertex LatLngs from this vertex. Only active for first and last vertices of a Polyline.
+ continue: function () {
+ if (!this.editor.continueBackward) return; // Only for PolylineEditor
+ var index = this.getIndex();
+ if (index === 0) this.editor.continueBackward(this.latlngs);
+ else if (index === this.getLastIndex()) this.editor.continueForward(this.latlngs);
+ }
+
+ });
+
+ L.Editable.mergeOptions({
+
+ // 🍂namespace Editable
+ // 🍂option vertexMarkerClass: class = VertexMarker
+ // Class to be used as vertex, for path editing.
+ vertexMarkerClass: L.Editable.VertexMarker
+
+ });
+
+ L.Editable.MiddleMarker = L.Marker.extend({
+
+ options: {
+ opacity: 0.5,
+ className: 'leaflet-div-icon leaflet-middle-icon',
+ draggable: true
+ },
+
+ 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(), options);
+ this._opacity = this.options.opacity;
+ this.options.icon = this.editor.tools.createVertexIcon({ className: this.options.className });
+ this.editor.editLayer.addLayer(this);
+ this.setVisibility();
+ },
+
+ setVisibility: function () {
+ var leftPoint = this._map.latLngToContainerPoint(this.left.latlng),
+ rightPoint = this._map.latLngToContainerPoint(this.right.latlng),
+ size = L.point(this.options.icon.options.iconSize);
+ if (leftPoint.distanceTo(rightPoint) < size.x * 3) this.hide();
+ else this.show();
+ },
+
+ show: function () {
+ this.setOpacity(this._opacity);
+ },
+
+ hide: function () {
+ this.setOpacity(0);
+ },
+
+ updateLatLng: function () {
+ this.setLatLng(this.computeLatLng());
+ this.setVisibility();
+ },
+
+ computeLatLng: function () {
+ var leftPoint = this.editor.map.latLngToContainerPoint(this.left.latlng),
+ rightPoint = this.editor.map.latLngToContainerPoint(this.right.latlng),
+ y = (leftPoint.y + rightPoint.y) / 2,
+ x = (leftPoint.x + rightPoint.x) / 2;
+ return this.editor.map.containerPointToLatLng([x, y]);
+ },
+
+ onAdd: function (map) {
+ L.Marker.prototype.onAdd.call(this, map);
+ L.DomEvent.on(this._icon, 'mousedown touchstart', this.onMouseDown, this);
+ map.on('zoomend', this.setVisibility, this);
+ },
+
+ onRemove: function (map) {
+ delete this.right.middleMarker;
+ L.DomEvent.off(this._icon, 'mousedown touchstart', this.onMouseDown, this);
+ map.off('zoomend', this.setVisibility, this);
+ L.Marker.prototype.onRemove.call(this, map);
+ },
+
+ onMouseDown: function (e) {
+ var iconPos = L.DomUtil.getPosition(this._icon),
+ latlng = this.editor.map.layerPointToLatLng(iconPos);
+ e = {
+ originalEvent: e,
+ latlng: latlng
+ };
+ if (this.options.opacity === 0) return;
+ L.Editable.makeCancellable(e);
+ this.editor.onMiddleMarkerMouseDown(e);
+ if (e._cancelled) return;
+ this.latlngs.splice(this.index(), 0, e.latlng);
+ this.editor.refresh();
+ var icon = this._icon;
+ var marker = this.editor.addVertexMarker(e.latlng, this.latlngs);
+ /* Hack to workaround browser not firing touchend when element is no more on DOM */
+ var parent = marker._icon.parentNode;
+ parent.removeChild(marker._icon);
+ marker._icon = icon;
+ parent.appendChild(marker._icon);
+ marker._initIcon();
+ marker._initInteraction();
+ marker.setOpacity(1);
+ /* End hack */
+ // Transfer ongoing dragging to real marker
+ L.Draggable._dragging = false;
+ marker.dragging._draggable._onDown(e.originalEvent);
+ this.delete();
+ },
+
+ delete: function () {
+ this.editor.editLayer.removeLayer(this);
+ },
+
+ index: function () {
+ return this.latlngs.indexOf(this.right.latlng);
+ }
+
+ });
+
+ L.Editable.mergeOptions({
+
+ // 🍂namespace Editable
+ // 🍂option middleMarkerClass: class = VertexMarker
+ // Class to be used as middle vertex, pulled by the user to create a new point in the middle of a path.
+ middleMarkerClass: L.Editable.MiddleMarker
+
+ });
+
+ // 🍂namespace Editable; 🍂class BaseEditor; 🍂aka L.Editable.BaseEditor
+ // When editing a feature (Marker, Polyline…), an editor is attached to it. This
+ // editor basically knows how to handle the edition.
+ L.Editable.BaseEditor = L.Handler.extend({
+
+ initialize: function (map, feature, options) {
+ L.setOptions(this, options);
+ this.map = map;
+ this.feature = feature;
+ this.feature.editor = this;
+ this.editLayer = new L.LayerGroup();
+ this.tools = this.options.editTools || map.editTools;
+ },
+
+ // 🍂method enable(): this
+ // Set up the drawing tools for the feature to be editable.
+ addHooks: function () {
+ if (this.isConnected()) this.onFeatureAdd();
+ else this.feature.once('add', this.onFeatureAdd, this);
+ this.onEnable();
+ this.feature.on(this._getEvents(), this);
+ return;
+ },
+
+ // 🍂method disable(): this
+ // Remove the drawing tools for the feature.
+ removeHooks: function () {
+ this.feature.off(this._getEvents(), this);
+ if (this.feature.dragging) this.feature.dragging.disable();
+ this.editLayer.clearLayers();
+ this.tools.editLayer.removeLayer(this.editLayer);
+ this.onDisable();
+ if (this._drawing) this.cancelDrawing();
+ return;
+ },
+
+ // 🍂method drawing(): boolean
+ // Return true if any drawing action is ongoing with this editor.
+ drawing: function () {
+ return !!this._drawing;
+ },
+
+ reset: function () {},
+
+ onFeatureAdd: function () {
+ this.tools.editLayer.addLayer(this.editLayer);
+ if (this.feature.dragging) this.feature.dragging.enable();
+ },
+
+ hasMiddleMarkers: function () {
+ return !this.options.skipMiddleMarkers && !this.tools.options.skipMiddleMarkers;
+ },
+
+ fireAndForward: function (type, e) {
+ e = e || {};
+ e.layer = this.feature;
+ this.feature.fire(type, e);
+ this.tools.fireAndForward(type, e);
+ },
+
+ onEnable: function () {
+ // 🍂namespace Editable
+ // 🍂event editable:enable: Event
+ // Fired when an existing feature is ready to be edited.
+ this.fireAndForward('editable:enable');
+ },
+
+ onDisable: function () {
+ // 🍂namespace Editable
+ // 🍂event editable:disable: Event
+ // Fired when an existing feature is not ready anymore to be edited.
+ this.fireAndForward('editable:disable');
+ },
+
+ onEditing: function () {
+ // 🍂namespace Editable
+ // 🍂event editable:editing: Event
+ // Fired as soon as any change is made to the feature geometry.
+ this.fireAndForward('editable:editing');
+ },
+
+ onStartDrawing: function () {
+ // 🍂namespace Editable
+ // 🍂section Drawing events
+ // 🍂event editable:drawing:start: Event
+ // Fired when a feature is to be drawn.
+ this.fireAndForward('editable:drawing:start');
+ },
+
+ onEndDrawing: function () {
+ // 🍂namespace Editable
+ // 🍂section Drawing events
+ // 🍂event editable:drawing:end: Event
+ // Fired when a feature is not drawn anymore.
+ this.fireAndForward('editable:drawing:end');
+ },
+
+ onCancelDrawing: function () {
+ // 🍂namespace Editable
+ // 🍂section Drawing events
+ // 🍂event editable:drawing:cancel: Event
+ // Fired when user cancel drawing while a feature is being drawn.
+ this.fireAndForward('editable:drawing:cancel');
+ },
+
+ onCommitDrawing: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Drawing events
+ // 🍂event editable:drawing:commit: Event
+ // Fired when user finish drawing a feature.
+ this.fireAndForward('editable:drawing:commit', e);
+ },
+
+ onDrawingMouseDown: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Drawing events
+ // 🍂event editable:drawing:mousedown: Event
+ // Fired when user `mousedown` while drawing.
+ this.fireAndForward('editable:drawing:mousedown', e);
+ },
+
+ onDrawingMouseUp: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Drawing events
+ // 🍂event editable:drawing:mouseup: Event
+ // Fired when user `mouseup` while drawing.
+ this.fireAndForward('editable:drawing:mouseup', e);
+ },
+
+ startDrawing: function () {
+ if (!this._drawing) this._drawing = L.Editable.FORWARD;
+ this.tools.registerForDrawing(this);
+ this.onStartDrawing();
+ },
+
+ commitDrawing: function (e) {
+ this.onCommitDrawing(e);
+ this.endDrawing();
+ },
+
+ cancelDrawing: function () {
+ // If called during a vertex drag, the vertex will be removed before
+ // the mouseup fires on it. This is a workaround. Maybe better fix is
+ // To have L.Draggable reset it's status on disable (Leaflet side).
+ L.Draggable._dragging = false;
+ this.onCancelDrawing();
+ this.endDrawing();
+ },
+
+ endDrawing: function () {
+ this._drawing = false;
+ this.tools.unregisterForDrawing(this);
+ this.onEndDrawing();
+ },
+
+ onDrawingClick: function (e) {
+ if (!this.drawing()) return;
+ L.Editable.makeCancellable(e);
+ // 🍂namespace Editable
+ // 🍂section Drawing events
+ // 🍂event editable:drawing:click: CancelableEvent
+ // Fired when user `click` while drawing, before any internal action is being processed.
+ this.fireAndForward('editable:drawing:click', e);
+ if (e._cancelled) return;
+ if (!this.isConnected()) this.connect(e);
+ this.processDrawingClick(e);
+ },
+
+ isConnected: function () {
+ return this.map.hasLayer(this.feature);
+ },
+
+ connect: function (e) {
+ this.tools.connectCreatedToMap(this.feature);
+ this.tools.editLayer.addLayer(this.editLayer);
+ },
+
+ onMove: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Drawing events
+ // 🍂event editable:drawing:move: Event
+ // Fired when `move` mouse while drawing, while dragging a marker, and while dragging a vertex.
+ this.fireAndForward('editable:drawing:move', e);
+ },
+
+ onDrawingMouseMove: function (e) {
+ this.onMove(e);
+ },
+
+ _getEvents: function () {
+ return {
+ dragstart: this.onDragStart,
+ drag: this.onDrag,
+ dragend: this.onDragEnd,
+ remove: this.disable
+ };
+ },
+
+ onDragStart: function (e) {
+ this.onEditing();
+ // 🍂namespace Editable
+ // 🍂event editable:dragstart: Event
+ // Fired before a path feature is dragged.
+ this.fireAndForward('editable:dragstart', e);
+ },
+
+ onDrag: function (e) {
+ this.onMove(e);
+ // 🍂namespace Editable
+ // 🍂event editable:drag: Event
+ // Fired when a path feature is being dragged.
+ this.fireAndForward('editable:drag', e);
+ },
+
+ onDragEnd: function (e) {
+ // 🍂namespace Editable
+ // 🍂event editable:dragend: Event
+ // Fired after a path feature has been dragged.
+ this.fireAndForward('editable:dragend', e);
+ }
+
+ });
+
+ // 🍂namespace Editable; 🍂class MarkerEditor; 🍂aka L.Editable.MarkerEditor
+ // 🍂inherits BaseEditor
+ // Editor for Marker.
+ L.Editable.MarkerEditor = L.Editable.BaseEditor.extend({
+
+ onDrawingMouseMove: function (e) {
+ L.Editable.BaseEditor.prototype.onDrawingMouseMove.call(this, e);
+ if (this._drawing) this.feature.setLatLng(e.latlng);
+ },
+
+ processDrawingClick: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Drawing events
+ // 🍂event editable:drawing:clicked: Event
+ // Fired when user `click` while drawing, after all internal actions.
+ this.fireAndForward('editable:drawing:clicked', e);
+ this.commitDrawing(e);
+ },
+
+ connect: function (e) {
+ // On touch, the latlng has not been updated because there is
+ // no mousemove.
+ if (e) this.feature._latlng = e.latlng;
+ L.Editable.BaseEditor.prototype.connect.call(this, e);
+ }
+
+ });
+
+ // 🍂namespace Editable; 🍂class PathEditor; 🍂aka L.Editable.PathEditor
+ // 🍂inherits BaseEditor
+ // Base class for all path editors.
+ L.Editable.PathEditor = L.Editable.BaseEditor.extend({
+
+ CLOSED: false,
+ MIN_VERTEX: 2,
+
+ addHooks: function () {
+ L.Editable.BaseEditor.prototype.addHooks.call(this);
+ if (this.feature) this.initVertexMarkers();
+ return this;
+ },
+
+ initVertexMarkers: function (latlngs) {
+ if (!this.enabled()) return;
+ latlngs = latlngs || this.getLatLngs();
+ if (L.Polyline._flat(latlngs)) this.addVertexMarkers(latlngs);
+ else
+ for (var i = 0; i < latlngs.length; i++) this.initVertexMarkers(latlngs[i]);
+ },
+
+ getLatLngs: function () {
+ return this.feature.getLatLngs();
+ },
+
+ // 🍂method reset()
+ // Rebuild edit elements (Vertex, MiddleMarker, etc.).
+ reset: function () {
+ this.editLayer.clearLayers();
+ this.initVertexMarkers();
+ },
+
+ addVertexMarker: function (latlng, latlngs) {
+ return new this.tools.options.vertexMarkerClass(latlng, latlngs, this);
+ },
+
+ addVertexMarkers: function (latlngs) {
+ for (var i = 0; i < latlngs.length; i++) {
+ this.addVertexMarker(latlngs[i], latlngs);
+ }
+ },
+
+ refreshVertexMarkers: function (latlngs) {
+ latlngs = latlngs || this.getDefaultLatLngs();
+ for (var i = 0; i < latlngs.length; i++) {
+ latlngs[i].__vertex.update();
+ }
+ },
+
+ addMiddleMarker: function (left, right, latlngs) {
+ return new this.tools.options.middleMarkerClass(left, right, latlngs, this);
+ },
+
+ onVertexMarkerClick: function (e) {
+ L.Editable.makeCancellable(e);
+ // 🍂namespace Editable
+ // 🍂section Vertex events
+ // 🍂event editable:vertex:click: CancelableVertexEvent
+ // Fired when a `click` is issued on a vertex, before any internal action is being processed.
+ this.fireAndForward('editable:vertex:click', e);
+ if (e._cancelled) return;
+ if (this.tools.drawing() && this.tools._drawingEditor !== this) return;
+ var index = e.vertex.getIndex(),
+ commit;
+ if (e.originalEvent.ctrlKey) {
+ this.onVertexMarkerCtrlClick(e);
+ } else if (e.originalEvent.altKey) {
+ this.onVertexMarkerAltClick(e);
+ } else if (e.originalEvent.shiftKey) {
+ this.onVertexMarkerShiftClick(e);
+ } else if (e.originalEvent.metaKey) {
+ this.onVertexMarkerMetaKeyClick(e);
+ } else if (index === e.vertex.getLastIndex() && this._drawing === L.Editable.FORWARD) {
+ if (index >= this.MIN_VERTEX - 1) commit = true;
+ } else if (index === 0 && this._drawing === L.Editable.BACKWARD && this._drawnLatLngs.length >= this.MIN_VERTEX) {
+ commit = true;
+ } else if (index === 0 && this._drawing === L.Editable.FORWARD && this._drawnLatLngs.length >= this.MIN_VERTEX && this.CLOSED) {
+ commit = true; // Allow to close on first point also for polygons
+ } else {
+ this.onVertexRawMarkerClick(e);
+ }
+ // 🍂namespace Editable
+ // 🍂section Vertex events
+ // 🍂event editable:vertex:clicked: VertexEvent
+ // Fired when a `click` is issued on a vertex, after all internal actions.
+ this.fireAndForward('editable:vertex:clicked', e);
+ if (commit) this.commitDrawing(e);
+ },
+
+ onVertexRawMarkerClick: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Vertex events
+ // 🍂event editable:vertex:rawclick: CancelableVertexEvent
+ // Fired when a `click` is issued on a vertex without any special key and without being in drawing mode.
+ this.fireAndForward('editable:vertex:rawclick', e);
+ if (e._cancelled) return;
+ if (!this.vertexCanBeDeleted(e.vertex)) return;
+ e.vertex.delete();
+ },
+
+ vertexCanBeDeleted: function (vertex) {
+ return vertex.latlngs.length > this.MIN_VERTEX;
+ },
+
+ onVertexDeleted: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Vertex events
+ // 🍂event editable:vertex:deleted: VertexEvent
+ // Fired after a vertex has been deleted by user.
+ this.fireAndForward('editable:vertex:deleted', e);
+ },
+
+ onVertexMarkerCtrlClick: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Vertex events
+ // 🍂event editable:vertex:ctrlclick: VertexEvent
+ // Fired when a `click` with `ctrlKey` is issued on a vertex.
+ this.fireAndForward('editable:vertex:ctrlclick', e);
+ },
+
+ onVertexMarkerShiftClick: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Vertex events
+ // 🍂event editable:vertex:shiftclick: VertexEvent
+ // Fired when a `click` with `shiftKey` is issued on a vertex.
+ this.fireAndForward('editable:vertex:shiftclick', e);
+ },
+
+ onVertexMarkerMetaKeyClick: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Vertex events
+ // 🍂event editable:vertex:metakeyclick: VertexEvent
+ // Fired when a `click` with `metaKey` is issued on a vertex.
+ this.fireAndForward('editable:vertex:metakeyclick', e);
+ },
+
+ onVertexMarkerAltClick: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Vertex events
+ // 🍂event editable:vertex:altclick: VertexEvent
+ // Fired when a `click` with `altKey` is issued on a vertex.
+ this.fireAndForward('editable:vertex:altclick', e);
+ },
+
+ onVertexMarkerContextMenu: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Vertex events
+ // 🍂event editable:vertex:contextmenu: VertexEvent
+ // Fired when a `contextmenu` is issued on a vertex.
+ this.fireAndForward('editable:vertex:contextmenu', e);
+ },
+
+ onVertexMarkerMouseDown: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Vertex events
+ // 🍂event editable:vertex:mousedown: VertexEvent
+ // Fired when user `mousedown` a vertex.
+ this.fireAndForward('editable:vertex:mousedown', e);
+ },
+
+ onMiddleMarkerMouseDown: function (e) {
+ // 🍂namespace Editable
+ // 🍂section MiddleMarker events
+ // 🍂event editable:middlemarker:mousedown: VertexEvent
+ // Fired when user `mousedown` a middle marker.
+ this.fireAndForward('editable:middlemarker:mousedown', e);
+ },
+
+ onVertexMarkerDrag: function (e) {
+ this.onMove(e);
+ if (this.feature._bounds) this.extendBounds(e);
+ // 🍂namespace Editable
+ // 🍂section Vertex events
+ // 🍂event editable:vertex:drag: VertexEvent
+ // Fired when a vertex is dragged by user.
+ this.fireAndForward('editable:vertex:drag', e);
+ },
+
+ onVertexMarkerDragStart: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Vertex events
+ // 🍂event editable:vertex:dragstart: VertexEvent
+ // Fired before a vertex is dragged by user.
+ this.fireAndForward('editable:vertex:dragstart', e);
+ },
+
+ onVertexMarkerDragEnd: function (e) {
+ // 🍂namespace Editable
+ // 🍂section Vertex events
+ // 🍂event editable:vertex:dragend: VertexEvent
+ // Fired after a vertex is dragged by user.
+ this.fireAndForward('editable:vertex:dragend', e);
+ },
+
+ setDrawnLatLngs: function (latlngs) {
+ this._drawnLatLngs = latlngs || this.getDefaultLatLngs();
+ },
+
+ startDrawing: function () {
+ if (!this._drawnLatLngs) this.setDrawnLatLngs();
+ L.Editable.BaseEditor.prototype.startDrawing.call(this);
+ },
+
+ startDrawingForward: function () {
+ this.startDrawing();
+ },
+
+ endDrawing: function () {
+ this.tools.detachForwardLineGuide();
+ this.tools.detachBackwardLineGuide();
+ if (this._drawnLatLngs && this._drawnLatLngs.length < this.MIN_VERTEX) this.deleteShape(this._drawnLatLngs);
+ L.Editable.BaseEditor.prototype.endDrawing.call(this);
+ delete this._drawnLatLngs;
+ },
+
+ addLatLng: function (latlng) {
+ if (this._drawing === L.Editable.FORWARD) this._drawnLatLngs.push(latlng);
+ else this._drawnLatLngs.unshift(latlng);
+ this.feature._bounds.extend(latlng);
+ this.addVertexMarker(latlng, this._drawnLatLngs);
+ this.refresh();
+ },
+
+ newPointForward: function (latlng) {
+ this.addLatLng(latlng);
+ this.tools.attachForwardLineGuide();
+ this.tools.anchorForwardLineGuide(latlng);
+ },
+
+ newPointBackward: function (latlng) {
+ this.addLatLng(latlng);
+ this.tools.anchorBackwardLineGuide(latlng);
+ },
+
+ // 🍂namespace PathEditor
+ // 🍂method push()
+ // Programmatically add a point while drawing.
+ push: function (latlng) {
+ if (!latlng) return console.error('L.Editable.PathEditor.push expect a vaild latlng as parameter');
+ if (this._drawing === L.Editable.FORWARD) this.newPointForward(latlng);
+ else this.newPointBackward(latlng);
+ },
+
+ removeLatLng: function (latlng) {
+ latlng.__vertex.delete();
+ this.refresh();
+ },
+
+ // 🍂method pop(): L.LatLng or null
+ // Programmatically remove last point (if any) while drawing.
+ pop: function () {
+ if (this._drawnLatLngs.length <= 1) return;
+ var latlng;
+ if (this._drawing === L.Editable.FORWARD) latlng = this._drawnLatLngs[this._drawnLatLngs.length - 1];
+ else latlng = this._drawnLatLngs[0];
+ this.removeLatLng(latlng);
+ if (this._drawing === L.Editable.FORWARD) this.tools.anchorForwardLineGuide(this._drawnLatLngs[this._drawnLatLngs.length - 1]);
+ else this.tools.anchorForwardLineGuide(this._drawnLatLngs[0]);
+ return latlng;
+ },
+
+ processDrawingClick: function (e) {
+ if (e.vertex && e.vertex.editor === this) return;
+ if (this._drawing === L.Editable.FORWARD) this.newPointForward(e.latlng);
+ else this.newPointBackward(e.latlng);
+ this.fireAndForward('editable:drawing:clicked', e);
+ },
+
+ onDrawingMouseMove: function (e) {
+ L.Editable.BaseEditor.prototype.onDrawingMouseMove.call(this, e);
+ if (this._drawing) {
+ this.tools.moveForwardLineGuide(e.latlng);
+ this.tools.moveBackwardLineGuide(e.latlng);
+ }
+ },
+
+ refresh: function () {
+ this.feature.redraw();
+ this.onEditing();
+ },
+
+ // 🍂namespace PathEditor
+ // 🍂method newShape(latlng?: L.LatLng)
+ // Add a new shape (Polyline, Polygon) in a multi, and setup up drawing tools to draw it;
+ // if optional `latlng` is given, start a path at this point.
+ newShape: function (latlng) {
+ var shape = this.addNewEmptyShape();
+ if (!shape) return;
+ this.setDrawnLatLngs(shape[0] || shape); // Polygon or polyline
+ this.startDrawingForward();
+ // 🍂namespace Editable
+ // 🍂section Shape events
+ // 🍂event editable:shape:new: ShapeEvent
+ // Fired when a new shape is created in a multi (Polygon or Polyline).
+ this.fireAndForward('editable:shape:new', { shape: shape });
+ if (latlng) this.newPointForward(latlng);
+ },
+
+ deleteShape: function (shape, latlngs) {
+ var e = { shape: shape };
+ L.Editable.makeCancellable(e);
+ // 🍂namespace Editable
+ // 🍂section Shape events
+ // 🍂event editable:shape:delete: CancelableShapeEvent
+ // Fired before a new shape is deleted in a multi (Polygon or Polyline).
+ this.fireAndForward('editable:shape:delete', e);
+ if (e._cancelled) return;
+ shape = this._deleteShape(shape, latlngs);
+ if (this.ensureNotFlat) this.ensureNotFlat(); // Polygon.
+ this.feature.setLatLngs(this.getLatLngs()); // Force bounds reset.
+ this.refresh();
+ this.reset();
+ // 🍂namespace Editable
+ // 🍂section Shape events
+ // 🍂event editable:shape:deleted: ShapeEvent
+ // Fired after a new shape is deleted in a multi (Polygon or Polyline).
+ this.fireAndForward('editable:shape:deleted', { shape: shape });
+ return shape;
+ },
+
+ _deleteShape: function (shape, latlngs) {
+ latlngs = latlngs || this.getLatLngs();
+ if (!latlngs.length) return;
+ var self = this,
+ inplaceDelete = function (latlngs, shape) {
+ // Called when deleting a flat latlngs
+ shape = latlngs.splice(0, Number.MAX_VALUE);
+ return shape;
+ },
+ spliceDelete = function (latlngs, shape) {
+ // Called when removing a latlngs inside an array
+ latlngs.splice(latlngs.indexOf(shape), 1);
+ if (!latlngs.length) self._deleteShape(latlngs);
+ return shape;
+ };
+ if (latlngs === shape) return inplaceDelete(latlngs, shape);
+ for (var i = 0; i < latlngs.length; i++) {
+ if (latlngs[i] === shape) return spliceDelete(latlngs, shape);
+ else if (latlngs[i].indexOf(shape) !== -1) return spliceDelete(latlngs[i], shape);
+ }
+ },
+
+ // 🍂namespace PathEditor
+ // 🍂method deleteShapeAt(latlng: L.LatLng): Array
+ // Remove a path shape at the given `latlng`.
+ deleteShapeAt: function (latlng) {
+ var shape = this.feature.shapeAt(latlng);
+ if (shape) return this.deleteShape(shape);
+ },
+
+ // 🍂method appendShape(shape: Array)
+ // Append a new shape to the Polygon or Polyline.
+ appendShape: function (shape) {
+ this.insertShape(shape);
+ },
+
+ // 🍂method prependShape(shape: Array)
+ // Prepend a new shape to the Polygon or Polyline.
+ prependShape: function (shape) {
+ this.insertShape(shape, 0);
+ },
+
+ // 🍂method insertShape(shape: Array, index: int)
+ // Insert a new shape to the Polygon or Polyline at given index (default is to append).
+ insertShape: function (shape, index) {
+ this.ensureMulti();
+ shape = this.formatShape(shape);
+ if (typeof index === 'undefined') index = this.feature._latlngs.length;
+ this.feature._latlngs.splice(index, 0, shape);
+ this.feature.redraw();
+ if (this._enabled) this.reset();
+ },
+
+ extendBounds: function (e) {
+ this.feature._bounds.extend(e.vertex.latlng);
+ },
+
+ onDragStart: function (e) {
+ this.editLayer.clearLayers();
+ L.Editable.BaseEditor.prototype.onDragStart.call(this, e);
+ },
+
+ onDragEnd: function (e) {
+ this.initVertexMarkers();
+ L.Editable.BaseEditor.prototype.onDragEnd.call(this, e);
+ }
+
+ });
+
+ // 🍂namespace Editable; 🍂class PolylineEditor; 🍂aka L.Editable.PolylineEditor
+ // 🍂inherits PathEditor
+ L.Editable.PolylineEditor = L.Editable.PathEditor.extend({
+
+ startDrawingBackward: function () {
+ this._drawing = L.Editable.BACKWARD;
+ this.startDrawing();
+ },
+
+ // 🍂method continueBackward(latlngs?: Array)
+ // Set up drawing tools to continue the line backward.
+ continueBackward: function (latlngs) {
+ if (this.drawing()) return;
+ latlngs = latlngs || this.getDefaultLatLngs();
+ this.setDrawnLatLngs(latlngs);
+ if (latlngs.length > 0) {
+ this.tools.attachBackwardLineGuide();
+ this.tools.anchorBackwardLineGuide(latlngs[0]);
+ }
+ this.startDrawingBackward();
+ },
+
+ // 🍂method continueForward(latlngs?: Array)
+ // Set up drawing tools to continue the line forward.
+ continueForward: function (latlngs) {
+ if (this.drawing()) return;
+ latlngs = latlngs || this.getDefaultLatLngs();
+ this.setDrawnLatLngs(latlngs);
+ if (latlngs.length > 0) {
+ this.tools.attachForwardLineGuide();
+ this.tools.anchorForwardLineGuide(latlngs[latlngs.length - 1]);
+ }
+ this.startDrawingForward();
+ },
+
+ getDefaultLatLngs: function (latlngs) {
+ latlngs = latlngs || this.feature._latlngs;
+ if (!latlngs.length || latlngs[0] instanceof L.LatLng) return latlngs;
+ else return this.getDefaultLatLngs(latlngs[0]);
+ },
+
+ ensureMulti: function () {
+ if (this.feature._latlngs.length && L.Polyline._flat(this.feature._latlngs)) {
+ this.feature._latlngs = [this.feature._latlngs];
+ }
+ },
+
+ addNewEmptyShape: function () {
+ if (this.feature._latlngs.length) {
+ var shape = [];
+ this.appendShape(shape);
+ return shape;
+ } else {
+ return this.feature._latlngs;
+ }
+ },
+
+ formatShape: function (shape) {
+ if (L.Polyline._flat(shape)) return shape;
+ else if (shape[0]) return this.formatShape(shape[0]);
+ },
+
+ // 🍂method splitShape(latlngs?: Array, index: int)
+ // Split the given `latlngs` shape at index `index` and integrate new shape in instance `latlngs`.
+ splitShape: function (shape, index) {
+ if (!index || index >= shape.length - 1) return;
+ this.ensureMulti();
+ var shapeIndex = this.feature._latlngs.indexOf(shape);
+ if (shapeIndex === -1) return;
+ var first = shape.slice(0, index + 1),
+ second = shape.slice(index);
+ // We deal with reference, we don't want twice the same latlng around.
+ second[0] = L.latLng(second[0].lat, second[0].lng, second[0].alt);
+ this.feature._latlngs.splice(shapeIndex, 1, first, second);
+ this.refresh();
+ this.reset();
+ }
+
+ });
+
+ // 🍂namespace Editable; 🍂class PolygonEditor; 🍂aka L.Editable.PolygonEditor
+ // 🍂inherits PathEditor
+ L.Editable.PolygonEditor = L.Editable.PathEditor.extend({
+
+ CLOSED: true,
+ MIN_VERTEX: 3,
+
+ newPointForward: function (latlng) {
+ L.Editable.PathEditor.prototype.newPointForward.call(this, latlng);
+ if (!this.tools.backwardLineGuide._latlngs.length) this.tools.anchorBackwardLineGuide(latlng);
+ if (this._drawnLatLngs.length === 2) this.tools.attachBackwardLineGuide();
+ },
+
+ addNewEmptyHole: function (latlng) {
+ this.ensureNotFlat();
+ var latlngs = this.feature.shapeAt(latlng);
+ if (!latlngs) return;
+ var holes = [];
+ latlngs.push(holes);
+ return holes;
+ },
+
+ // 🍂method newHole(latlng?: L.LatLng, index: int)
+ // Set up drawing tools for creating a new hole on the Polygon. If the `latlng` param is given, a first point is created.
+ newHole: function (latlng) {
+ var holes = this.addNewEmptyHole(latlng);
+ if (!holes) return;
+ this.setDrawnLatLngs(holes);
+ this.startDrawingForward();
+ if (latlng) this.newPointForward(latlng);
+ },
+
+ addNewEmptyShape: function () {
+ if (this.feature._latlngs.length && this.feature._latlngs[0].length) {
+ var shape = [];
+ this.appendShape(shape);
+ return shape;
+ } else {
+ return this.feature._latlngs;
+ }
+ },
+
+ ensureMulti: function () {
+ if (this.feature._latlngs.length && L.Polyline._flat(this.feature._latlngs[0])) {
+ this.feature._latlngs = [this.feature._latlngs];
+ }
+ },
+
+ ensureNotFlat: function () {
+ if (!this.feature._latlngs.length || L.Polyline._flat(this.feature._latlngs)) this.feature._latlngs = [this.feature._latlngs];
+ },
+
+ vertexCanBeDeleted: function (vertex) {
+ var parent = this.feature.parentShape(vertex.latlngs),
+ idx = L.Util.indexOf(parent, vertex.latlngs);
+ if (idx > 0) return true; // Holes can be totally deleted without removing the layer itself.
+ return L.Editable.PathEditor.prototype.vertexCanBeDeleted.call(this, vertex);
+ },
+
+ getDefaultLatLngs: function () {
+ if (!this.feature._latlngs.length) this.feature._latlngs.push([]);
+ return this.feature._latlngs[0];
+ },
+
+ formatShape: function (shape) {
+ // [[1, 2], [3, 4]] => must be nested
+ // [] => must be nested
+ // [[]] => is already nested
+ if (L.Polyline._flat(shape) && (!shape[0] || shape[0].length !== 0)) return [shape];
+ else return shape;
+ }
+
+ });
+
+ // 🍂namespace Editable; 🍂class RectangleEditor; 🍂aka L.Editable.RectangleEditor
+ // 🍂inherits PathEditor
+ L.Editable.RectangleEditor = L.Editable.PathEditor.extend({
+
+ CLOSED: true,
+ MIN_VERTEX: 4,
+
+ options: {
+ skipMiddleMarkers: true
+ },
+
+ extendBounds: function (e) {
+ var index = e.vertex.getIndex(),
+ next = e.vertex.getNext(),
+ previous = e.vertex.getPrevious(),
+ oppositeIndex = (index + 2) % 4,
+ opposite = e.vertex.latlngs[oppositeIndex],
+ bounds = new L.LatLngBounds(e.latlng, opposite);
+ // Update latlngs by hand to preserve order.
+ previous.latlng.update([e.latlng.lat, opposite.lng]);
+ next.latlng.update([opposite.lat, e.latlng.lng]);
+ this.updateBounds(bounds);
+ this.refreshVertexMarkers();
+ },
+
+ onDrawingMouseDown: function (e) {
+ L.Editable.PathEditor.prototype.onDrawingMouseDown.call(this, e);
+ this.connect();
+ var latlngs = this.getDefaultLatLngs();
+ // L.Polygon._convertLatLngs removes last latlng if it equals first point,
+ // which is the case here as all latlngs are [0, 0]
+ if (latlngs.length === 3) latlngs.push(e.latlng);
+ var bounds = new L.LatLngBounds(e.latlng, e.latlng);
+ this.updateBounds(bounds);
+ this.updateLatLngs(bounds);
+ this.refresh();
+ this.reset();
+ // Stop dragging map.
+ // L.Draggable has two workflows:
+ // - mousedown => mousemove => mouseup
+ // - touchstart => touchmove => touchend
+ // Problem: L.Map.Tap does not allow us to listen to touchstart, so we only
+ // can deal with mousedown, but then when in a touch device, we are dealing with
+ // simulated events (actually simulated by L.Map.Tap), which are no more taken
+ // into account by L.Draggable.
+ // Ref.: https://github.com/Leaflet/Leaflet.Editable/issues/103
+ e.originalEvent._simulated = false;
+ this.map.dragging._draggable._onUp(e.originalEvent);
+ // Now transfer ongoing drag action to the bottom right corner.
+ // Should we refine which corne will handle the drag according to
+ // drag direction?
+ latlngs[3].__vertex.dragging._draggable._onDown(e.originalEvent);
+ },
+
+ onDrawingMouseUp: function (e) {
+ this.commitDrawing(e);
+ e.originalEvent._simulated = false;
+ L.Editable.PathEditor.prototype.onDrawingMouseUp.call(this, e);
+ },
+
+ onDrawingMouseMove: function (e) {
+ e.originalEvent._simulated = false;
+ L.Editable.PathEditor.prototype.onDrawingMouseMove.call(this, e);
+ },
+
+
+ getDefaultLatLngs: function (latlngs) {
+ return latlngs || this.feature._latlngs[0];
+ },
+
+ updateBounds: function (bounds) {
+ this.feature._bounds = bounds;
+ },
+
+ updateLatLngs: function (bounds) {
+ var latlngs = this.getDefaultLatLngs(),
+ newLatlngs = this.feature._boundsToLatLngs(bounds);
+ // Keep references.
+ for (var i = 0; i < latlngs.length; i++) {
+ latlngs[i].update(newLatlngs[i]);
+ };
+ }
+
+ });
+
+ // 🍂namespace Editable; 🍂class CircleEditor; 🍂aka L.Editable.CircleEditor
+ // 🍂inherits PathEditor
+ L.Editable.CircleEditor = L.Editable.PathEditor.extend({
+
+ MIN_VERTEX: 2,
+
+ options: {
+ skipMiddleMarkers: true
+ },
+
+ initialize: function (map, feature, options) {
+ L.Editable.PathEditor.prototype.initialize.call(this, map, feature, options);
+ this._resizeLatLng = this.computeResizeLatLng();
+ },
+
+ computeResizeLatLng: 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]);
+ },
+
+ updateResizeLatLng: function () {
+ this._resizeLatLng.update(this.computeResizeLatLng());
+ this._resizeLatLng.__vertex.update();
+ },
+
+ getLatLngs: function () {
+ return [this.feature._latlng, this._resizeLatLng];
+ },
+
+ getDefaultLatLngs: function () {
+ return this.getLatLngs();
+ },
+
+ onVertexMarkerDrag: function (e) {
+ if (e.vertex.getIndex() === 1) this.resize(e);
+ else this.updateResizeLatLng(e);
+ L.Editable.PathEditor.prototype.onVertexMarkerDrag.call(this, e);
+ },
+
+ resize: function (e) {
+ var radius = this.feature._latlng.distanceTo(e.latlng)
+ this.feature.setRadius(radius);
+ },
+
+ onDrawingMouseDown: function (e) {
+ L.Editable.PathEditor.prototype.onDrawingMouseDown.call(this, e);
+ this._resizeLatLng.update(e.latlng);
+ this.feature._latlng.update(e.latlng);
+ this.connect();
+ // Stop dragging map.
+ e.originalEvent._simulated = false;
+ this.map.dragging._draggable._onUp(e.originalEvent);
+ // Now transfer ongoing drag action to the radius handler.
+ this._resizeLatLng.__vertex.dragging._draggable._onDown(e.originalEvent);
+ },
+
+ onDrawingMouseUp: function (e) {
+ this.commitDrawing(e);
+ e.originalEvent._simulated = false;
+ L.Editable.PathEditor.prototype.onDrawingMouseUp.call(this, e);
+ },
+
+ onDrawingMouseMove: function (e) {
+ e.originalEvent._simulated = false;
+ L.Editable.PathEditor.prototype.onDrawingMouseMove.call(this, e);
+ },
+
+ onDrag: function (e) {
+ L.Editable.PathEditor.prototype.onDrag.call(this, e);
+ this.feature.dragging.updateLatLng(this._resizeLatLng);
+ }
+
+ });
+
+ // 🍂namespace Editable; 🍂class EditableMixin
+ // `EditableMixin` is included to `L.Polyline`, `L.Polygon`, `L.Rectangle`, `L.Circle`
+ // and `L.Marker`. It adds some methods to them.
+ // *When editing is enabled, the editor is accessible on the instance with the
+ // `editor` property.*
+ var EditableMixin = {
+
+ createEditor: function (map) {
+ map = map || this._map;
+ var tools = (this.options.editOptions || {}).editTools || map.editTools;
+ if (!tools) throw Error('Unable to detect Editable instance.')
+ var Klass = this.options.editorClass || this.getEditorClass(tools);
+ return new Klass(map, this, this.options.editOptions);
+ },
+
+ // 🍂method enableEdit(map?: L.Map): this.editor
+ // Enable editing, by creating an editor if not existing, and then calling `enable` on it.
+ enableEdit: function (map) {
+ if (!this.editor) this.createEditor(map);
+ this.editor.enable();
+ return this.editor;
+ },
+
+ // 🍂method editEnabled(): boolean
+ // Return true if current instance has an editor attached, and this editor is enabled.
+ editEnabled: function () {
+ return this.editor && this.editor.enabled();
+ },
+
+ // 🍂method disableEdit()
+ // Disable editing, also remove the editor property reference.
+ disableEdit: function () {
+ if (this.editor) {
+ this.editor.disable();
+ delete this.editor;
+ }
+ },
+
+ // 🍂method toggleEdit()
+ // Enable or disable editing, according to current status.
+ toggleEdit: function () {
+ if (this.editEnabled()) this.disableEdit();
+ else this.enableEdit();
+ },
+
+ _onEditableAdd: function () {
+ if (this.editor) this.enableEdit();
+ }
+
+ };
+
+ var PolylineMixin = {
+
+ getEditorClass: function (tools) {
+ return (tools && tools.options.polylineEditorClass) ? tools.options.polylineEditorClass : L.Editable.PolylineEditor;
+ },
+
+ shapeAt: function (latlng, latlngs) {
+ // We can have those cases:
+ // - latlngs are just a flat array of latlngs, use this
+ // - latlngs is an array of arrays of latlngs, loop over
+ var shape = null;
+ latlngs = latlngs || this._latlngs;
+ if (!latlngs.length) return shape;
+ else if (L.Polyline._flat(latlngs) && this.isInLatLngs(latlng, latlngs)) shape = latlngs;
+ else
+ for (var i = 0; i < latlngs.length; i++)
+ if (this.isInLatLngs(latlng, latlngs[i])) return latlngs[i];
+ return shape;
+ },
+
+ isInLatLngs: function (l, latlngs) {
+ if (!latlngs) return false;
+ var i, k, len, part = [],
+ p,
+ w = this._clickTolerance();
+ this._projectLatlngs(latlngs, part, this._pxBounds);
+ part = part[0];
+ p = this._map.latLngToLayerPoint(l);
+
+ if (!this._pxBounds.contains(p)) { return false; }
+ for (i = 1, len = part.length, k = 0; i < len; k = i++) {
+
+ if (L.LineUtil.pointToSegmentDistance(p, part[k], part[i]) <= w) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ };
+
+ var PolygonMixin = {
+
+ getEditorClass: function (tools) {
+ return (tools && tools.options.polygonEditorClass) ? tools.options.polygonEditorClass : L.Editable.PolygonEditor;
+ },
+
+ shapeAt: function (latlng, latlngs) {
+ // We can have those cases:
+ // - latlngs are just a flat array of latlngs, use this
+ // - latlngs is an array of arrays of latlngs, this is a simple polygon (maybe with holes), use the first
+ // - latlngs is an array of arrays of arrays, this is a multi, loop over
+ var shape = null;
+ latlngs = latlngs || this._latlngs;
+ if (!latlngs.length) return shape;
+ else if (L.Polyline._flat(latlngs) && this.isInLatLngs(latlng, latlngs)) shape = latlngs;
+ else if (L.Polyline._flat(latlngs[0]) && this.isInLatLngs(latlng, latlngs[0])) shape = latlngs;
+ else
+ for (var i = 0; i < latlngs.length; i++)
+ if (this.isInLatLngs(latlng, latlngs[i][0])) return latlngs[i];
+ return shape;
+ },
+
+ isInLatLngs: function (l, latlngs) {
+ var inside = false,
+ l1, l2, j, k, len2;
+
+ for (j = 0, len2 = latlngs.length, k = len2 - 1; j < len2; k = j++) {
+ l1 = latlngs[j];
+ l2 = latlngs[k];
+
+ if (((l1.lat > l.lat) !== (l2.lat > l.lat)) &&
+ (l.lng < (l2.lng - l1.lng) * (l.lat - l1.lat) / (l2.lat - l1.lat) + l1.lng)) {
+ inside = !inside;
+ }
+ }
+
+ return inside;
+ },
+
+ parentShape: function (shape, latlngs) {
+ latlngs = latlngs || this._latlngs;
+ if (!latlngs) return;
+ var idx = L.Util.indexOf(latlngs, shape);
+ if (idx !== -1) return latlngs;
+ for (var i = 0; i < latlngs.length; i++) {
+ idx = L.Util.indexOf(latlngs[i], shape);
+ if (idx !== -1) return latlngs[i];
+ }
+ }
+
+ };
+
+
+ var MarkerMixin = {
+
+ getEditorClass: function (tools) {
+ return (tools && tools.options.markerEditorClass) ? tools.options.markerEditorClass : L.Editable.MarkerEditor;
+ }
+
+ };
+
+ var RectangleMixin = {
+
+ getEditorClass: function (tools) {
+ return (tools && tools.options.rectangleEditorClass) ? tools.options.rectangleEditorClass : L.Editable.RectangleEditor;
+ }
+
+ };
+
+ var CircleMixin = {
+
+ getEditorClass: function (tools) {
+ return (tools && tools.options.circleEditorClass) ? tools.options.circleEditorClass : L.Editable.CircleEditor;
+ }
+
+ };
+
+ var keepEditable = function () {
+ // Make sure you can remove/readd an editable layer.
+ this.on('add', this._onEditableAdd);
+ };
+
+
+
+ if (L.Polyline) {
+ L.Polyline.include(EditableMixin);
+ L.Polyline.include(PolylineMixin);
+ L.Polyline.addInitHook(keepEditable);
+ }
+ if (L.Polygon) {
+ L.Polygon.include(EditableMixin);
+ L.Polygon.include(PolygonMixin);
+ }
+ if (L.Marker) {
+ L.Marker.include(EditableMixin);
+ L.Marker.include(MarkerMixin);
+ L.Marker.addInitHook(keepEditable);
+ }
+ if (L.Rectangle) {
+ L.Rectangle.include(EditableMixin);
+ L.Rectangle.include(RectangleMixin);
+ }
+ if (L.Circle) {
+ L.Circle.include(EditableMixin);
+ L.Circle.include(CircleMixin);
+ }
+
+ L.LatLng.prototype.update = function (latlng) {
+ latlng = L.latLng(latlng);
+ this.lat = latlng.lat;
+ this.lng = latlng.lng;
+ }
+
+}, window));
diff --git a/cadasta/core/static/js/smap/edit/controls.js b/cadasta/core/static/js/smap/edit/controls.js
new file mode 100644
index 000000000..c1835abca
--- /dev/null
+++ b/cadasta/core/static/js/smap/edit/controls.js
@@ -0,0 +1,299 @@
+var EditorToolbars = function () {
+
+ var SubToolbar = L.Toolbar.extend({});
+
+ var BaseToolbarAction = L.ToolbarAction.extend({
+ initialize: function (map, options) {
+ this.map = map;
+ this.editor = map.locationEditor;
+ this.tooltip = map.locationEditor.tooltip;
+ L.setOptions(this, options);
+ L.ToolbarAction.prototype.initialize.call(this, options);
+ }
+ });
+
+ var SubAction = L.ToolbarAction.extend({
+ initialize: function (map, action, options) {
+ this.map = map;
+ this.editor = map.locationEditor;
+ this.action = action;
+ L.setOptions(this, options);
+ L.ToolbarAction.prototype.initialize.call(this, options);
+ },
+ addHooks: function () {
+ this.action.disable();
+ }
+ });
+
+ var CancelAction = SubAction.extend({
+ options: {
+ toolbarIcon: {
+ html: 'Cancel',
+ className: 'cancel-draw',
+ tooltip: 'Cancel drawing'
+ },
+ },
+ addHooks: function () {
+ this.editor.cancelDrawing();
+ SubAction.prototype.addHooks.call(this);
+ }
+ });
+
+ // draw toolbars
+
+ var DrawControl = BaseToolbarAction.extend({
+ enable: function () {
+ if (this.editor.deleting()) return;
+ var layer = this.editor.location.layer;
+ if (layer) {
+ var currentEditor = layer.editor;
+ if (currentEditor) {
+ if (currentEditor instanceof this.options.type) {
+ BaseToolbarAction.prototype.enable.call(this);
+ }
+ } else {
+ BaseToolbarAction.prototype.enable.call(this);
+ }
+ } else {
+ BaseToolbarAction.prototype.enable.call(this);
+ }
+ }
+ });
+
+ var LineControl = DrawControl.extend({
+ options: {
+ toolbarIcon: {
+ tooltip: 'Draw line',
+ className: 'cadasta-toolbar draw-polyline',
+ },
+ subToolbar: new SubToolbar({
+ className: 'cancel-draw-line leaflet-subtoolbar',
+ actions: [CancelAction],
+ }),
+ type: L.Editable.PolylineEditor,
+ },
+ addHooks: function () {
+ var layer = this.editor.location.layer;
+ if (layer) {
+ var currentEditor = layer.editor;
+ if (currentEditor) {
+ if (currentEditor.enabled() &&
+ currentEditor instanceof this.options.type) {
+ this.editor.addMulti(this.options.type);
+ }
+ } else {
+ this.editor.startPolyline();
+ }
+ } else {
+ this.editor.startPolyline();
+ }
+ },
+ });
+
+ var PolygonControl = DrawControl.extend({
+ options: {
+ toolbarIcon: {
+ tooltip: 'Draw a polygon',
+ className: 'cadasta-toolbar draw-polygon',
+ },
+ subToolbar: new SubToolbar({
+ className: 'cancel-draw-poly leaflet-subtoolbar',
+ actions: [CancelAction],
+ }),
+ type: L.Editable.PolygonEditor,
+ },
+ addHooks: function (e) {
+ var layer = this.editor.location.layer;
+ if (layer) {
+ var currentEditor = layer.editor;
+ if (currentEditor) {
+ if (currentEditor.enabled() &&
+ currentEditor instanceof this.options.type) {
+ this.editor.addMulti(e, this.options.type);
+ }
+ } else {
+ this.editor.startPolygon();
+ }
+ } else {
+ this.editor.startPolygon();
+ }
+ },
+ });
+
+ var RectangleControl = DrawControl.extend({
+ options: {
+ toolbarIcon: {
+ tooltip: 'Draw a rectangle',
+ className: 'cadasta-toolbar draw-rectangle',
+ },
+ subToolbar: new SubToolbar({
+ className: 'cancel-draw-rect leaflet-subtoolbar',
+ actions: [CancelAction],
+ }),
+ type: L.Editable.RectangleEditor,
+ },
+ addHooks: function () {
+ this.editor.startRectangle();
+ },
+ enable: function () {
+ if (this.editor.deleting()) return;
+ var layer = this.editor.location.layer;
+ if (layer) {
+ var currentEditor = layer.editor;
+ if (currentEditor && currentEditor instanceof this.options.type) return;
+ DrawControl.prototype.enable.call(this);
+ } else {
+ DrawControl.prototype.enable.call(this);
+ }
+ }
+ });
+
+ var MarkerControl = DrawControl.extend({
+ options: {
+ toolbarIcon: {
+ tooltip: 'Draw marker',
+ className: 'cadasta-toolbar draw-marker'
+ },
+ subToolbar: new SubToolbar({
+ className: 'cancel-draw-marker leaflet-subtoolbar',
+ actions: [CancelAction],
+ }),
+ type: L.Editable.MarkerEditor,
+ },
+ addHooks: function () {
+ this.editor.startMarker();
+ }
+ });
+
+
+
+ // edit tools
+
+ var SaveEdit = SubAction.extend({
+ options: {
+ toolbarIcon: {
+ html: 'Save',
+ tooltip: 'Save edits',
+ }
+ },
+ addHooks: function () {
+ this.editor.save();
+ SubAction.prototype.addHooks.call(this);
+ }
+ });
+
+ var CancelEdit = SubAction.extend({
+ options: {
+ toolbarIcon: {
+ html: 'Cancel',
+ className: 'cancel-edit',
+ tooltip: 'Cancel edits'
+ },
+ },
+ addHooks: function () {
+ this.editor.cancelEdit();
+ SubAction.prototype.addHooks.call(this);
+ }
+ });
+
+ var SaveDelete = SubAction.extend({
+ options: {
+ toolbarIcon: {
+ html: 'Save',
+ className: 'save-delete',
+ tooltip: 'Save changes'
+ },
+ },
+ addHooks: function () {
+ this.editor.delete();
+ SubAction.prototype.addHooks.call(this);
+ }
+ });
+
+ var CancelDelete = SubAction.extend({
+ options: {
+ toolbarIcon: {
+ html: 'Cancel',
+ className: 'cancel-delete',
+ tooltip: 'Cancel deletion'
+ },
+ },
+ addHooks: function () {
+ this.editor.cancelDelete();
+ SubAction.prototype.addHooks.call(this);
+ }
+ });
+
+ // main edit actions
+
+ var EditAction = BaseToolbarAction.extend({
+ options: {
+ toolbarIcon: {
+ html: ' ',
+ className: 'edit-action',
+ tooltip: 'Edit feature',
+ },
+ subToolbar: new SubToolbar({
+ className: 'leaflet-subtoolbar',
+ actions: [SaveEdit, CancelEdit],
+ })
+ },
+ addHooks: function () {
+ this.editor.edit();
+ },
+ enable: function (e) {
+ if (this.editor.hasEditableLayer() && !this.editor.deleting()) {
+ L.ToolbarAction.prototype.enable.call(this);
+ }
+ }
+ });
+
+ var DeleteAction = BaseToolbarAction.extend({
+ options: {
+ toolbarIcon: {
+ html: '',
+ className: 'delete-action',
+ tooltip: 'Delete feature',
+ },
+ subToolbar: new SubToolbar({
+ className: 'leaflet-subtoolbar',
+ actions: [SaveDelete, CancelDelete],
+ })
+ },
+ addHooks: function () {
+ this.editor.startDelete();
+ },
+ enable: function () {
+ if (this.editor.hasEditableLayer()) {
+ L.ToolbarAction.prototype.enable.call(this);
+ }
+ },
+ disable: function () {
+ if (this.editor.deleting()) return;
+ L.ToolbarAction.prototype.disable.call(this);
+ }
+ });
+
+
+ // toolbars
+ var Toolbars = [];
+ var EditToolbar = L.Toolbar.Control.extend({});
+ var DrawToolbar = L.Toolbar.Control.extend({});
+
+ Toolbars.push(new DrawToolbar({
+ position: 'topleft',
+ className: 'leaflet-smap-draw',
+ actions: [
+ LineControl, PolygonControl,
+ RectangleControl, MarkerControl
+ ]
+ }));
+
+ Toolbars.push(new EditToolbar({
+ position: 'topleft',
+ className: 'leaflet-smap-edit',
+ actions: [EditAction, DeleteAction],
+ }));
+
+ return Toolbars;
+};
diff --git a/cadasta/core/static/js/smap/edit/editor.js b/cadasta/core/static/js/smap/edit/editor.js
new file mode 100644
index 000000000..775965ee4
--- /dev/null
+++ b/cadasta/core/static/js/smap/edit/editor.js
@@ -0,0 +1,642 @@
+/* eslint-env jquery */
+
+
+var Location = L.Editable.extend({
+
+ _deleting: false,
+ _deleted: false,
+ _new: false,
+ _dirty: false,
+
+ layer: null,
+ feature: null,
+
+ initialize: function (map, options) {
+ this._undoBuffer = {};
+ this.on('editable:drawing:start', this._drawStart, this);
+ this.on('editable:drawing:end', this._drawEnd, this);
+ L.Editable.prototype.initialize.call(this, map, options);
+ },
+
+ // edit functions
+
+ _startEdit: function () {
+ if (this.layer) {
+ if (!this.layer._new) {
+ this._backupLayer();
+ }
+ this.layer.enableEdit(this.map);
+ this.layer._dirty = true;
+ }
+ },
+
+ _stopEdit: function () {
+ if (this.layer) {
+ this.layer.disableEdit(this.map);
+ this._clearBackup();
+ }
+ },
+
+ _saveEdit: function () {
+ this.layer.disableEdit();
+ var geom = this.layer.toGeoJSON().geometry;
+ this.layer.feature.geometry = geom;
+ if (!this.layer._new) {
+ this._backupLayer();
+ }
+ var gj = JSON.stringify(geom);
+ $('textarea[name="geometry"]').html(gj);
+ this.layer._dirty = false;
+ this.layer._new = false;
+ },
+
+ _undoEdit: function () {
+ this._undo();
+ if (this.layer) this.layer._dirty = false;
+ this._deleting = this._deleted = false;
+ },
+
+ _undo: function () {
+ if (this.layer) {
+ this.layer.disableEdit();
+ latLngs = this._undoBuffer[this.layer._leaflet_id];
+ if (latLngs && latLngs.latlngs) {
+ if (this.layer instanceof L.Marker) {
+ this.layer.setLatLng(latLngs.latlngs);
+ this.map.geojsonLayer.removeLayer(this.layer);
+ this.map.geojsonLayer.addLayer(this.layer);
+ } else {
+ this.layer.setLatLngs(latLngs.latlngs);
+ this.map.geojsonLayer.removeLayer(this.layer);
+ this.map.geojsonLayer.addLayer(this.layer);
+ }
+ this._clearBackup();
+ } else {
+ this._setDeleted();
+ }
+ }
+ },
+
+ // delete functions
+
+ _startDelete: function () {
+ this.layer.disableEdit();
+ if (!this.layer._new) {
+ this._backupLayer();
+ }
+ this._deleting = true;
+ this._deleted = false;
+ },
+
+ _setDeleted: function (e) {
+ if (!this.layer._new) {
+ this._backupLayer();
+ }
+ if (this.layer instanceof L.Polyline || this.layer instanceof L.Polygon || this.layer instanceof L.Rectangle) {
+ this.layer.enableEdit();
+ this.layer.editor.deleteShape(this.layer._latlngs);
+ this.layer.disableEdit();
+ // this.map.geojsonLayer.removeLayer(this.layer);
+ this._deleted = true;
+ } else if (this.layer instanceof L.Marker) {
+ this.layer.enableEdit();
+ this.layer.remove();
+ // this.map.geojsonLayer.removeLayer(this.layer);
+ this._deleted = true;
+ }
+
+ this.map.geojsonLayer.removeLayer(this.layer);
+
+ if (this.layer._new) {
+ this.layer = null;
+ }
+
+ this.featuresLayer.clearLayers();
+ },
+
+ _undoDelete: function () {
+ this._undo();
+ this._deleted = false;
+ this._deleting = false;
+ },
+
+ _saveDelete: function () {
+ if (this._deleted) {
+ $('textarea[name="geometry"]').html('');
+ this.featuresLayer.clearLayers();
+ this._clearBackup();
+ }
+ this._deleting = false;
+ },
+
+ // draw functions
+
+ _drawStart: function (e) {},
+
+ _drawEnd: function (e) {
+ if (!this._hasDrawnFeature()) return;
+ if (!this._checkValid(e.layer)) return;
+ if (this.layer) {
+ this._update(e.layer);
+ } else {
+ this._createNew(e.layer);
+ }
+ this.featuresLayer.clearLayers();
+ this.layer.disableEdit();
+ this.layer.dragging.disable();
+ this._deleted = false;
+ this._deleting = false;
+ },
+
+ _update: function (lyr) {
+ this.layer.disableEdit();
+ var geometry = lyr.toGeoJSON().geometry;
+ var layer = LatLngUtil.copyLayer(lyr);
+ L.stamp(layer);
+ this.layer.feature.geometry = geometry;
+ layer.feature = this.layer.feature;
+ this.layer = layer;
+ if (!this.layer._new) {
+ this._backupLayer();
+ }
+ this._replaceGeoJSONFeature(this.layer);
+ this.layer._dirty = true;
+ },
+
+ _createNew: function (lyr) {
+ var feature = lyr.toGeoJSON();
+ var layer = LatLngUtil.copyLayer(lyr);
+ feature.id = L.stamp(layer);
+ layer.feature = feature;
+
+ this.layer = layer;
+ this.map.geojsonLayer.addLayer(this.layer);
+ this.layer._new = true;
+ this.layer._dirty = true;
+ },
+
+ _checkValid: function (layer) {
+ if (layer instanceof L.Polygon || layer instanceof L.Rectangle || layer instanceof L.Polyline) {
+ // check if a feature has been drawn
+ if (!layer.getBounds().isValid()) return false;
+ var bounds = layer.getBounds(),
+ nw = bounds.getNorthWest(),
+ se = bounds.getSouthEast();
+ if (nw.lat === se.lat && nw.lng === se.lng) {
+ this.featuresLayer.removeLayer(layer);
+ return false;
+ }
+ return true;
+ } else {
+ return true;
+ }
+ },
+
+ // utils
+
+ _replaceGeoJSONFeature: function (layer) {
+ this.map.geojsonLayer.eachLayer(function (l) {
+ if (l.feature.id === layer.feature.id) {
+ l.remove();
+ this.map.geojsonLayer.addLayer(layer);
+ }
+ }, this);
+ },
+
+ _findLayer: function (fid) {
+ var layer = null;
+ this.map.geojsonLayer.eachLayer(function (l) {
+ if (l.feature.id === fid) {
+ layer = l;
+ return;
+ }
+ });
+ return layer;
+ },
+
+ _backupLayer: function () {
+ this._undoBuffer = {};
+ if (this.layer instanceof L.Polyline || this.layer instanceof L.Polygon || this.layer instanceof L.Rectangle) {
+ this._undoBuffer[this.layer._leaflet_id] = {
+ latlngs: LatLngUtil.cloneLatLngs(this.layer.getLatLngs()),
+ };
+ }
+ if (this.layer instanceof L.Marker) {
+ this._undoBuffer[this.layer._leaflet_id] = {
+ latlngs: LatLngUtil.cloneLatLng(this.layer.getLatLng()),
+ };
+ }
+ },
+
+ _hasDrawnFeature: function () {
+ return this.featuresLayer.getLayers().length > 0;
+ },
+
+ _clearBackup: function () {
+ this._undoBuffer = {};
+ },
+
+ _reset: function () {
+ this.layer = null;
+ this.feature = null;
+ this.featuresLayer.clearLayers();
+ this._clearBackup();
+ },
+
+});
+
+
+var LocationEditor = L.Evented.extend({
+
+ _editing: false,
+ _prevent_click: false,
+
+ initialize: function (map, options) {
+ this.map = map;
+ map.locationEditor = this;
+ this.location = new Location(map);
+ map.editTools = this.location;
+ this.toolbars = new EditorToolbars();
+
+ this.tooltip = new Tooltip(map);
+
+ this._addDeleteEvent();
+ this._addRouterEvents();
+ this._addEditableEvents();
+ },
+
+ onLayerClick: function (e) {
+ if (this.dirty() && !this.deleting()) return;
+ var feature = e.target.feature;
+ var layer = e.layer || e.target;
+ if (this.preventClick() && feature.id !== this.location.layer.feature.id) return;
+ if (this.editing() && feature.id !== this.location.layer.feature.id) return;
+ if (this.deleting()) {
+ this.deleteLayer(layer, e);
+ return;
+ }
+ if (!this.editing()) {
+ window.location.href = "#/" + feature.properties.url + '/';
+ }
+ this.setEditable(feature, layer);
+ },
+
+ // edit functions
+
+ setEditable: function (feature, layer) {
+ if (this.location.layer) {
+ Styles.resetStyle(this.location.layer);
+ }
+ layer.feature = feature;
+ this.location.layer = layer;
+ this.location.feature = feature;
+ Styles.setSelectedStyle(layer);
+ },
+
+ edit: function () {
+ this.tooltip.update(this.tooltip.EDIT_ENABLED);
+ if (this.editing()) {
+ return;
+ }
+ this.location._startEdit();
+ },
+
+ cancelEdit: function () {
+ this.tooltip.remove();
+ this.location._undoEdit();
+ },
+
+ editing: function () {
+ return this._editing;
+ },
+
+ preventClick: function () {
+ return this._prevent_click;
+ },
+
+ dirty: function () {
+ if (this.location.layer) {
+ return this.location.layer._dirty;
+ }
+ },
+
+ _editStart: function (e) {
+ this._editing = true;
+ this._prevent_click = true;
+ Styles.setEditStyle(e.layer);
+ },
+
+ _editStop: function (e) {
+ this._editing = false;
+ Styles.setSelectedStyle(e.layer);
+ },
+
+ // delete functions
+
+ delete: function () {
+ this.tooltip.remove();
+ this.location._saveDelete();
+ if (this.location._deleted) {
+ this._disableEditToolbar();
+ } else {
+ Styles.setSelectedStyle(this.location.layer);
+ }
+ },
+
+ cancelDelete: function () {
+ this.tooltip.remove();
+ this.location._undoDelete();
+ Styles.setSelectedStyle(this.location.layer);
+ },
+
+ startDelete: function () {
+ this.tooltip.update(this.tooltip.START_DELETE);
+ if (this.location.layer) {
+ this.location._startDelete();
+ Styles.setDeleteStyle(this.location.layer);
+ }
+ },
+
+ deleting: function () {
+ return this.location._deleting;
+ },
+
+ deleted: function () {
+ return this.location._deleted;
+ },
+
+ deleteLayer: function (layer, e) {
+ this.tooltip.update(this.tooltip.CONTINUE_DELETE);
+ var currentLayer = this.location.layer;
+ if (currentLayer.feature.id !== layer.feature.id) {
+ return;
+ }
+ this.location._setDeleted(e);
+ },
+
+ _removeLayer: function () {
+ var hash_path = window.location.hash.slice(1) || '/';
+ var fid = hash_path.split('/')[3];
+ var layer = this.location._findLayer(fid);
+ if (layer) layer.remove();
+ },
+
+ // new location functions
+
+ _addNew: function () {
+ this._resetView();
+ this._addEditControls();
+ this._disableEditToolbar();
+ },
+
+ isNew: function () {
+ if (this.location.layer) {
+ return this.location.layer._new;
+ }
+ },
+
+ // draw functions
+
+ startRectangle: function () {
+ this.tooltip.update(this.tooltip.ADD_RECTANGLE);
+ this.location.startRectangle();
+ },
+
+ startPolygon: function () {
+ this.tooltip.update(this.tooltip.ADD_POLYGON);
+ this.location.startPolygon();
+ },
+
+ addMulti: function (e, type) {
+ if (type instanceof L.Editable.PolygonEditor) {
+ this.tooltip.update(this.tooltip.UPDATE_MULTIPOLYGON);
+ } else {
+ this.tooltip.update(this.tooltip.UPDATE_MULTILINESTRING);
+ }
+ this.location.layer.editor.newShape();
+ },
+
+ startPolyline: function () {
+ this.tooltip.update(this.tooltip.ADD_LINESTRING);
+ this.location.startPolyline();
+ },
+
+ startMarker: function () {
+ this.tooltip.update(this.tooltip.ADD_MARKER);
+ this.location.startMarker();
+ },
+
+ hasEditableLayer: function () {
+ return ((this.location.layer !== null ? true : false) ||
+ this.hasDrawnFeature()) &&
+ (!this.deleting() && !this.deleted());
+ },
+
+ hasDrawnFeature: function () {
+ return this.location.featuresLayer.getLayers().length > 0;
+ },
+
+ cancelDrawing: function (e) {
+ this.location.stopDrawing();
+ this.tooltip.remove();
+ if (this.location.layer) {
+ this.deleteLayer(this.location.layer, e);
+ this.delete();
+ }
+ this._disableEditToolbar(deactivate = true);
+ },
+
+ dispose: function () {
+ this.cancelEdit();
+ this._resetView();
+ },
+
+ _drawStart: function (e) {
+ // this._addTooltip();
+ },
+
+ _drawEnd: function (e) {
+ this._cancelDraw();
+ if (this.location.layer) {
+ this.tooltip.remove();
+ if (!this.location.layer._events.hasOwnProperty('click')) {
+ this.location.layer.on('click', this.onLayerClick, this);
+ }
+ this._enableEditToolbar(active = true);
+ Styles.setEditStyle(this.location.layer);
+ }
+ },
+
+ _vertexNew: function (e) {
+ var latlngs;
+ if (e.layer.editor instanceof L.Editable.PolylineEditor) {
+ latlngs = e.layer._latlngs;
+ if (latlngs.length >= 1) {
+ this.tooltip.update(this.tooltip.FINISH_LINE);
+ } else {
+ this.tooltip.update(this.tooltip.CONTINUE_LINE);
+ }
+ }
+ if (e.layer.editor instanceof L.Editable.PolygonEditor) {
+ latlngs = e.layer._latlngs[0];
+ if (latlngs.length < 2) {
+ this.tooltip.update(this.tooltip.CONTINUE_POLYGON);
+ }
+ if (latlngs.length >= 2 && e.layer.editor instanceof L.Editable.PolygonEditor) {
+ this.tooltip.update(this.tooltip.FINISH_POLYGON);
+ }
+ }
+ },
+
+ _vertexDrag: function (e) {
+ this.tooltip.update(this.tooltip.VERTEX_DRAG);
+ },
+
+ _vertexDragend: function (e) {
+ this.tooltip.update(this.tooltip.EDIT_ENABLED);
+ },
+
+ // saving
+
+ save: function () {
+ this.tooltip.remove();
+ this.location._saveEdit();
+ this._editing = false;
+ },
+
+ // editor toolbars
+
+ _setUpEditor: function (e) {
+ if (!this.location.layer) {
+ var hash_path = window.location.hash.slice(1) || '/';
+ var fid = hash_path.split('/')[3];
+ if (this.map.geojsonLayer.getLayers().length > 0) {
+ var layer = this.location._findLayer(fid);
+ this.location.layer = layer;
+ Styles.setSelectedStyle(this.location.layer);
+ this.edit();
+ this._addEditControls();
+ } else {
+ this.map.on('endtileload', function () {
+ var layer = this.location._findLayer(fid);
+ this.location.layer = layer;
+ Styles.setSelectedStyle(this.location.layer);
+ this.edit();
+ this._addEditControls();
+ }, this);
+ }
+ } else {
+ this._addEditControls();
+ }
+ },
+
+ _addEditControls: function () {
+ const map = this.map;
+ this.toolbars.forEach(function (toolbar) {
+ toolbar.addTo(map);
+ });
+ this._enableEditToolbar(active = true);
+ },
+
+ _removeEditControls: function () {
+ const map = this.map;
+ this.toolbars.forEach(function (toolbar) {
+ if (toolbar) {
+ map.removeControl(toolbar);
+ }
+ });
+ this.location._stopEdit();
+ this._prevent_click = false;
+ this.tooltip.remove();
+ },
+
+ _enableEditToolbar: function (active = false) {
+ var editLink = $('a.edit-action').get(0);
+ var deleteLink = $('a.delete-action').get(0);
+ editLink.href = window.location.href;
+ deleteLink.href = window.location.href;
+ if (active) {
+ editLink.click();
+ Styles.setEditStyle(this.location.layer);
+ }
+ $('span#edit, span#delete').removeClass('smap-edit-disable');
+ },
+
+ _disableEditToolbar: function (deactivate = false) {
+ var edit = $('ul.leaflet-smap-edit a').prop('disabled', 'disabled');
+ $('span#edit, span#delete').addClass('smap-edit-disable');
+ if (deactivate) {
+ var editLink = $('a.edit-action').get(0);
+ editLink.href = window.location.href;
+ editLink.click();
+ }
+ },
+
+ _cancelEdit: function (reset = true) {
+ var cancelEdit = $('a.cancel-edit'),
+ cancelDelete = $('a.cancel-delete');
+ if (cancelEdit.is(':visible')) {
+ cancelEdit.get(0).click();
+ }
+ if (cancelDelete.is(':visible')) {
+ cancelDelete.get(0).click();
+ }
+ if (this.location.layer && reset) {
+ Styles.resetStyle(this.location.layer);
+ }
+ },
+
+ _cancelDraw: function () {
+ var cancelDraw = $('a.cancel-draw');
+ cancelDraw.each(function (idx, ele) {
+ if ($(ele).is(':visible')) {
+ // ele.click();
+ var ul = $(ele).closest('ul');
+ $(ul).css('display', 'none');
+ }
+ });
+ },
+
+ _resetView: function () {
+ this._editing = false;
+ this._prevent_click = false;
+ this._removeEditControls();
+ Styles.resetStyle(this.location.layer);
+ this.location._reset();
+ },
+
+ _cleanAddForm: function () {
+ // if an add location form is canceled, geometry should be removed
+ this.cancelEdit();
+ this.location._saveDelete();
+ },
+
+ // events
+
+ _addDeleteEvent: function () {
+ this.on('location:delete', this._removeLayer, this);
+ this.on('location:reset', this._cleanAddForm, this);
+ this.on('location:reset_dirty', this._cleanAddForm, this);
+ },
+
+ _addRouterEvents: function () {
+ // router events
+ this.on('route:location:edit', this._setUpEditor, this);
+ this.on('route:location:new', this._addNew, this);
+ this.on('route:location:detail', this._removeEditControls, this);
+ this.on('route:overview', this._resetView, this);
+ this.on('route:map', this._resetView, this);
+ },
+
+ _addEditableEvents: function () {
+ // edit events
+ this.location.on('editable:enable', this._editStart, this);
+ this.location.on('editable:disable', this._editStop, this);
+ this.location.on('editable:drawing:start', this._drawStart, this);
+ this.location.on('editable:drawing:end', this._drawEnd, this);
+ this.location.on('editable:vertex:drag', this._vertexDrag, this);
+ this.location.on('editable:vertex:dragend', this._vertexDragend, this);
+ this.location.on('editable:drawing:click', this._vertexNew, this);
+ }
+
+});
diff --git a/cadasta/core/static/js/smap/edit/leaflet.toolbar.js b/cadasta/core/static/js/smap/edit/leaflet.toolbar.js
new file mode 100644
index 000000000..739f66bb7
--- /dev/null
+++ b/cadasta/core/static/js/smap/edit/leaflet.toolbar.js
@@ -0,0 +1,365 @@
+(function (window, document, undefined) {
+
+ "use strict";
+
+ L.Toolbar = (L.Layer || L.Class).extend({
+ statics: {
+ baseClass: 'leaflet-toolbar'
+ },
+
+ includes: L.Mixin.Events,
+
+ options: {
+ className: '',
+ filter: function () { return true; },
+ actions: []
+ },
+
+ initialize: function (options) {
+ L.setOptions(this, options);
+ this._toolbar_type = this.constructor._toolbar_class_id;
+ },
+
+ addTo: function (map) {
+ this._arguments = [].slice.call(arguments);
+
+ map.addLayer(this);
+
+ return this;
+ },
+
+ onAdd: function (map) {
+ var currentToolbar = map._toolbars[this._toolbar_type];
+
+ if (this._calculateDepth() === 0) {
+ if (currentToolbar) { map.removeLayer(currentToolbar); }
+ map._toolbars[this._toolbar_type] = this;
+ }
+ },
+
+ onRemove: function (map) {
+ /*
+ * TODO: Cleanup event listeners.
+ * For some reason, this throws:
+ * "Uncaught TypeError: Cannot read property 'dragging' of null"
+ * on this._marker when a toolbar icon is clicked.
+ */
+ // for (var i = 0, l = this._disabledEvents.length; i < l; i++) {
+ // L.DomEvent.off(this._ul, this._disabledEvents[i], L.DomEvent.stopPropagation);
+ // }
+
+ if (this._calculateDepth() === 0) {
+ delete map._toolbars[this._toolbar_type];
+ }
+ },
+
+ appendToContainer: function (container) {
+ var baseClass = this.constructor.baseClass + '-' + this._calculateDepth(),
+ className = baseClass + ' ' + this.options.className,
+ Action, action,
+ i, j, l, m;
+
+ this._container = container;
+ this._ul = L.DomUtil.create('ul', className, container);
+
+ /* Ensure that clicks, drags, etc. don't bubble up to the map. */
+ this._disabledEvents = ['click', 'mousedown', 'dblclick'];
+
+ for (j = 0, m = this._disabledEvents.length; j < m; j++) {
+ L.DomEvent.on(this._ul, this._disabledEvents[j], L.DomEvent.stopPropagation);
+ }
+
+ /* Instantiate each toolbar action and add its corresponding toolbar icon. */
+ for (i = 0, l = this.options.actions.length; i < l; i++) {
+ Action = this._getActionConstructor(this.options.actions[i]);
+
+ action = new Action();
+ action._createIcon(this, this._ul, this._arguments);
+ }
+ },
+
+ _getActionConstructor: function (Action) {
+ var args = this._arguments,
+ toolbar = this;
+
+ return Action.extend({
+ initialize: function () {
+ Action.prototype.initialize.apply(this, args);
+ },
+ enable: function (e) {
+ /* Ensure that only one action in a toolbar will be active at a time. */
+ if (toolbar._active) { toolbar._active.disable(); }
+ toolbar._active = this;
+
+ Action.prototype.enable.call(this, e);
+ }
+ });
+ },
+
+ /* Used to hide subToolbars without removing them from the map. */
+ _hide: function () {
+ this._ul.style.display = 'none';
+ },
+
+ /* Used to show subToolbars without removing them from the map. */
+ _show: function () {
+ this._ul.style.display = 'block';
+ },
+
+ _calculateDepth: function () {
+ var depth = 0,
+ toolbar = this.parentToolbar;
+
+ while (toolbar) {
+ depth += 1;
+ toolbar = toolbar.parentToolbar;
+ }
+
+ return depth;
+ }
+ });
+
+ L.toolbar = {};
+
+ var toolbar_class_id = 0;
+
+ L.Toolbar.extend = function extend(props) {
+ var statics = L.extend({}, props.statics, {
+ "_toolbar_class_id": toolbar_class_id
+ });
+
+ toolbar_class_id += 1;
+ L.extend(props, { statics: statics });
+
+ return L.Class.extend.call(this, props);
+ };
+
+ L.Map.addInitHook(function () {
+ this._toolbars = {};
+ });
+
+ L.ToolbarAction = L.Handler.extend({
+ statics: {
+ baseClass: 'leaflet-toolbar-icon'
+ },
+
+ options: {
+ toolbarIcon: {
+ html: '',
+ className: '',
+ tooltip: ''
+ },
+ subToolbar: new L.Toolbar()
+ },
+
+ initialize: function (options) {
+ var defaultIconOptions = L.ToolbarAction.prototype.options.toolbarIcon;
+
+ L.setOptions(this, options);
+ this.options.toolbarIcon = L.extend({}, defaultIconOptions, this.options.toolbarIcon);
+ },
+
+ enable: function (e) {
+ if (e) { L.DomEvent.preventDefault(e); }
+ if (this._enabled) { return; }
+ this._enabled = true;
+
+ if (this.addHooks) { this.addHooks(); }
+ },
+
+ disable: function () {
+ if (!this._enabled) { return; }
+ this._enabled = false;
+
+ if (this.removeHooks) { this.removeHooks(); }
+ },
+
+ _createIcon: function (toolbar, container, args) {
+ var iconOptions = this.options.toolbarIcon;
+
+ this.toolbar = toolbar;
+ this._icon = L.DomUtil.create('li', '', container);
+ this._link = L.DomUtil.create('a', '', this._icon);
+
+ this._link.innerHTML = iconOptions.html;
+ // this._link.setAttribute('href', '');
+ this._link.setAttribute('title', iconOptions.tooltip);
+
+ L.DomUtil.addClass(this._link, this.constructor.baseClass);
+ if (iconOptions.className) {
+ L.DomUtil.addClass(this._link, iconOptions.className);
+ }
+
+ L.DomEvent.on(this._link, 'click', this.enable, this);
+
+ /* Add secondary toolbar */
+ this._addSubToolbar(toolbar, this._icon, args);
+ },
+
+ _addSubToolbar: function (toolbar, container, args) {
+ var subToolbar = this.options.subToolbar,
+ addHooks = this.addHooks,
+ removeHooks = this.removeHooks;
+
+ /* For calculating the nesting depth. */
+ subToolbar.parentToolbar = toolbar;
+
+ if (subToolbar.options.actions.length > 0) {
+ /* Make a copy of args so as not to pollute the args array used by other actions. */
+ args = [].slice.call(args);
+ args.push(this);
+
+ subToolbar.addTo.apply(subToolbar, args);
+ subToolbar.appendToContainer(container);
+
+ this.addHooks = function (map) {
+ if (typeof addHooks === 'function') { addHooks.call(this, map); }
+ subToolbar._show();
+ };
+
+ this.removeHooks = function (map) {
+ if (typeof removeHooks === 'function') { removeHooks.call(this, map); }
+ subToolbar._hide();
+ };
+ }
+ }
+ });
+
+ L.toolbarAction = function toolbarAction(options) {
+ return new L.ToolbarAction(options);
+ };
+
+ L.ToolbarAction.extendOptions = function (options) {
+ return this.extend({ options: options });
+ };
+
+ L.Toolbar.Control = L.Toolbar.extend({
+ statics: {
+ baseClass: 'leaflet-control-toolbar ' + L.Toolbar.baseClass
+ },
+
+ initialize: function (options) {
+ L.Toolbar.prototype.initialize.call(this, options);
+
+ this._control = new L.Control.Toolbar(this.options);
+ },
+
+ onAdd: function (map) {
+ this._control.addTo(map);
+
+ L.Toolbar.prototype.onAdd.call(this, map);
+
+ this.appendToContainer(this._control.getContainer());
+ },
+
+ onRemove: function (map) {
+ L.Toolbar.prototype.onRemove.call(this, map);
+ if (this._control.remove) { this._control.remove(); } // Leaflet 1.0
+ else { this._control.removeFrom(map); }
+ }
+ });
+
+ L.Control.Toolbar = L.Control.extend({
+ onAdd: function () {
+ return L.DomUtil.create('div', '');
+ }
+ });
+
+ L.toolbar.control = function (options) {
+ return new L.Toolbar.Control(options);
+ };
+
+ // A convenience class for built-in popup toolbars.
+
+ L.Toolbar.Popup = L.Toolbar.extend({
+ statics: {
+ baseClass: 'leaflet-popup-toolbar ' + L.Toolbar.baseClass
+ },
+
+ options: {
+ anchor: [0, 0]
+ },
+
+ initialize: function (latlng, options) {
+ L.Toolbar.prototype.initialize.call(this, options);
+
+ /*
+ * Developers can't pass a DivIcon in the options for L.Toolbar.Popup
+ * (the use of DivIcons is an implementation detail which may change).
+ */
+ this._marker = new L.Marker(latlng, {
+ icon: new L.DivIcon({
+ className: this.options.className,
+ iconAnchor: [0, 0]
+ })
+ });
+ },
+
+ onAdd: function (map) {
+ this._map = map;
+ this._marker.addTo(map);
+
+ L.Toolbar.prototype.onAdd.call(this, map);
+
+ this.appendToContainer(this._marker._icon);
+
+ this._setStyles();
+ },
+
+ onRemove: function (map) {
+ map.removeLayer(this._marker);
+
+ L.Toolbar.prototype.onRemove.call(this, map);
+
+ delete this._map;
+ },
+
+ setLatLng: function (latlng) {
+ this._marker.setLatLng(latlng);
+
+ return this;
+ },
+
+ _setStyles: function () {
+ var container = this._container,
+ toolbar = this._ul,
+ anchor = L.point(this.options.anchor),
+ icons = toolbar.querySelectorAll('.leaflet-toolbar-icon'),
+ buttonHeights = [],
+ toolbarWidth = 0,
+ toolbarHeight,
+ tipSize,
+ tipAnchor;
+
+ /* Calculate the dimensions of the toolbar. */
+ for (var i = 0, l = icons.length; i < l; i++) {
+ if (icons[i].parentNode.parentNode === toolbar) {
+ buttonHeights.push(parseInt(L.DomUtil.getStyle(icons[i], 'height'), 10));
+ toolbarWidth += Math.ceil(parseFloat(L.DomUtil.getStyle(icons[i], 'width')));
+ }
+ }
+ toolbar.style.width = toolbarWidth + 'px';
+
+ /* Create and place the toolbar tip. */
+ this._tipContainer = L.DomUtil.create('div', 'leaflet-toolbar-tip-container', container);
+ this._tipContainer.style.width = toolbarWidth + 'px';
+
+ this._tip = L.DomUtil.create('div', 'leaflet-toolbar-tip', this._tipContainer);
+
+ /* Set the tipAnchor point. */
+ toolbarHeight = Math.max.apply(undefined, buttonHeights);
+ tipSize = parseInt(L.DomUtil.getStyle(this._tip, 'width'), 10);
+ tipAnchor = new L.Point(toolbarWidth / 2, toolbarHeight + 0.7071 * tipSize);
+
+ /* The anchor option allows app developers to adjust the toolbar's position. */
+ container.style.marginLeft = (anchor.x - tipAnchor.x) + 'px';
+ container.style.marginTop = (anchor.y - tipAnchor.y) + 'px';
+ }
+ });
+
+ L.toolbar.popup = function (options) {
+ return new L.Toolbar.Popup(options);
+ };
+
+
+})(window, document);
diff --git a/cadasta/core/static/js/smap/edit/path.drag.js b/cadasta/core/static/js/smap/edit/path.drag.js
new file mode 100644
index 000000000..8f6b8d6c7
--- /dev/null
+++ b/cadasta/core/static/js/smap/edit/path.drag.js
@@ -0,0 +1,137 @@
+'use strict';
+
+/* A Draggable that does not update the element position
+and takes care of only bubbling to targetted path in Canvas mode. */
+L.PathDraggable = L.Draggable.extend({
+
+ initialize: function (path) {
+ this._path = path;
+ this._canvas = (path._map.getRenderer(path) instanceof L.Canvas);
+ var element = this._canvas ? this._path._map.getRenderer(this._path)._container : this._path._path;
+ L.Draggable.prototype.initialize.call(this, element, element, true);
+ },
+
+ _updatePosition: function () {
+ var e = { originalEvent: this._lastEvent };
+ this.fire('drag', e);
+ },
+
+ _onDown: function (e) {
+ var first = e.touches ? e.touches[0] : e;
+ this._startPoint = new L.Point(first.clientX, first.clientY);
+ if (this._canvas && !this._path._containsPoint(this._path._map.mouseEventToLayerPoint(first))) { return; }
+ L.Draggable.prototype._onDown.call(this, e);
+ }
+
+});
+
+
+L.Handler.PathDrag = L.Handler.extend({
+
+ initialize: function (path) {
+ this._path = path;
+ },
+
+ getEvents: function () {
+ return {
+ dragstart: this._onDragStart,
+ drag: this._onDrag,
+ dragend: this._onDragEnd
+ };
+ },
+
+ addHooks: function () {
+ if (!this._draggable) { this._draggable = new L.PathDraggable(this._path); }
+ this._draggable.on(this.getEvents(), this).enable();
+ L.DomUtil.addClass(this._draggable._element, 'leaflet-path-draggable');
+ },
+
+ removeHooks: function () {
+ this._draggable.off(this.getEvents(), this).disable();
+ L.DomUtil.removeClass(this._draggable._element, 'leaflet-path-draggable');
+ },
+
+ moved: function () {
+ return this._draggable && this._draggable._moved;
+ },
+
+ _onDragStart: function () {
+ this._startPoint = this._draggable._startPoint;
+ this._path
+ .closePopup()
+ .fire('movestart')
+ .fire('dragstart');
+ },
+
+ _onDrag: function (e) {
+ var path = this._path,
+ event = (e.originalEvent.touches && e.originalEvent.touches.length === 1 ? e.originalEvent.touches[0] : e.originalEvent),
+ newPoint = L.point(event.clientX, event.clientY),
+ latlng = path._map.layerPointToLatLng(newPoint);
+
+ this._offset = newPoint.subtract(this._startPoint);
+ this._startPoint = newPoint;
+
+ this._path.eachLatLng(this.updateLatLng, this);
+ path.redraw();
+
+ e.latlng = latlng;
+ e.offset = this._offset;
+ path.fire('move', e)
+ .fire('drag', e);
+ },
+
+ _onDragEnd: function (e) {
+ if (this._path._bounds) this.resetBounds();
+ this._path.fire('moveend')
+ .fire('dragend', e);
+ },
+
+ latLngToLayerPoint: function (latlng) {
+ // Same as map.latLngToLayerPoint, but without the round().
+ var projectedPoint = this._path._map.project(L.latLng(latlng));
+ return projectedPoint._subtract(this._path._map.getPixelOrigin());
+ },
+
+ updateLatLng: function (latlng) {
+ var oldPoint = this.latLngToLayerPoint(latlng);
+ oldPoint._add(this._offset);
+ var newLatLng = this._path._map.layerPointToLatLng(oldPoint);
+ latlng.lat = newLatLng.lat;
+ latlng.lng = newLatLng.lng;
+ },
+
+ resetBounds: function () {
+ this._path._bounds = new L.LatLngBounds();
+ this._path.eachLatLng(function (latlng) {
+ this._bounds.extend(latlng);
+ });
+ }
+
+});
+
+L.Path.include({
+
+ eachLatLng: function (callback, context) {
+ context = context || this;
+ var loop = function (latlngs) {
+ for (var i = 0; i < latlngs.length; i++) {
+ if (L.Util.isArray(latlngs[i])) loop(latlngs[i]);
+ else callback.call(context, latlngs[i]);
+ }
+ };
+ loop(this.getLatLngs ? this.getLatLngs() : [this.getLatLng()]);
+ }
+
+});
+
+L.Path.addInitHook(function () {
+
+ this.dragging = new L.Handler.PathDrag(this);
+ if (this.options.draggable) {
+ this.on('add', function () {
+ this.dragging.enable();
+ });
+ }
+
+});
diff --git a/cadasta/core/static/js/smap/edit/styles.js b/cadasta/core/static/js/smap/edit/styles.js
new file mode 100644
index 000000000..6b3c95f3b
--- /dev/null
+++ b/cadasta/core/static/js/smap/edit/styles.js
@@ -0,0 +1,87 @@
+Styles = {
+
+ setSelectedStyle: function (layer) {
+ if (layer instanceof L.Polygon || layer instanceof L.Rectangle) {
+ layer.setStyle({
+ fillColor: '#f06eaa',
+ color: '#f06eaa',
+ weight: 5,
+ fillOpacity: 0.2,
+ fill: true,
+ opacity: 0.5,
+ dashArray: null,
+ clickable: true
+ });
+ }
+ if (layer instanceof L.Polyline) {
+ layer.setStyle({
+ color: '#f06eaa',
+ weight: 4,
+ opacity: 0.5,
+ dashArray: null
+ });
+ }
+ },
+
+ setEditStyle: function (layer) {
+ if (layer instanceof L.Polygon || layer instanceof L.Rectangle) {
+ layer.setStyle({
+ fillColor: '#f06eaa',
+ dashArray: '10, 10',
+ color: '#fe57a1',
+ weight: 5,
+ opacity: 0.6,
+ fillOpacity: 0.1,
+ clickable: true
+ });
+ }
+ if (layer instanceof L.Polyline) {
+ layer.setStyle({
+ dashArray: '10, 10',
+ color: '#fe57a1',
+ weight: 5,
+ opacity: 0.6,
+ });
+ }
+ },
+
+ setDeleteStyle: function (layer) {
+ if (layer instanceof L.Polygon || layer instanceof L.Rectangle) {
+ layer.setStyle({
+ fillColor: 'red',
+ color: 'red',
+ dashArray: '10, 10'
+ });
+ }
+ if (layer instanceof L.Polyline) {
+ layer.setStyle({
+ color: 'red',
+ dashArray: '10, 10'
+ });
+ }
+ },
+
+ resetStyle: function (layer) {
+ if (layer instanceof L.Polygon || layer instanceof L.Rectangle) {
+ layer.setStyle({
+ weight: 2,
+ color: 'blue',
+ fillColor: 'blue',
+ stroke: true,
+ fill: true,
+ fillOpacity: 0.2,
+ opacity: 1,
+ dashArray: null,
+ });
+ }
+ if (layer instanceof L.Polyline) {
+ layer.setStyle({
+ weight: 2,
+ color: 'blue',
+ stroke: true,
+ opacity: 1,
+ dashArray: null,
+ });
+ }
+ }
+}
diff --git a/cadasta/core/static/js/smap/edit/tooltip.js b/cadasta/core/static/js/smap/edit/tooltip.js
new file mode 100644
index 000000000..51f3faeae
--- /dev/null
+++ b/cadasta/core/static/js/smap/edit/tooltip.js
@@ -0,0 +1,80 @@
+Tooltip = L.Class.extend({
+
+ EDIT_ENABLED: 'Click cancel to undo changes.
' +
+ 'Drag handles, or marker to edit feature.
' +
+ 'Click on a handle to delete it.',
+
+ START_DELETE: 'Click on the highlighted feature to remove it.',
+ CONTINUE_DELETE: 'Click cancel to undo or save to save deletion.',
+ DELETE_LAYER: 'Click cancel to undo or save to save deletion.',
+
+ VERTEX_DRAG: 'Release mouse to finish drawing.',
+
+ FINISH_LINE: 'Click on last point to finish line.',
+ CONTINUE_LINE: 'Click to continue line',
+
+ CONTINUE_POLYGON: 'Click to continue adding vertices.',
+ FINISH_POLYGON: 'Click to continue adding vertices.
' +
+ 'Click last point to finish polygon.',
+
+ ADD_MARKER: 'Click on the map to add a marker.',
+ ADD_RECTANGLE: 'Click and drag on the map to create a rectangle.',
+ ADD_POLYGON: 'Click on the map to start a new polygon.',
+ ADD_LINESTRING: 'Click on the map to start a new line.',
+
+ UPDATE_MULTIPOLYGON: 'Click on the map update the multipolygon.',
+ UPDATE_MULTILINESTRING: 'Click on the map to update the multilinestring.',
+
+
+ _enabled: false,
+
+
+ initialize: function (map) {
+ this._map = map;
+ this._popupPane = map._panes.popupPane;
+ this._container = L.DomUtil.create(
+ 'div', 'editor-tooltip', this._popupPane);
+
+ this._map.on('mouseout', this._onMouseOut, this);
+ },
+
+ update: function (labelText) {
+ if (!this._enabled) this._enable();
+ if (!this._container) {
+ return this;
+ }
+ this._container.innerHTML = labelText;
+ return this;
+ },
+
+ remove: function () {
+ if (this._enabled) this._disable();
+ },
+
+ _enable: function () {
+ L.DomEvent.on(this._map, 'mousemove', this._move, this);
+ this._enabled = true;
+ },
+
+ _disable: function () {
+ this._container.innerHTML = '';
+ this._container.style.visibility = 'hidden';
+ L.DomEvent.off(this._map, 'mousemove', this._move, this);
+ this._enabled = false;
+ },
+
+ _move: function (e) {
+ if (this._container.style.visibility === 'hidden') {
+ this._container.style.visibility = 'visible';
+ }
+ this._container.style.left = e.layerPoint.x + 'px';
+ this._container.style.top = e.layerPoint.y + 'px';
+ },
+
+ _onMouseOut: function () {
+ if (this._container) {
+ this._container.style.visibility = 'hidden';
+ }
+ }
+
+});
diff --git a/cadasta/core/static/js/smap/index.js b/cadasta/core/static/js/smap/index.js
index 4f6041830..28a6ac26f 100644
--- a/cadasta/core/static/js/smap/index.js
+++ b/cadasta/core/static/js/smap/index.js
@@ -1,21 +1,73 @@
var map = L.map('mapid');
+var editor = new LocationEditor(map);
var sr = new SimpleRouter(map);
sr.router();
/*****************
EVENT LISTENERS
*****************/
-window.addEventListener('hashchange', function() {
+window.addEventListener('hashchange', function () {
sr.router();
});
-window.addEventListener('load', function() {
+window.addEventListener('load', function () {
sr.router();
});
-map.on('endtileload', function() {
+map.on('endtileload', function () {
rm.updateState({
'check_location_coords': true,
});
});
SMap(map);
-var hash = new L.Hash(map);
\ No newline at end of file
+var hash = new L.Hash(map);
+
+// handle location form navigation
+
+$(document).on('click', 'a', function (e) {
+ if (editor.dirty() && (editor.editing() || editor.deleting())) {
+ if (e.currentTarget.hash === '#/search') return;
+ if (e.currentTarget.hash === '#archive_confirm') return;
+ var path = e.currentTarget.hash || e.currentTarget.href;
+ if ((path && path != window.location.hash) || e.currentTarget.form) {
+ e.preventDefault();
+ var modal = $('#unsaved_edits').modal({
+ keyboard: false,
+ background: true,
+ }, 'show');
+ modal.one('click', '.forget-changes', function (e) {
+ editor.dispose();
+ modal.hide();
+ // if (window.location.hash.includes('new')){
+ editor.fire('location:reset_dirty');
+ // }
+ window.location.href = path;
+ });
+ }
+ }
+});
+
+$(document).on('click', '#delete-location', function (e) {
+ editor.fire('location:delete');
+});
+
+$(document).on('click', '.btn-default.cancel', function (e) {
+ if (!editor.dirty()) {
+ editor.fire('location:reset');
+ }
+});
+
+$(document).on('click', 'button[name="submit-button"]', function (e) {
+ if (editor.dirty()) {
+ e.preventDefault();
+ var $form = $('#location-wizard');
+ var modal = $('#unsaved_edits').modal({
+ keyboard: false,
+ background: true,
+ }, 'show');
+ modal.one('click', '.forget-changes', function (e) {
+ editor.cancelEdit();
+ $form.trigger('submit');
+ modal.hide();
+ });
+ }
+});
diff --git a/cadasta/core/static/js/smap/location_edit_js_temp.html b/cadasta/core/static/js/smap/location_edit_js_temp.html
deleted file mode 100644
index 6e9e742d2..000000000
--- a/cadasta/core/static/js/smap/location_edit_js_temp.html
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-{% if get_current_language != "en-us" %}
-{% get_current_language as LANGUAGE_CODE %}
-
-{% endif %}
- -->
-
\ No newline at end of file
diff --git a/cadasta/core/static/js/smap/map.js b/cadasta/core/static/js/smap/map.js
index 0bf9db1e8..05faccb88 100644
--- a/cadasta/core/static/js/smap/map.js
+++ b/cadasta/core/static/js/smap/map.js
@@ -1,163 +1,152 @@
-var SMap = function(map) {
- var layerscontrol = L.control.layers().addTo(map);
-
- var geojsonTileLayer = new L.TileLayer.GeoJSON(
- 1, options.locations_count, url,
- {
- clipTiles: true,
- unique: function (feature) {return feature.id;}
- },
- {
- style: { weight: 2 },
- onEachFeature: function(feature, layer) {
- if (options.trans) {
- layer.bindPopup("
{% trans "Are you sure you want to delete this location?" %}
- \ No newline at end of file + diff --git a/cadasta/templates/spatial/location_form.html b/cadasta/templates/spatial/location_form.html index 4bf57159c..ee7c54349 100644 --- a/cadasta/templates/spatial/location_form.html +++ b/cadasta/templates/spatial/location_form.html @@ -9,6 +9,7 @@