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("
" + - "

Location" + - feature.properties.type + "

" + - "
" + options.trans.open + "" + - "
"); +var SMap = function (map) { + var layerscontrol = L.control.layers().addTo(map); + + var geojsonTileLayer = new L.TileLayer.GeoJSON( + 1, options.num_locations, url, { + clipTiles: true, + unique: function (feature) { return feature.id; } + }, { + style: { color: 'blue', weight: 2 }, + onEachFeature: function (feature, layer) { + layer.on('click', function (e) { + map.locationEditor.onLayerClick(e); + }); + } + }); + + function add_tile_layers() { + for (var i = 0, n = layers.length; i < n; i++) { + var attrs = L.Util.extend(layers[i].attrs); + var layer = { name: layers[i].label, url: layers[i].url, options: attrs }; + var l = L.tileLayer(layer.url, layer.options); + layerscontrol.addBaseLayer(l, layer.name); + + if (i === 0) { + l.addTo(map); + } } - } - }); - - function add_tile_layers() { - for (var i = 0, n = layers.length; i < n; i++) { - var attrs = L.Util.extend(layers[i].attrs); - var layer = {name: layers[i].label, url: layers[i].url, options: attrs}; - var l = L.tileLayer(layer.url, layer.options); - layerscontrol.addBaseLayer(l, layer.name); - - if (i === 0) { - l.addTo(map); - } } - } - - add_tile_layers(); - map.addLayer(geojsonTileLayer); - rm.setGeoJsonLayer(geojsonTileLayer); - - function load_project_extent() { - if (options.projectExtent) { - var boundary = L.geoJson( - options.projectExtent, { - style: { - stroke: true, - color: "#0e305e", - weight: 2, - dashArray: "5, 5", - opacity: 1, - fill: false, - clickable: false, - } + + add_tile_layers(); + map.addLayer(geojsonTileLayer); + rm.setGeoJsonLayer(geojsonTileLayer); + map.geojsonTileLayer = geojsonTileLayer; + map.geojsonLayer = geojsonTileLayer.geojsonLayer; + + function load_project_extent() { + if (options.projectExtent) { + var boundary = L.geoJson( + options.projectExtent, { + style: { + stroke: true, + color: "#0e305e", + weight: 2, + dashArray: "5, 5", + opacity: 1, + fill: false, + clickable: false, + } + } + ); + boundary.addTo(map); + projectBounds = boundary.getBounds(); + options.projectExtent = projectBounds; + if (options.fitBounds === 'project') { + map.fitBounds(projectBounds); + return projectBounds; + } + } else { + map.fitBounds([ + [-45.0, -180.0], + [45.0, 180.0] + ]); } - ); - boundary.addTo(map); - projectBounds = boundary.getBounds(); - options.projectExtent = projectBounds; - if (options.fitBounds === 'project') { - map.fitBounds(projectBounds); - return projectBounds; - } - } else { - map.fitBounds([[-45.0, -180.0], [45.0, 180.0]]); } - } - load_project_extent(); + load_project_extent(); - /*** CURRENTLY DOES NOT WORK ***/ - function load_features() { - if (options.fitBounds === 'locations') { - var bounds = geojsonTileLayer.geojsonLayer.getBounds(); - if (bounds.isValid()) { - map.fitBounds(bounds); - } + /*** CURRENTLY DOES NOT WORK ***/ + function load_features() { + if (options.fitBounds === 'locations') { + var bounds = geojsonTileLayer.geojsonLayer.getBounds(); + if (bounds.isValid()) { + map.fitBounds(bounds); + } + } } - } - - load_features(); - - function render_spatial_resource(){ - $.ajax(fetch_spatial_resources).done(function(data){ - if (data.length === 0) return; - var spatialResources = {}; - $.each(data, function(idx, resource){ - var name = resource.name; - var layers = {}; - var group = new L.LayerGroup(); - $.each(resource.spatial_resources, function(i, spatial_resource){ - var layer = L.geoJson(spatial_resource.geom).addTo(group); - layers.name = spatial_resource.name; - layers.group = group; - }); - spatialResources[name] = layers; - }); - $.each(spatialResources, function(sr){ - var layer = spatialResources[sr]; - layerscontrol.addOverlay(layer.group, layer.name, sr); - }); - }); - } - - render_spatial_resource(); - - function geoLocate() { - return function(event) { - map.locate({ setView: true }); - }; - } - - function add_map_controls() { - map.removeControl(map.zoomControl); - map.addControl(L.control.zoom({ - zoomInTitle: gettext("Zoom in"), - zoomOutTitle: gettext("Zoom out") - })); - - var geocoder = L.control.geocoder('search-QctWfva', { - markers: false - }).addTo(map); - geocoder.on('select', function (e) { - map.setZoomAround(e.latlng, 9); - }); - - var Geolocate = L.Control.extend({ - options: { - position: 'topleft' - }, - - onAdd: function() { - var controlDiv = L.DomUtil.create( - 'div', 'leaflet-bar leaflet-control leaflet-control-geolocate' - ); - controlDiv.title = gettext('Go to my location'); - L.DomEvent - .addListener(controlDiv, 'click', L.DomEvent.stopPropagation) - .addListener(controlDiv, 'click', L.DomEvent.preventDefault) - .addListener(controlDiv, 'click', geoLocate(map)); - - L.DomUtil.create('span', 'glyphicon glyphicon-map-marker', controlDiv); - - return controlDiv; - } - }); - - map.addControl(new Geolocate()); - - function add_popup_actions() { - map.on('popupopen', function() { - $('#spatial-pop-up').on('click', function() { - map.closePopup(); + + load_features(); + + function render_spatial_resource() { + $.ajax(fetch_spatial_resources).done(function (data) { + if (data.length === 0) return; + var spatialResources = {}; + $.each(data, function (idx, resource) { + var name = resource.name; + var layers = {}; + var group = new L.LayerGroup(); + $.each(resource.spatial_resources, function (i, spatial_resource) { + var layer = L.geoJson(spatial_resource.geom).addTo(group); + layers.name = spatial_resource.name; + layers.group = group; + }); + spatialResources[name] = layers; + }); + $.each(spatialResources, function (sr) { + var layer = spatialResources[sr]; + layerscontrol.addOverlay(layer.group, layer.name, sr); + }); }); - }); } - add_popup_actions(); + render_spatial_resource(); - return map; - } + function geoLocate() { + return function (event) { + map.locate({ setView: true }); + }; + } + + function add_map_controls() { + map.removeControl(map.zoomControl); + map.addControl(L.control.zoom({ + zoomInTitle: gettext("Zoom in"), + zoomOutTitle: gettext("Zoom out") + })); + + var geocoder = L.control.geocoder('search-QctWfva', { + markers: false + }).addTo(map); + geocoder.on('select', function (e) { + map.setZoomAround(e.latlng, 9); + }); + + var Geolocate = L.Control.extend({ + options: { + position: 'topleft' + }, + + onAdd: function () { + var controlDiv = L.DomUtil.create( + 'div', 'leaflet-bar leaflet-control leaflet-control-geolocate' + ); + controlDiv.title = gettext('Go to my location'); + L.DomEvent + .addListener(controlDiv, 'click', L.DomEvent.stopPropagation) + .addListener(controlDiv, 'click', L.DomEvent.preventDefault) + .addListener(controlDiv, 'click', geoLocate(map)); + + L.DomUtil.create('span', 'glyphicon glyphicon-map-marker', controlDiv); + + return controlDiv; + } + }); + + map.addControl(new Geolocate()); + + return map; + } - add_map_controls(); + add_map_controls(); }; diff --git a/cadasta/core/static/js/smap/router.js b/cadasta/core/static/js/smap/router.js index 5309547c5..2ccf73754 100644 --- a/cadasta/core/static/js/smap/router.js +++ b/cadasta/core/static/js/smap/router.js @@ -1,89 +1,93 @@ // Based on http://joakim.beng.se/blog/posts/a-javascript-router-in-20-lines.html -var SimpleRouter = function(map){ - var rm = RouterMixins; - var routes = new CreateRoutes(); +var SimpleRouter = function (map) { + var rm = RouterMixins; + var routes = new CreateRoutes(map); - function router(force_reload=false) { - var async_url = '/async' + location.pathname; - var hash_path = location.hash.slice(1) || '/'; + function router(force_reload = false) { + var async_url = '/async' + location.pathname; + var hash_path = location.hash.slice(1) || '/'; - // first_load will only be true if the first page landed on is a record without coordinates - if (!hash_path.includes('/records/') || hash_path.includes('coords=')) { - rm.setFirstLoad(false); - } + // first_load will only be true if the first page landed on is a record without coordinates + if (!hash_path.includes('/records/') || hash_path.includes('coords=')) { + rm.setFirstLoad(false); + } - if (hash_path.includes('coords=')) { - hash_path = hash_path.substr(0, hash_path.indexOf('coords=') - 1) || '/'; - } + if (hash_path.includes('coords=')) { + hash_path = hash_path.substr(0, hash_path.indexOf('coords=') - 1) || '/'; + } - // Prevents router from reloading every time the coords changes. - // force_reload is only true when a detach form is submitted - if (force_reload === false) { - if (rm.getLastHashPath() === hash_path || hash_path === '/search') { - return; + // Prevents router from reloading every time the coords changes. + // force_reload is only true when a detach form is submitted + if (force_reload === false) { + if (rm.getLastHashPath() === hash_path || hash_path === '/search') { + return; + } } - } - if (hash_path !== '/') { - async_url = async_url + hash_path.substr(1); - } + if (hash_path !== '/') { + async_url = async_url + hash_path.substr(1); + } - // Fail safe in case a hashpath does not contain the final backslash - if (hash_path.substr(-1) !== '/' && !hash_path.includes('?')) { - console.log(hash_path, 'was missing the final backslash.'); - hash_path += '/'; - } + // Fail safe in case a hashpath does not contain the final backslash + if (hash_path.substr(-1) !== '/' && !hash_path.includes('?')) { + hash_path += '/'; + } + + var route = routes[hash_path] || null; - var route = routes[hash_path] || null; + // If route is null, there is an ID in the hash. Remove the record id from hash_path to match key in routes. + if (!route) { + var new_hash_path = hash_path.split('/'); + new_hash_path.splice(3, 1); + new_hash_path = new_hash_path.join('/'); + route = routes[new_hash_path]; + } - // If route is null, there is an ID in the hash. Remove the record id from hash_path to match key in routes. - if (!route) { - var new_hash_path = hash_path.split('/'); - new_hash_path.splice(3, 1); - new_hash_path = new_hash_path.join('/'); - route = routes[new_hash_path]; - } + if (!route) { + console.log('that route was undefined:', window.location.hash); + window.location.hash = '#/overview'; + return; + } - var el = document.getElementById(rm.getRouteElement(route.el)); - var geturl = $.ajax({ - type: "GET", - url: async_url, - success: function (response, status, xhr) { - var permission_error = geturl.getResponseHeader('Permission-Error'); - var anonymous_user = geturl.getResponseHeader('Anonymous-User'); - var location_coords = geturl.getResponseHeader('Coordinates'); + var el = document.getElementById(rm.getRouteElement(route.el)); + var geturl = $.ajax({ + type: "GET", + url: async_url, + success: function (response, status, xhr) { + var permission_error = geturl.getResponseHeader('Permission-Error'); + var anonymous_user = geturl.getResponseHeader('Anonymous-User'); + var location_coords = geturl.getResponseHeader('Coordinates'); - if (location_coords && rm.getFirstLoad()) { - rm.setCurrentLocationCoords(location_coords); - } + if (location_coords && rm.getFirstLoad()) { + rm.setCurrentLocationCoords(location_coords); + } - if (permission_error) { - if (rm.getLastHashPath() && rm.getLastHashPath() !== '/' && rm.getLastHashPath() !== '/overview/') { - history.back(); - } else if (!window.location.hash.includes('/overview.')) { - window.location.hash = "/overview/"; - } - if ($('.alert-warning').length === 0) { - $('#messages').append(rm.permissionDenied(permission_error)); - } - return; + if (permission_error) { + if (rm.getLastHashPath() && rm.getLastHashPath() !== '/' && rm.getLastHashPath() !== '/overview/') { + history.back(); + } else if (!window.location.hash.includes('/overview.')) { + window.location.hash = "/overview/"; + } + if ($('.alert-warning').length === 0) { + $('#messages').append(rm.permissionDenied(permission_error)); + } + return; - } else if (anonymous_user) { - window.location = "/account/login/?next=" + window.location.pathname; + } else if (anonymous_user) { + window.location = "/account/login/?next=" + window.location.pathname; - } else { - route.controller(); - el.innerHTML = response; - if (route.eventHook) { - route.eventHook(); + } else { + route.controller(); + el.innerHTML = response; + if (route.eventHook) { + route.eventHook(); + } + } + rm.setLastHashPath(hash_path); } - } - rm.setLastHashPath(hash_path); - } }); } // end router() - return { - router: router, - }; + return { + router: router, + }; }; // end SimpleRouter() - diff --git a/cadasta/core/static/js/smap/router_mixins.js b/cadasta/core/static/js/smap/router_mixins.js index 5b682dcad..82a1c5f30 100644 --- a/cadasta/core/static/js/smap/router_mixins.js +++ b/cadasta/core/static/js/smap/router_mixins.js @@ -1,528 +1,528 @@ var state; var RouterMixins = { - settings: { - current_location: {url: null, feature: null}, - current_relationship: {url: null}, - el: { - 'detail': 'project-detail', - 'modal': 'additional-modals', - }, - last_hash: '', - coords: null, - first_load: true, - }, - - init: function() { - state = this.settings; - }, - - updatePage: function(kwargs) { - if (kwargs.page_title) { - this.setPageTitle(kwargs.page_title); - } - - if (kwargs.display_modal) { - this.displayModal(); - } else { - this.hideModal(); - } - - if (kwargs.display_detail_panel) { - this.displayDetailPanel(); - } else { - this.hideDetailPanel(); - } - - if (kwargs.display_edit_panel) { - this.displayEditDetailPanel(); - } - - if (kwargs.active_sidebar) { - this.setSidebar(kwargs.active_sidebar); - } - - if (kwargs.reset_current_location) { - this.resetPreviousLocationStyle(); - } - }, - - updateState: function(kwargs) { - /********* - 'current_location': String. where to find the url for the location connected to the record. Location detail pages will pull from the window.location.hash, Relationship detail pages will pull from the #current-location link - *********/ - if (kwargs.current_location) { - this.setCurrentLocationUrl(kwargs.current_location); - this.setCurrentLocationFeature(); - } - - /********* - 'current_relationship': String. where to find the the url for the relationship connected to the record. Relationship detail pages will pull from the window.location.hash. - *********/ - if (kwargs.current_relationship) { - this.setCurrentRelationshipUrl(kwargs.current_relationship); - } - - /********* - 'datatable': Boolean. Does the page contain a DataTable? - *********/ - if (kwargs.datatable) { - dataTableHook(); - } - - /********* - 'active_tab': S1['overview', 'relationships', 'resources']. Choose which of the bootstrap tabs in the Location Detail page you want activated. - *********/ - if (kwargs.active_tab) { - rm.activateTab(kwargs.active_tab); - } - - /********* - 'check_location_coords': Boolean. Only used when a page first loads. If the page loads to a location/relationship detail page, and if there are not coords, it will zoom in on the coordinates provided in the header. - (used in smap/index.js on 'endtileload') - *********/ - if (kwargs.check_location_coords) { - this.getCurrentLocationCoords(); - } - - /********* - 'detach_forms': Boolean. Does the page contain a Datatable with resources that can be detached? - *********/ - if (kwargs.detach_forms) { - this.detachFormSubmission(); - } - - /********* - 'form': { - 'type': ['modal', 'detail'] Does the form appear in the detail panel, or does a modal appear? - - 'success_url': ['relationship', 'location'] Does the page need to redirect to a location or relationship detail page? - - 'tab': [null, 'overview', 'resources', 'relationships'] If the success url redirects to a Location Detail page, is there a specific tab you want activated? - - 'callback': [null, rm.relationshipHooks, rm.uploadResourcesHooks] Did you call any event hooks after updateState? If the form fails, you'll need to call them again after the form is submitted. - } - *********/ - if (kwargs.form) { - success_url = this.getSuccessUrl( - kwargs.form.success_url, - kwargs.form.tab - ); - rm.formSubmission(kwargs.form.type, success_url, kwargs.form.callback); - } - - }, - - /*************** - DOM MANIPULATION - ****************/ - resizeMap: function(size) { - size = size || 300; - $('#project-map').height(size); - - window.setTimeout(function() { - map.invalidateSize(); - }, 500); - }, - - displayDetailPanel: function() { - if ($('.content-single').hasClass('detail-hidden')) { - $('.content-single').removeClass('detail-hidden'); - this.resizeMap(); - } - - if ($('#' + state.el.detail).hasClass('detail-edit')) { - $('#' + state.el.detail).removeClass('detail-edit'); - } - }, - - displayEditDetailPanel: function() { - if ($('.content-single').hasClass('detail-hidden')) { - $('.content-single').removeClass('detail-hidden'); - this.resizeMap(); - } - - if (!$('#' + state.el.detail).hasClass('detail-edit')) { - $('#' + state.el.detail).addClass('detail-edit'); - } - }, - - hideDetailPanel: function() { - if (!$('.content-single').hasClass('detail-hidden')) { - $('.content-single').addClass('detail-hidden'); - this.resizeMap($(window).height() - 30); - } - }, - - displayModal: function() { - if (!$('#' + state.el.modal).is(':visible')) { - $('#' + state.el.modal).modal('show'); - } - }, - - hideModal: function() { - if ($('#' + state.el.modal).is(':visible')) { - $('#' + state.el.modal).modal('hide'); - } - }, - - // Only useful if multiple sidebar buttons lead to the map - setSidebar: function(sidebar_button) { - if (!$('#sidebar').hasClass(sidebar_button)) { - $('#sidebar').removeClass().addClass(sidebar_button); - } - }, - - activateTab: function(tab) { - // For the location details page. Tabs are built using bootstrap - var tab_options = ['overview', 'resources', 'relationships']; - tab_options.splice(tab_options.indexOf(tab), 1); - - $('#' + tab + '-tab').addClass('active'); - $($('#' + tab + '-tab').children()[0]).attr({'aria-expanded':"true"}); - $('#' + tab).addClass('active'); - - for (var i in tab_options) { - $('#' + tab_options[i] + '-tab').removeClass('active'); - $($('#' + tab_options[i] + '-tab').children()[0]).attr({'aria-expanded':"false"}); - $('#' + tab_options[i]).removeClass('active'); - } - }, - - setPageTitle: function(new_title) { - var page_title = $('head title').context.title; - - if (!page_title.includes(new_title)) { - page_title = page_title.split('|'); - page_title.splice(1, 1, new_title); - page_title = page_title.join('|'); - - $('head title').context.title = page_title; - } - }, - - /*************** - UPDATING THE STATE - ****************/ - setGeoJsonLayer: function(layer) { - state.geojsonlayer = layer; - }, - - getRouteElement: function(el) { - return state.el[el]; - }, - - setCurrentRelationshipUrl: function(url) { - if (!url.includes(state.current_relationship.url)) { - url = url.split('/'); - state.current_relationship.url = '#/records/relationship/' + url[3] + '/'; - } - - return state.current_relationship.url; - }, - - getCurrentRelationshipUrl: function() { - return state.current_relationship.url; - }, - - setCurrentLocationUrl: function(url) { - if (!url.includes(state.current_location.url)) { - url = url.split('/'); - state.current_location.url = '#/records/location/' + url[3] + '/'; - } - - return state.current_location.url; - }, - - getCurrentLocationUrl: function() { - return state.current_location.url; - }, - - setCurrentLocationFeature: function() { - var url = state.current_location.url; - - if (state.current_location.feature) { - if (url.includes(state.current_location.feature.feature.id)) { - this.setCurrentLocationStyle(); - return; - } - } - - var layers = state.geojsonlayer.geojsonLayer._layers; - var found = false; - - for (var i in layers) { - if (url.includes(layers[i].feature.id)) { - found = true; - this.resetPreviousLocationStyle(); - - state.current_location.feature = layers[i]; - - this.setCurrentLocationStyle(); - layers[i]._popup.setContent(this.activePopup(layers[i].feature)); - return; - } - } - }, - - setCurrentLocationCoords: function(coords) { - if (!state.coords) { - coords = coords.replace('(', '').replace(')', '').split(','); - state.coords = [[coords[0], coords[1]], [coords[2], coords[3]]]; - } - }, - - getCurrentLocationCoords: function() { - if (state.first_load && state.coords) { - map.fitBounds(state.coords); - state.first_load = false; - } - }, - - setCurrentLocationStyle: function() { - var location = state.current_location.feature; - if (location.setStyle) { - location.setStyle({color: '#edaa00', fillColor: '#edaa00', weight: 3}); - } - }, - - resetPreviousLocationStyle: function() { - // before state.current_location is updated, the old state needs to be reset - var location = state.current_location.feature; - if (location) { - if (location.setStyle) { - location.setStyle({color: '#3388ff', fillColor: '#3388ff', weight: 2}); - } - - feature = location.feature; - location._popup.setContent(this.nonActivePopup(feature)); - } - }, - - // Checked to prevent router from firing with each coords change. - setLastHashPath: function(hash) { - state.last_hash = hash; - }, - - getLastHashPath: function() { - return state.last_hash; - }, - - // Checked to see if this is the first time the page has loaded, and if we need to zoom on a location. - setFirstLoad: function(val) { - if (state.first_load != val) { - state.first_load = val; - } - }, - - getFirstLoad: function() { - return state.first_load; - }, - - getSuccessUrl: function(type, tab=null) { - url = ''; - if (type === 'overview') { - return '#/overview/'; - } - - if (type === 'location') { - url = rm.getCurrentLocationUrl() ? rm.getCurrentLocationUrl() : rm.setCurrentLocationUrl(); - } else if (type === 'relationship') { - url = rm.getCurrentRelationshipUrl() ? rm.getCurrentRelationshipUrl() : rm.setCurrentRelationshipUrl(); - } - - url = tab ? url + '?tab=' + tab : url; - - return url; - }, - - /*************** - ADDING EVENT HOOKS - ****************/ - - addEventScript: function(file) { - scripts = $('script[src="/static/js/' + file + '.js"]'); - if (scripts.length) { - $.each(scripts, function(i) { - scripts[i].parentElement.removeChild(scripts[i]); - }); - } - - $('body').append($('')); - }, - - uploadResourceHooks: function() { - rm.addEventScript('file-upload.js'); - }, - - addResourceHooks: function() { - rm.addEventScript('script_add_lib.js'); - }, - - relationshipHooks: function() { - rm.addEventScript('rel_new_item.js'); - rm.addEventScript('rel_tenure.js'); - rm.addEventScript('party_attrs.js'); - - var template = function(party) { - if (!party.id) { - return party.text; - } - return $( - '
' + - '' + party.text + '' + - '' + party.element.dataset.type + '' + - '
' - ); - }; - $("#party-select").select2({ - minimumResultsForSearch: 6, - templateResult: template, - theme: "bootstrap", - }); - - $('.datepicker').datepicker({ - yearRange: "c-200:c+200", - changeMonth: true, - changeYear: true, - }); - }, - - locationDetailHooks: function() { - function formatHashTab(tab) { - var hash = window.location.hash; - var coords = ''; - - if (hash.includes('coords=')) { - coords = hash.substr(hash.indexOf('coords=')); - hash = hash.substr(0, hash.indexOf('coords=') - 1); - } - - if (hash.includes('?tab=' + tab)) { - return; - } - - if (hash.includes('?tab=')) { - hash = hash.split('?tab=')[0]; - } - window.location.hash = hash + '?tab=' + tab + '&' + coords; - rm.activateTab(tab); - } - - $('#resources-tab').click(function() { - formatHashTab('resources'); - }); - - $('#overview-tab').click(function() { - formatHashTab('overview'); - }); - - $('#relationships-tab').click(function() { - formatHashTab('relationships'); - }); - }, - - /*************** - INTERCEPTING FORM SUBMISSIONS - ****************/ - formSubmission: function(form_type, success_url=null, eventHook=null){ - var form_type_options = { - 'detail': '#detail-form', - 'modal': '#modal-form', - 'location-wizard': '#location-wizard', - }; - - var form = form_type_options[form_type] || form_type; - var detach = false; - var parent = this; - - // if form_type is a complete form and not just an id, it came from detachFormSubmission - if (form === form_type) { - detach = true; - } - - $(form).submit(function(e){ - e.preventDefault(); - var target = e.originalEvent || e.originalTarget; - var formaction = $('.submit-btn', target.target ).attr('formAction'); - $('.submit-btn', target.target).prop({'disabled': true}); - - var data = $(this).serializeArray().reduce(function(obj, item) { - obj[item.name] = item.value; - return obj; - }, {}); - - var posturl = $.ajax({ - method: "POST", - url: formaction, - data: data, - success: function(response, status, xhr) { - form_error = posturl.getResponseHeader('Form-Error'); - if (form_error) { - el_type = state.el[form_type] || 'detail'; - var el = document.getElementById(el_type); - el.innerHTML = response; - - rm.formSubmission(form_type, success_url, eventHook); - if (eventHook) { - eventHook(); + settings: { + current_location: { url: null, feature: null }, + current_relationship: { url: null }, + el: { + 'detail': 'project-detail', + 'modal': 'additional-modals', + }, + last_hash: '', + coords: null, + first_load: true, + }, + + init: function () { + state = this.settings; + }, + + updatePage: function (kwargs) { + if (kwargs.page_title) { + this.setPageTitle(kwargs.page_title); + } + + if (kwargs.display_modal) { + this.displayModal(); + } else { + this.hideModal(); + } + + if (kwargs.display_detail_panel) { + this.displayDetailPanel(); + } else { + this.hideDetailPanel(); + } + + if (kwargs.display_edit_panel) { + this.displayEditDetailPanel(); + } + + if (kwargs.active_sidebar) { + this.setSidebar(kwargs.active_sidebar); + } + + if (kwargs.reset_current_location) { + // this.resetPreviousLocationStyle(); + if (state.current_location.feature) { + Styles.resetStyle(state.current_location.feature); + } + } + }, + + updateState: function (kwargs) { + /********* + 'current_location': String. where to find the url for the location connected to the record. Location detail pages will pull from the window.location.hash, Relationship detail pages will pull from the #current-location link + *********/ + if (kwargs.current_location) { + this.setCurrentLocationUrl(kwargs.current_location); + this.setCurrentLocationFeature(); + } + + /********* + 'current_relationship': String. where to find the the url for the relationship connected to the record. Relationship detail pages will pull from the window.location.hash. + *********/ + if (kwargs.current_relationship) { + this.setCurrentRelationshipUrl(kwargs.current_relationship); + } + + /********* + 'datatable': Boolean. Does the page contain a DataTable? + *********/ + if (kwargs.datatable) { + dataTableHook(); + } + + /********* + 'active_tab': S1['overview', 'relationships', 'resources']. Choose which of the bootstrap tabs in the Location Detail page you want activated. + *********/ + if (kwargs.active_tab) { + rm.activateTab(kwargs.active_tab); + } + + /********* + 'check_location_coords': Boolean. Only used when a page first loads. If the page loads to a location/relationship detail page, and if there are not coords, it will zoom in on the coordinates provided in the header. + (used in smap/index.js on 'endtileload') + *********/ + if (kwargs.check_location_coords) { + this.getCurrentLocationCoords(); + } + + /********* + 'detach_forms': Boolean. Does the page contain a Datatable with resources that can be detached? + *********/ + if (kwargs.detach_forms) { + this.detachFormSubmission(); + } + + /********* + 'form': { + 'type': ['modal', 'detail'] Does the form appear in the detail panel, or does a modal appear? + + 'success_url': ['relationship', 'location'] Does the page need to redirect to a location or relationship detail page? + + 'tab': [null, 'overview', 'resources', 'relationships'] If the success url redirects to a Location Detail page, is there a specific tab you want activated? + + 'callback': [null, rm.relationshipHooks, rm.uploadResourcesHooks] Did you call any event hooks after updateState? If the form fails, you'll need to call them again after the form is submitted. + } + *********/ + if (kwargs.form) { + success_url = this.getSuccessUrl( + kwargs.form.success_url, + kwargs.form.tab + ); + rm.formSubmission(kwargs.form.type, success_url, kwargs.form.callback); + } + + }, + + /*************** + DOM MANIPULATION + ****************/ + resizeMap: function (size) { + size = size || 300; + $('#project-map').height(size); + + window.setTimeout(function () { + map.invalidateSize(); + }, 500); + }, + + displayDetailPanel: function () { + if ($('.content-single').hasClass('detail-hidden')) { + $('.content-single').removeClass('detail-hidden'); + this.resizeMap(); + } + + if ($('#' + state.el.detail).hasClass('detail-edit')) { + $('#' + state.el.detail).removeClass('detail-edit'); + } + }, + + displayEditDetailPanel: function () { + if ($('.content-single').hasClass('detail-hidden')) { + $('.content-single').removeClass('detail-hidden'); + this.resizeMap(); + } + + if (!$('#' + state.el.detail).hasClass('detail-edit')) { + $('#' + state.el.detail).addClass('detail-edit'); + } + }, + + hideDetailPanel: function () { + if (!$('.content-single').hasClass('detail-hidden')) { + $('.content-single').addClass('detail-hidden'); + this.resizeMap($(window).height() - 30); + } + }, + + displayModal: function () { + if (!$('#' + state.el.modal).is(':visible')) { + $('#' + state.el.modal).modal('show'); + } + }, + + hideModal: function () { + if ($('#' + state.el.modal).is(':visible')) { + $('#' + state.el.modal).modal('hide'); + } + }, + + // Only useful if multiple sidebar buttons lead to the map + setSidebar: function (sidebar_button) { + if (!$('#sidebar').hasClass(sidebar_button)) { + $('#sidebar').removeClass().addClass(sidebar_button); + } + }, + + activateTab: function (tab) { + // For the location details page. Tabs are built using bootstrap + var tab_options = ['overview', 'resources', 'relationships']; + tab_options.splice(tab_options.indexOf(tab), 1); + + $('#' + tab + '-tab').addClass('active'); + $($('#' + tab + '-tab').children()[0]).attr({ 'aria-expanded': "true" }); + $('#' + tab).addClass('active'); + + for (var i in tab_options) { + $('#' + tab_options[i] + '-tab').removeClass('active'); + $($('#' + tab_options[i] + '-tab').children()[0]).attr({ 'aria-expanded': "false" }); + $('#' + tab_options[i]).removeClass('active'); + } + }, + + setPageTitle: function (new_title) { + var page_title = $('head title').context.title; + + if (!page_title.includes(new_title)) { + page_title = page_title.split('|'); + page_title.splice(1, 1, new_title); + page_title = page_title.join('|'); + + $('head title').context.title = page_title; + } + }, + + /*************** + UPDATING THE STATE + ****************/ + setGeoJsonLayer: function (layer) { + state.geojsonlayer = layer; + }, + + getRouteElement: function (el) { + return state.el[el]; + }, + + setCurrentRelationshipUrl: function (url) { + if (!url.includes(state.current_relationship.url)) { + url = url.split('/'); + state.current_relationship.url = '#/records/relationship/' + url[3] + '/'; + } + + return state.current_relationship.url; + }, + + getCurrentRelationshipUrl: function () { + return state.current_relationship.url; + }, + + setCurrentLocationUrl: function (url) { + if (!url.includes(state.current_location.url)) { + url = url.split('/'); + state.current_location.url = '#/records/location/' + url[3] + '/'; + } + + return state.current_location.url; + }, + + getCurrentLocationUrl: function () { + return state.current_location.url; + }, + + setCurrentLocationFeature: function () { + var url = state.current_location.url; + + if (state.current_location.feature) { + if (url.includes(state.current_location.feature.feature.id)) { + Styles.setSelectedStyle(state.current_location.feature); + return; } + } - } else { - if (detach) { - sr.router(true); - } else { - window.location.replace(success_url); + var layers = state.geojsonlayer.geojsonLayer._layers; + var found = false; + + for (var i in layers) { + if (url.includes(layers[i].feature.id)) { + found = true; + Styles.resetStyle(state.current_location.feature); + state.current_location.feature = layers[i]; + Styles.setSelectedStyle(layers[i]); + return; + } else if (!isNaN(layers[i].feature.id)) { + var new_url = url.substr(2); + new_url = new_url.substr(0, new_url.length -1); + layer_id = new_url.split('/')[2]; + + layers[i].feature.id = layer_id; + layers[i].feature.properties.url = new_url; + + found = true; + Styles.resetStyle(state.current_location.feature); + state.current_location.feature = layers[i]; + Styles.setSelectedStyle(layers[i]); + return; } - } - } - }); - }); - }, - - detachFormSubmission: function(){ - var parent = this; - $.each($('.detach-form'), function(i, form){ - parent.formSubmission(form); - }); - - if ($('.paginate_button').length) { - $('.paginate_button.next').on('click', function(i, button) { - parent.detachFormSubmission(); - }); - $('.paginate_button.previous').on('click', function(i, button) { - parent.detachFormSubmission(); - }); - } - }, - - /****************** - EXTRA HTML TEMPLATES - ******************/ - permissionDenied: function(error){ - return ""; - }, - - nonActivePopup: function(feature) { - return "

Location" + - feature.properties.type + + } + + + }, + + setCurrentLocationCoords: function (coords) { + if (!state.coords) { + coords = coords.replace('(', '').replace(')', '').split(','); + state.coords = [ + [coords[0], coords[1]], + [coords[2], coords[3]] + ]; + } + }, + + getCurrentLocationCoords: function () { + if (state.first_load && state.coords) { + map.fitBounds(state.coords); + state.first_load = false; + } + }, + + // Checked to prevent router from firing with each coords change. + setLastHashPath: function (hash) { + state.last_hash = hash; + }, + + getLastHashPath: function () { + return state.last_hash; + }, + + // Checked to see if this is the first time the page has loaded, and if we need to zoom on a location. + setFirstLoad: function (val) { + if (state.first_load != val) { + state.first_load = val; + } + }, + + getFirstLoad: function () { + return state.first_load; + }, + + getSuccessUrl: function (type, tab = null) { + url = ''; + if (type === 'overview') { + return '#/overview/'; + } + + if (type === 'location') { + url = rm.getCurrentLocationUrl() ? rm.getCurrentLocationUrl() : rm.setCurrentLocationUrl(); + } else if (type === 'relationship') { + url = rm.getCurrentRelationshipUrl() ? rm.getCurrentRelationshipUrl() : rm.setCurrentRelationshipUrl(); + } + + url = tab ? url + '?tab=' + tab : url; + + return url; + }, + + /*************** + ADDING EVENT HOOKS + ****************/ + + addEventScript: function (file) { + scripts = $('script[src="/static/js/' + file + '.js"]'); + if (scripts.length) { + $.each(scripts, function (i) { + scripts[i].parentElement.removeChild(scripts[i]); + }); + } + + $('body').append($('')); + }, + + uploadResourceHooks: function () { + rm.addEventScript('file-upload.js'); + }, + + addResourceHooks: function () { + rm.addEventScript('script_add_lib.js'); + }, + + relationshipHooks: function () { + rm.addEventScript('rel_new_item.js'); + rm.addEventScript('rel_tenure.js'); + rm.addEventScript('party_attrs.js'); + + var template = function (party) { + if (!party.id) { + return party.text; + } + return $( + '
' + + '' + party.text + '' + + '' + party.element.dataset.type + '' + + '
' + ); + }; + $("#party-select").select2({ + minimumResultsForSearch: 6, + templateResult: template, + theme: "bootstrap", + }); + + $('.datepicker').datepicker({ + yearRange: "c-200:c+200", + changeMonth: true, + changeYear: true, + }); + }, + + locationDetailHooks: function () { + function formatHashTab(tab) { + var hash = window.location.hash; + var coords = ''; + + if (hash.includes('coords=')) { + coords = hash.substr(hash.indexOf('coords=')); + hash = hash.substr(0, hash.indexOf('coords=') - 1); + } + + if (hash.includes('?tab=' + tab)) { + return; + } + + if (hash.includes('?tab=')) { + hash = hash.split('?tab=')[0]; + } + window.location.hash = hash + '?tab=' + tab + '&' + coords; + rm.activateTab(tab); + } + + $('#resources-tab').click(function () { + formatHashTab('resources'); + }); + + $('#overview-tab').click(function () { + formatHashTab('overview'); + }); + + $('#relationships-tab').click(function () { + formatHashTab('relationships'); + }); + }, + + /*************** + INTERCEPTING FORM SUBMISSIONS + ****************/ + formSubmission: function (form_type, success_url = null, eventHook = null) { + var form_type_options = { + 'detail': '#detail-form', + 'modal': '#modal-form', + 'location-wizard': '#location-wizard', + }; + + var form = form_type_options[form_type] || form_type; + var detach = false; + var parent = this; + + // if form_type is a complete form and not just an id, it came from detachFormSubmission + if (form === form_type) { + detach = true; + } + + $(form).submit(function (e) { + e.preventDefault(); + var target = e.originalEvent || e.originalTarget || e.target; + var formaction = $('.submit-btn', target.target).attr('formaction'); + $('.submit-btn', target.target).prop({ 'disabled': true }); + + var data = $(this).serializeArray().reduce(function (obj, item) { + obj[item.name] = item.value; + return obj; + }, {}); + + var posturl = $.ajax({ + method: "POST", + url: formaction, + data: data, + success: function (response, status, xhr) { + form_error = posturl.getResponseHeader('Form-Error'); + if (form_error) { + el_type = state.el[form_type] || 'project-detail'; + var el = document.getElementById(el_type); + el.innerHTML = response; + + rm.formSubmission(form_type, success_url, eventHook); + if (eventHook) { + eventHook(); + } + + } else if (typeof response === 'object' && 'new_location_id' in response) { + window.location.replace('#/records/location/' + response.new_location_id + '/'); + } else { + if (detach) { + sr.router(true); + } else { + window.location.replace(success_url); + } + } + } + }); + }); + }, + + detachFormSubmission: function () { + var parent = this; + $.each($('.detach-form'), function (i, form) { + parent.formSubmission(form); + }); + + if ($('.paginate_button').length) { + $('.paginate_button.next').on('click', function (i, button) { + parent.detachFormSubmission(); + }); + $('.paginate_button.previous').on('click', function (i, button) { + parent.detachFormSubmission(); + }); + } + }, + + /****************** + EXTRA HTML TEMPLATES + ******************/ + permissionDenied: function (error) { + return ""; + }, + + nonActivePopup: function (feature) { + return "

Location" + + feature.properties.type + "

" + ""; - }, + }, - activePopup: function(feature) { - return "

Location" + - feature.properties.type + + activePopup: function (feature) { + return "

Location" + + feature.properties.type + "

" + "
" + - options.trans.current_viewing + - "
"; - }, + options.trans.current_viewing + + "

"; + }, }; diff --git a/cadasta/core/static/js/smap/routes.js b/cadasta/core/static/js/smap/routes.js index 26d40e618..9be814f02 100644 --- a/cadasta/core/static/js/smap/routes.js +++ b/cadasta/core/static/js/smap/routes.js @@ -1,373 +1,393 @@ var rm = RouterMixins; -var CreateRoutes = function(){ - 'use strict'; - var routes = {}; - rm.init(); - - function route(path, el, controller, eventHook=null) { - routes[path] = { - el: el, - controller: controller, - eventHook: eventHook - }; - } - - /********************* - MAP - *********************/ - - route('/map/', 'detail', - function() { - rm.updatePage({ - 'page_title': options.trans.project_map, - 'display_detail_panel': false, - 'display_modal': false, - 'active_sidebar': 'map', - }); - }); - - /********************* - OVERVIEW - *********************/ - - route('/overview/', 'detail', - function() { - rm.updatePage({ - 'page_title': options.trans.project_overview, - 'display_detail_panel': true, - 'display_modal': false, - 'active_sidebar': 'overview', - - 'reset_current_location': true, - }); - }); - - route('/', 'detail', - function() { - rm.updatePage({ - 'page_title': options.trans.project_overview, - 'display_detail_panel': true, - 'display_modal': false, - 'active_sidebar': 'overview', - - 'reset_current_location': true, - }); - }); - - - /************** - SPATIAL RECORDS - **************/ - route('/records/location/', 'detail', - function() { - rm.updatePage({ - 'page_title': options.trans.location_detail, - 'display_detail_panel': true, - 'display_modal': false, - }); - }, - function(){ - rm.updateState({ - 'current_location': window.location.hash, - 'datatable': true, - 'detach_forms': true, - 'active_tab': 'overview', - }); - - rm.locationDetailHooks(); - }); - - /********************* - SPATIAL DETAIL TABS - *********************/ - route('/records/location/?tab=overview', 'detail', - function() { - rm.updatePage({ - 'page_title': options.trans.location_detail, - 'display_detail_panel': true, - 'display_modal': false, - }); - }, - function(){ - rm.updateState({ - 'current_location': window.location.hash, - 'datatable': true, - 'detach_forms': true, - 'active_tab': 'overview', - }); - - rm.locationDetailHooks(); - }); - - route('/records/location/?tab=resources', 'detail', - function() { - rm.updatePage({ - 'page_title': options.trans.location_detail, - 'display_detail_panel': true, - 'display_modal': false, - }); - }, - function(){ - rm.updateState({ - 'current_location': window.location.hash, - 'datatable': true, - 'detach_forms': true, - 'active_tab': 'resources', - }); - - rm.locationDetailHooks(); - }); - - route('/records/location/?tab=relationships', 'detail', - function() { - rm.updatePage({ - 'page_title': options.trans.location_detail, - 'display_detail_panel': true, - 'display_modal': false, - }); - }, - function(){ - rm.updateState({ - 'current_location': window.location.hash, - 'datatable': true, - 'detach_forms': true, - 'active_tab': 'relationships', - }); - rm.locationDetailHooks(); - }); - - /********************* - SPATIAL ACTIONS - *********************/ - - /****** REQUIRES GEOEDITING *************/ - route('/records/location/new/', 'detail', - function() { - rm.updatePage({ - 'page_title': options.trans.location_add, - 'display_edit_panel': true, - 'display_modal': false, - }); - }); - /*******************************************/ - - - /****** REQUIRES GEOEDITING *************/ - route('/records/location/edit/', 'detail', - function() { - rm.updatePage({ - 'page_title': options.trans.location_edit, - 'display_edit_panel': true, - 'display_modal': false, - }); - }, - function(){ - rm.updateState({ - 'current_location': window.location.hash, - 'form': { - 'type': 'location-wizard', - 'success_url': 'location', - } - }); - }); - /*******************************************/ - - route('/records/location/delete/', 'modal', - function() { - rm.updatePage({ - 'page_title': options.trans.location_delete, - 'display_detail_panel': true, - 'display_modal': true, - }); - }, - function(){ - rm.updateState({ - 'current_location': window.location.hash, - 'form': { - 'type': 'modal', - 'success_url': 'overview', +var CreateRoutes = function (map) { + 'use strict'; + var routes = {}; + rm.init(); + + function route(path, el, controller, eventHook = null) { + routes[path] = { + el: el, + controller: controller, + eventHook: eventHook + }; + } + + /********************* + MAP + *********************/ + + route('/map/', 'detail', + function () { + rm.updatePage({ + 'page_title': options.trans.project_map, + 'display_detail_panel': false, + 'display_modal': false, + 'active_sidebar': 'map', + }); + map.locationEditor.fire('route:map'); + }); + + /********************* + OVERVIEW + *********************/ + + route('/overview/', 'detail', + function () { + rm.updatePage({ + 'page_title': options.trans.project_overview, + 'display_detail_panel': true, + 'display_modal': false, + 'active_sidebar': 'overview', + + 'reset_current_location': true, + }); + map.locationEditor.fire('route:overview'); + }); + + route('/', 'detail', + function () { + rm.updatePage({ + 'page_title': options.trans.project_overview, + 'display_detail_panel': true, + 'display_modal': false, + 'active_sidebar': 'overview', + 'reset_current_location': true, + }); + }); + + + /************** + SPATIAL RECORDS + **************/ + route('/records/location/', 'detail', + function () { + rm.updatePage({ + 'page_title': options.trans.location_detail, + 'display_detail_panel': true, + 'display_modal': false, + }); + }, + function () { + rm.updateState({ + 'current_location': window.location.hash, + 'datatable': true, + 'detach_forms': true, + 'active_tab': 'overview', + }); + + rm.locationDetailHooks(); + map.locationEditor.fire('route:location:detail'); + }); + + /********************* + SPATIAL DETAIL TABS + *********************/ + route('/records/location/?tab=overview', 'detail', + function () { + rm.updatePage({ + 'page_title': options.trans.location_detail, + 'display_detail_panel': true, + 'display_modal': false, + }); }, - }); - }); - - /********************* - SPATIAL RESOURCES - *********************/ - - route('/records/location/resources/add/', 'modal', - function() { - rm.updatePage({ - 'page_title': options.trans.location_resource_add, - 'display_detail_panel': true, - 'display_modal': true, - }); - }, - function() { - rm.updateState({ - 'current_location': window.location.hash, - 'active_tab': 'resources', - 'datatable': true, - 'form': { - 'type': 'modal', - 'success_url': 'location', - 'tab': 'resources', + function () { + rm.updateState({ + 'current_location': window.location.hash, + 'datatable': true, + 'detach_forms': true, + 'active_tab': 'overview', + }); + + rm.locationDetailHooks(); + }); + + route('/records/location/?tab=resources', 'detail', + function () { + rm.updatePage({ + 'page_title': options.trans.location_detail, + 'display_detail_panel': true, + 'display_modal': false, + }); }, - }); - rm.addResourceHooks(); - }); - - route('/records/location/resources/new/', 'modal', - function() { - rm.updatePage({ - 'page_title': options.trans.location_resource_new, - 'display_detail_panel': true, - 'display_modal': true, - }); - }, - function() { - rm.updateState({ - 'current_location': window.location.hash, - 'active_tab': 'resources', - 'form': { - 'type': 'modal', - 'success_url': 'location', - 'tab': 'resources', - 'callback': rm.uploadResourceHooks, + function () { + rm.updateState({ + 'current_location': window.location.hash, + 'datatable': true, + 'detach_forms': true, + 'active_tab': 'resources', + }); + + rm.locationDetailHooks(); + }); + + route('/records/location/?tab=relationships', 'detail', + function () { + rm.updatePage({ + 'page_title': options.trans.location_detail, + 'display_detail_panel': true, + 'display_modal': false, + }); }, - }); - - rm.uploadResourceHooks(); - }); - - /********************* - SPATIAL RELATIONSHIPS - *********************/ - - route('/records/location/relationships/new/', 'modal', - function() { - rm.updatePage({ - 'page_title': options.trans.rel_add, - 'display_detail_panel': true, - 'display_modal': true, - }); - - }, function() { - rm.updateState({ - 'current_location': window.location.hash, - 'active_tab': 'relationships', - 'form': { - 'type': 'modal', - 'success_url': 'location', - 'tab': 'relationships', - 'callback': rm.relationshipHooks, + function () { + rm.updateState({ + 'current_location': window.location.hash, + 'datatable': true, + 'detach_forms': true, + 'active_tab': 'relationships', + }); + rm.locationDetailHooks(); + }); + + /********************* + SPATIAL ACTIONS + *********************/ + + /****** REQUIRES GEOEDITING *************/ + route('/records/location/new/', 'detail', + function () { + rm.updatePage({ + 'page_title': options.trans.location_add, + 'display_edit_panel': true, + 'display_modal': false, + }); + map.locationEditor.fire('route:location:new'); }, - }); - rm.relationshipHooks(); - }); - - /******************* - RELATIONSHIP RECORDS - *******************/ - route('/records/relationship/', 'detail', - function() { - rm.updatePage({ - 'page_title': options.trans.rel_detail, - 'display_detail_panel': true, - 'display_modal': false, - }); - }, - function() { - rm.updateState({ - 'current_location': $("#current-location").attr('href'), - 'current_relationship': window.location.hash, - 'active_tab': 'relationships', - 'datatable': true, - 'detach_forms': true, - }); - }); - - /******************* - RELATIONSHIP ACTIONS - *******************/ - - route('/records/relationship/edit/', 'detail', - function() { - rm.updatePage({ - 'page_title': options.trans.rel_edit, - 'display_edit_panel': true, - 'display_modal': false, - }); - }, function(){ - rm.updateState({ - 'current_location': $("#current-location").attr('href'), - 'current_relationship': window.location.hash, - 'form': { - 'type': 'detail', - 'success_url': 'relationship', + function () { + rm.updateState({ + 'form': { + 'type': 'location-wizard', + 'success_url': 'overview', + } + }); + } + ); + /*******************************************/ + + + /****** REQUIRES GEOEDITING *************/ + route('/records/location/edit/', 'detail', + function () { + rm.updatePage({ + 'page_title': options.trans.location_edit, + 'display_edit_panel': true, + 'display_modal': false, + }); }, - }); - }); - - route('/records/relationship/delete/', 'modal', - function() { - rm.updatePage({ - 'page_title': options.trans.rel_delete, - 'display_detail_panel': true, - 'display_modal': true, - }); - }, - function(){ - rm.updateState({ - 'current_relationship': window.location.hash, - 'form': { - 'type': 'modal', - 'success_url': 'location', - 'tab': 'relationships', + function () { + rm.updateState({ + 'current_location': window.location.hash, + 'form': { + 'type': 'location-wizard', + 'success_url': 'location', + } + }); + // trigger editing once tiles finished loading + var hash_path = window.location.hash.slice(1) || '/'; + var fid = hash_path.split('/')[3]; + map.locationEditor.fire('route:location:edit', { 'fid': fid }); + }); + /*******************************************/ + + route('/records/location/delete/', 'modal', + function () { + rm.updatePage({ + 'page_title': options.trans.location_delete, + 'display_detail_panel': true, + 'display_modal': true, + }); }, - }); - }); - - - /********************* - RELATIONSHIP RESOURCES - *********************/ - route('/records/relationship/resources/add/', 'modal', - function() { - rm.updatePage({ - 'page_title': options.trans.rel_resource_add, - 'display_detail_panel': true, - 'display_modal': true, - }); - }, function() { - rm.updateState({ - 'current_relationship': window.location.hash, - 'datatable': true, - 'form': { - 'type': 'modal', - 'success_url': 'relationship', + function () { + rm.updateState({ + 'current_location': window.location.hash, + 'form': { + 'type': 'modal', + 'success_url': 'overview', + }, + }); + }); + + /********************* + SPATIAL RESOURCES + *********************/ + + route('/records/location/resources/add/', 'modal', + function () { + rm.updatePage({ + 'page_title': options.trans.location_resource_add, + 'display_detail_panel': true, + 'display_modal': true, + }); }, - }); - rm.addResourceHooks(); - }); - - route('/records/relationship/resources/new/', 'modal', - function() { - rm.updatePage({ - 'page_title': options.trans.rel_resource_new, - 'display_detail_panel': true, - 'display_modal': true, - }); - }, function() { - rm.updateState({ - 'current_relationship': window.location.hash, - 'form': { - 'type': 'modal', - 'success_url': 'relationship', - 'callback': rm.uploadResourceHooks, + function () { + rm.updateState({ + 'current_location': window.location.hash, + 'active_tab': 'resources', + 'datatable': true, + 'form': { + 'type': 'modal', + 'success_url': 'location', + 'tab': 'resources', + }, + }); + rm.addResourceHooks(); + }); + + route('/records/location/resources/new/', 'modal', + function () { + rm.updatePage({ + 'page_title': options.trans.location_resource_new, + 'display_detail_panel': true, + 'display_modal': true, + }); }, - }); - rm.uploadResourceHooks(); - }); + function () { + rm.updateState({ + 'current_location': window.location.hash, + 'active_tab': 'resources', + 'form': { + 'type': 'modal', + 'success_url': 'location', + 'tab': 'resources', + 'callback': rm.uploadResourceHooks, + }, + }); + + rm.uploadResourceHooks(); + }); + + /********************* + SPATIAL RELATIONSHIPS + *********************/ + + route('/records/location/relationships/new/', 'modal', + function () { + rm.updatePage({ + 'page_title': options.trans.rel_add, + 'display_detail_panel': true, + 'display_modal': true, + }); - return routes; + }, + function () { + rm.updateState({ + 'current_location': window.location.hash, + 'active_tab': 'relationships', + 'form': { + 'type': 'modal', + 'success_url': 'location', + 'tab': 'relationships', + 'callback': rm.relationshipHooks, + }, + }); + rm.relationshipHooks(); + }); + + /******************* + RELATIONSHIP RECORDS + *******************/ + route('/records/relationship/', 'detail', + function () { + rm.updatePage({ + 'page_title': options.trans.rel_detail, + 'display_detail_panel': true, + 'display_modal': false, + }); + }, + function () { + rm.updateState({ + 'current_location': $("#current-location").attr('href'), + 'current_relationship': window.location.hash, + 'active_tab': 'relationships', + 'datatable': true, + 'detach_forms': true, + }); + }); + + /******************* + RELATIONSHIP ACTIONS + *******************/ + + route('/records/relationship/edit/', 'detail', + function () { + rm.updatePage({ + 'page_title': options.trans.rel_edit, + 'display_edit_panel': true, + 'display_modal': false, + }); + }, + function () { + rm.updateState({ + 'current_location': $("#current-location").attr('href'), + 'current_relationship': window.location.hash, + 'form': { + 'type': 'detail', + 'success_url': 'relationship', + }, + }); + }); + + route('/records/relationship/delete/', 'modal', + function () { + rm.updatePage({ + 'page_title': options.trans.rel_delete, + 'display_detail_panel': true, + 'display_modal': true, + }); + }, + function () { + rm.updateState({ + 'current_relationship': window.location.hash, + 'form': { + 'type': 'modal', + 'success_url': 'location', + 'tab': 'relationships', + }, + }); + }); + + + /********************* + RELATIONSHIP RESOURCES + *********************/ + route('/records/relationship/resources/add/', 'modal', + function () { + rm.updatePage({ + 'page_title': options.trans.rel_resource_add, + 'display_detail_panel': true, + 'display_modal': true, + }); + }, + function () { + rm.updateState({ + 'current_relationship': window.location.hash, + 'datatable': true, + 'form': { + 'type': 'modal', + 'success_url': 'relationship', + }, + }); + rm.addResourceHooks(); + }); + + route('/records/relationship/resources/new/', 'modal', + function () { + rm.updatePage({ + 'page_title': options.trans.rel_resource_new, + 'display_detail_panel': true, + 'display_modal': true, + }); + }, + function () { + rm.updateState({ + 'current_relationship': window.location.hash, + 'form': { + 'type': 'modal', + 'success_url': 'relationship', + 'callback': rm.uploadResourceHooks, + }, + }); + rm.uploadResourceHooks(); + }); + + return routes; }; diff --git a/cadasta/party/views/async.py b/cadasta/party/views/async.py index 931137f09..7086756b2 100644 --- a/cadasta/party/views/async.py +++ b/cadasta/party/views/async.py @@ -37,17 +37,17 @@ def get_context_data(self, *args, **kwargs): questionnaire_id=project.current_questionnaire) context['type_labels'] = template_xlang_labels( tenure_type.label_xlat) - except Question.DoesNotExist: + except Question.DoesNotExist: + pass + else: + try: + option = QuestionOption.objects.get( + question=tenure_type, + name=context['relationship'].tenure_type_id) + context['type_choice_labels'] = template_xlang_labels( + option.label_xlat) + except QuestionOption.DoesNotExist: pass - else: - try: - option = QuestionOption.objects.get( - question=tenure_type, - name=context['relationship'].tenure_type_id) - context['type_choice_labels'] = template_xlang_labels( - option.label_xlat) - except Question.DoesNotExist: - pass return context diff --git a/cadasta/spatial/tests/test_views_async.py b/cadasta/spatial/tests/test_views_async.py index 6ad4aa2cc..febe26840 100644 --- a/cadasta/spatial/tests/test_views_async.py +++ b/cadasta/spatial/tests/test_views_async.py @@ -1,31 +1,31 @@ -import pytest import json from importlib import import_module -from django.http import HttpRequest, Http404 -from django.core.urlresolvers import reverse + +import pytest + +from accounts.tests.factories import UserFactory +from core.tests.utils.cases import FileStorageTestCase, UserTestCase +from core.tests.utils.files import make_dirs # noqa from django.conf import settings from django.contrib.contenttypes.models import ContentType +from django.core.urlresolvers import reverse +from django.http import Http404, HttpRequest from django.test import TestCase -from questionnaires.tests import factories as q_factories -from skivvy import APITestCase, ViewTestCase, remove_csrf - -from tutelary.models import Policy, assign_user_policies from jsonattrs.models import Attribute, AttributeType, Schema - -from accounts.tests.factories import UserFactory from organization.tests.factories import ProjectFactory -from core.tests.utils.cases import UserTestCase, FileStorageTestCase -from core.tests.utils.files import make_dirs # noqa +from party.models import Party, TenureRelationship, TenureRelationshipType +from party.tests.factories import PartyFactory, TenureRelationshipFactory +from questionnaires.tests import factories as q_factories +from resources.forms import AddResourceFromLibraryForm, ResourceForm from resources.tests.factories import ResourceFactory from resources.tests.utils import clear_temp # noqa -from resources.forms import AddResourceFromLibraryForm, ResourceForm -from party.tests.factories import PartyFactory, TenureRelationshipFactory -from party.models import Party, TenureRelationship, TenureRelationshipType +from skivvy import APITestCase, ViewTestCase, remove_csrf +from tutelary.models import Policy, assign_user_policies -from .factories import SpatialUnitFactory from .. import forms from ..models import SpatialUnit from ..views import async +from .factories import SpatialUnitFactory SessionStore = import_module(settings.SESSION_ENGINE).SessionStore @@ -33,20 +33,19 @@ def assign_policies(user, deny_edit_delete_permissions=False): clauses = { 'clause': [ - # { - # "effect": "allow", - # "object": ["*"], - # "action": ["org.*"] - # }, { - # 'effect': 'allow', - # 'object': ['organization/*'], - # 'action': ['org.*', "org.*.*"] - # }, { - # 'effect': 'allow', - # 'object': ['project/*/*'], - # 'action': ['project.*', 'project.*.*', 'spatial.*'] - # }, { + "effect": "allow", + "object": ["*"], + "action": ["org.*"] + }, { + 'effect': 'allow', + 'object': ['organization/*'], + 'action': ['org.*', "org.*.*"] + }, { + 'effect': 'allow', + 'object': ['project/*/*'], + 'action': ['project.*', 'project.*.*', 'spatial.*'] + }, { 'effect': 'allow', 'object': ['project/*/*'], 'action': ['spatial.*', 'tenure_rel.*'] @@ -64,10 +63,10 @@ def assign_policies(user, deny_edit_delete_permissions=False): if deny_edit_delete_permissions: deny_clause = { - 'effect': 'deny', - 'object': ['spatial/*/*/*'], - 'action': ['spatial.update', 'spatial.delete'] - } + 'effect': 'deny', + 'object': ['spatial/*/*/*'], + 'action': ['spatial.update', 'spatial.delete'] + } clauses['clause'].append(deny_clause) policy = Policy.objects.create( @@ -126,16 +125,21 @@ def get_success_url(self): return (reverse('organization:project-dashboard', kwargs={ 'organization': self.project.organization.slug, 'project': self.project.slug, - }) + '#/records/location/{}/'.format(self.location_created.id)) + }) + '#/records/location/{}/'.format(self.location_created.id)) def test_get_with_authorized_user(self): user = UserFactory.create() assign_policies(user) response = self.request(user=user) assert response.status_code == 200 + submit_url = ( + '/async/organizations/{}/projects/{}/records/location/new/' + .format(self.project.organization.slug, + self.project.slug) + ) expected_content = self.render_content(cancel_url=reverse( 'organization:project-dashboard', kwargs=self.setup_url_kwargs()) + - '#/overview/') + '#/overview/', submit_url=submit_url) assert response.content == expected_content assert ' + {% endblock %} {% block extra_script %} @@ -46,7 +47,7 @@ rel_resource_new: '{% trans " Upload new resource for relationship " %}', rel_delete: '{% trans " Delete relationship " %}', }; - + {% if project.extent %} var projectExtent = {{ project.extent.geojson|safe }}; {% else %} @@ -64,6 +65,8 @@ var layers = {{ leaflet_tiles|safe }} + + @@ -71,6 +74,12 @@ + + + + + + {% endblock %} diff --git a/cadasta/templates/organization/project_wrapper.html b/cadasta/templates/organization/project_wrapper.html index be9196031..098c1a61e 100644 --- a/cadasta/templates/organization/project_wrapper.html +++ b/cadasta/templates/organization/project_wrapper.html @@ -47,15 +47,15 @@

{% trans "More actions" %}
-
{% trans "More" %}
+
{% trans "More" %}
{% endif %}

@@ -111,7 +111,7 @@

{% trans "Import data" %} {% endif %} - + @@ -186,7 +186,7 @@

{% trans "Unarchive project" %}

+ + diff --git a/cadasta/templates/spatial/location_delete.html b/cadasta/templates/spatial/location_delete.html index a9e0147b3..bbfca0993 100644 --- a/cadasta/templates/spatial/location_delete.html +++ b/cadasta/templates/spatial/location_delete.html @@ -12,7 +12,7 @@

{% 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 @@

{% trans "Location" %} {% if add == 'yes' %}{% trans "Add locat

{% trans "Draw location on map" %}

+
{{ form.geometry.errors }}

{% trans "Overview" %}

@@ -33,7 +34,10 @@

{% trans "Overview" %}

{% endfor %} diff --git a/cadasta/templates/spatial/relationship_add.html b/cadasta/templates/spatial/relationship_add.html index 38d8775dc..39786502c 100644 --- a/cadasta/templates/spatial/relationship_add.html +++ b/cadasta/templates/spatial/relationship_add.html @@ -4,6 +4,8 @@ {% load staticfiles %} {% block page_title %}{% trans "Add relationship" %} | {% endblock %} + +{% block form_modal %}