L.FieldStore = L.Class.extend({ initialize: function (fieldid, options) { this.formfield = document.getElementById(fieldid); L.setOptions(this, options); }, load: function () { var value = (this.formfield.value || ''); return this._deserialize(value); }, save: function (layer) { this.formfield.value = this._serialize(layer); }, _serialize: function (layer) { var items = typeof(layer.getLayers) == 'function' ? layer.getLayers() : [layer], is_multi = this.options.is_collection || items.length > 1, is_generic = this.options.is_generic, collection_type = this.options.collection_type, is_empty = items.length === 0; if (is_empty) return ''; var geom = is_multi ? layer : items[0]; if (typeof geom.toGeoJSON != 'function') { throw 'Unsupported layer type ' + geom.constructor.name; } // Leaflet requires access to original feature attribute for GeoJSON // serialization. (see https://github.com/Leaflet/Leaflet/blob/v0.7.3/src/layer/GeoJSON.js#L256-L258) // When creating new records, it's empty so we force it here. if (!geom.feature) { geom.feature = {geometry: {type: this.options.geom_type}}; } var geojson = geom.toGeoJSON(this.options.precision); var is_geometrycollection = (geojson.geometry && geojson.geometry.type == 'GeometryCollection'); if (is_multi && is_generic && !is_geometrycollection) { var flat = {type: 'GeometryCollection', geometries: []}; for (var i=0; i < geojson.features.length; i++) { flat.geometries.push(geojson.features[i].geometry); } geojson = flat; } // Special case for MultiPolyline/MultiPolygon because it was removed from leaflet 1.0 else if (is_multi && collection_type != 'featureGroup') { var latlngs = []; for (var i = 0; i < geojson.features.length; i++) { var latlng = []; var coord = geojson.features[i].geometry.coordinates; if (collection_type == 'polygon') { coord = coord[0]; } for (var j = 0; j < coord.length; j++) { latlng.push([coord[j][1], coord[j][0]]); } if (collection_type == 'polygon') { latlng = [latlng]; } latlngs.push(latlng); } geom = L[collection_type](latlngs); geojson = geom.toGeoJSON(this.options.precision).geometry; } // In order to make multipoint work, it seems we need to treat it similarly to the GeometryCollections else if (this.options.geom_type == 'MULTIPOINT') { var flat = {type: 'MultiPoint', coordinates: []}; for (var i=0; i < geojson.features.length; i++) { flat.coordinates.push(geojson.features[i].geometry.coordinates); } geojson = flat; } else { geojson = geojson.geometry; } return JSON.stringify(geojson); }, _deserialize: function (value) { if (/^\s*$/.test(value)) { return null; } // Helps to get rid of the float value conversion error this.formfield.value = JSON.stringify(JSON.parse(value)); return L.GeoJSON.geometryToLayer(JSON.parse(value)); }, }); L.GeometryField = L.Class.extend({ statics: { unsavedText: 'Map geometry is unsaved' }, options: { field_store_class: L.FieldStore }, initialize: function (options) { var geom_type = options.geom_type.toLowerCase(); options.is_generic = /geometry/.test(geom_type); options.is_collection = /(^multi|collection$)/.test(geom_type); options.is_linestring = /linestring$/.test(geom_type) || options.is_generic; options.is_polygon = /polygon$/.test(geom_type) || options.is_generic; options.is_point = /point$/.test(geom_type) || options.is_generic; options.collection_type = ({ 'multilinestring': 'polyline', 'multipolygon': 'polygon', })[geom_type] || 'featureGroup'; L.setOptions(this, options); this._drawControl = null; this._unsavedChanges = false; // Warn if leaving with unsaved changes var _beforeunload = window.onbeforeunload; window.onbeforeunload = L.Util.bind(function(e) { if (this._unsavedChanges) return L.GeometryField.unsavedText; if (typeof(_beforeunload) == 'function') return _beforeunload(); }, this); }, addTo: function (map) { this._map = map; var store_opts = L.Util.extend(this.options, {defaults: map.defaults}); this.store = new this.options.field_store_class(this.options.fieldid, store_opts); this.drawnItems = this._editionLayer(); map.addLayer(this.drawnItems); // Initialize the draw control and pass it the FeatureGroup of editable layers var drawControl = this._drawControl = new L.Control.Draw(this._controlDrawOptions()); if (this.options.modifiable) { map.addControl(drawControl); L.DomUtil.addClass(drawControl._container, this.options.fieldid); // // In case there is several draw controls on the same map (target map option) map['drawControl' + this.options.fieldid] = drawControl; // We use a flag to ignore events of other draw controls for (var toolbar in drawControl._toolbars) { drawControl._toolbars[toolbar].on('enable disable', function (e) { this._acceptDrawEvents = e.type === 'enable'; }, this); } map.on('draw:created draw:edited draw:deleted', function (e) { // Ignore if coming from other Draw controls if (!this._acceptDrawEvents) return; // Call onCreated(), onEdited(), onDeleted() var eventName = e.type.replace('draw:', ''), method = 'on' + eventName.charAt(0).toUpperCase() + eventName.slice(1); this[method](e); }, this); // Flag for unsaved changes map.on('draw:drawstart draw:editstart', function () { if (this._acceptDrawEvents) this._unsavedChanges = true; }, this); map.on('draw:drawstop draw:editstop', function () { if (this._acceptDrawEvents) this._unsavedChanges = false; }, this); } this.load(); map.fire('map:loadfield', {field: this, fieldid: this.options.fieldid}); return this; }, load: function () { var geometry = this.store.load(); if (geometry) { // Add initial geometry to the map if (geometry instanceof L.LayerGroup) { geometry.eachLayer(function (l) { this.drawnItems.addLayer(l); }, this); } else if (this.options.collection_type !== 'featureGroup' && (geometry instanceof L.Polygon || geometry instanceof L.Polyline)) { var latlngs = geometry.getLatLngs(); for (var i = 0; i < latlngs.length; i++) { this.drawnItems.addLayer(L[this.options.collection_type](latlngs[i])); } } else { this.drawnItems.addLayer(geometry); } this.drawnItems.addTo(this._map); } this._setView(); return geometry; }, _setView: function () { // Change view extent if (this.drawnItems.getLayers().length > 0) { var bounds = this.drawnItems.getBounds(); var options = { maxZoom: this._map.maxZoom || 15 }; this._map.fitBounds(bounds, options); } // Else keep view extent set by django-leaflet template tag }, onCreated: function (e) { // Remove previously drawn if field is not collection. if (!this.options.is_collection) { this.drawnItems.eachLayer(function (l) { this._map.removeLayer(l); }, this); this.drawnItems.clearLayers(); } var layer = e.layer; this._map.addLayer(layer); this.drawnItems.addLayer(layer); this.store.save(this.drawnItems); }, onEdited: function (e) { this.store.save(this.drawnItems); }, onDeleted: function (e) { var layer = e.layer; this.drawnItems.removeLayer(layer); this.store.save(this.drawnItems); }, _editionLayer: function () { var type = 'featureGroup', constructor = L[type]; if (typeof(constructor) != 'function') { throw 'Unsupported geometry type: ' + type; } return constructor([], {}); }, _controlDrawOptions: function () { return { edit: { featureGroup: this.drawnItems }, draw: { polyline: this.options.is_linestring, polygon: this.options.is_polygon, circle: false, // Turns off this drawing tool rectangle: this.options.is_polygon, marker: this.options.is_point, } }; } });