diff --git a/README.rst b/README.rst
index fd4cb62..f1c1401 100644
--- a/README.rst
+++ b/README.rst
@@ -6,8 +6,7 @@ This extension contains plugins that add geospatial capabilities to CKAN.
The following plugins are currently available:
* Spatial model for CKAN datasets and automatic geo-indexing (`spatial_metadata`)
-* Spatial Search - Spatial search integration and API call (`spatial_query`).
-* Spatial Search Widget - Map widget integrated on the search form (`spatial_query_widget`).
+* Spatial Search - Spatial filtering for the dataset search (`spatial_query`).
* WMS Preview - a Web Map Service (WMS) previewer (`wms_preview`).
* CSW Server - a basic CSW server - to server metadata from the CKAN instance (`cswserver`)
* GEMINI Harvesters - for importing INSPIRE-style metadata into CKAN (`gemini_csw_harvester`, `gemini_doc_harvester`, `gemini_waf_harvester`)
@@ -15,6 +14,7 @@ The following plugins are currently available:
These snippets (to be used with CKAN>=2.0):
* Dataset Extent Map - Map widget showing a dataset extent.
+* Spatial Search Widget - Map widget integrated on the search form (`spatial_query_widget`).
These libraries:
* CSW Client - a basic client for accessing a CSW server
@@ -165,16 +165,20 @@ the information stored in the extra with the geometry table.
Spatial Search Widget
+++++++++++++++++++++
-**Note**: this plugin requires CKAN 1.6 or higher.
+The extension provides a snippet to add a map widget to the search form, which allows
+filtering results by an area of interest.
-To enable the search map widget you need to add the `spatial_query_widget` plugin to your
-ini file (See `Configuration`_). You also need to load both the `spatial_metadata`
-and the `spatial_query` plugins.
+To add the map widget to the to the sidebar of the search page, add
+this to the dataset search page template
+(``myproj/ckanext/myproj/templates/package/search.html``)::
-When the plugin is enabled, a map widget will be shown in the dataset search form,
-where users can refine their searchs drawing an area of interest.
+ {% block secondary_content %}
+ {% snippet "spatial/snippets/spatial_query.html" %}
+ {% endblock %}
+
+You need to load the `spatial_metadata` plugin to use this snippet.
Solr configuration issues on legacy PostGIS backend
+++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/ckanext/spatial/public/css/spatial_query.css b/ckanext/spatial/public/css/spatial_query.css
new file mode 100644
index 0000000..2a683ea
--- /dev/null
+++ b/ckanext/spatial/public/css/spatial_query.css
@@ -0,0 +1,82 @@
+.module-narrow #dataset-map-container {
+ height: 200px;
+}
+.module-content #dataset-map-container {
+ height: 250px;
+}
+#dataset-map-attribution {
+ font-size: 11px;
+ line-height: 1.5;
+}
+.module-narrow #dataset-map-attribution {
+ margin: 5px 8px;
+ color: #666;
+}
+.leaflet-draw-label-single {
+ display: none;
+}
+.leaflet-draw-label-subtext {
+ display: none;
+}
+#field-location {
+ width: 190px;
+}
+.select2-results .select2-no-results {
+ padding: 3px 6px;
+}
+#dataset-map-edit {
+ margin: 5px 8px;
+}
+#dataset-map-edit-buttons {
+ display: none;
+}
+.leaflet-control-draw-rectangle {
+ background-image: url("../img/pencil.png");
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ -o-border-radius: 5px;
+ border-radius: 5px;
+}
+.dataset-map-expanded #dataset-map {
+ position: absolute;
+ width: 940px;
+ height: 384px;
+ top: -384px;
+ left: -1px;
+ background-color: white;
+ border: 1px solid #CCC;
+ margin: 0;
+}
+.dataset-map-expanded #dataset-map #dataset-map-container {
+ height: 300px;
+}
+.dataset-map-expanded #dataset-map #dataset-map-attribution {
+ *zoom: 1;
+}
+.dataset-map-expanded #dataset-map #dataset-map-attribution:before,
+.dataset-map-expanded #dataset-map #dataset-map-attribution:after {
+ display: table;
+ content: "";
+ line-height: 0;
+}
+.dataset-map-expanded #dataset-map #dataset-map-attribution:after {
+ clear: both;
+}
+.dataset-map-expanded #dataset-map #dataset-map-attribution div {
+ float: left;
+ margin-right: 10px;
+}
+.dataset-map-expanded #dataset-map #dataset-map-edit-buttons {
+ display: block;
+ float: right;
+ padding: 10px;
+}
+.dataset-map-expanded #dataset-map #dataset-map-edit {
+ display: none;
+}
+.dataset-map-expanded #dataset-map .module-heading {
+ border-top-color: #000;
+}
+.dataset-map-expanded .wrapper {
+ margin-top: 383px;
+}
diff --git a/ckanext/spatial/public/img/pencil.png b/ckanext/spatial/public/img/pencil.png
new file mode 100644
index 0000000..3ef9c8b
Binary files /dev/null and b/ckanext/spatial/public/img/pencil.png differ
diff --git a/ckanext/spatial/public/js/spatial_query.js b/ckanext/spatial/public/js/spatial_query.js
new file mode 100644
index 0000000..3ee661e
--- /dev/null
+++ b/ckanext/spatial/public/js/spatial_query.js
@@ -0,0 +1,198 @@
+/* Module for handling the spatial querying
+ */
+this.ckan.module('spatial-query', function ($, _) {
+
+ return {
+ options: {
+ i18n: {
+ },
+ style: {
+ color: '#F06F64',
+ weight: 2,
+ opacity: 1,
+ fillColor: '#F06F64',
+ fillOpacity: 0.1
+ },
+ default_extent: [[15.62, -139.21], [64.92, -61.87]] //TODO: customize
+ //[[90, 180], [-90, -180]]
+ },
+ template: {
+ buttons: [
+ '
'
+ ].join('')
+ },
+
+ initialize: function () {
+ var module = this;
+ $.proxyAll(this, /_on/);
+ this.el.ready(this._onReady);
+ },
+
+ _getParameterByName: function (name) {
+ var match = RegExp('[?&]' + name + '=([^&]*)')
+ .exec(window.location.search);
+ return match ?
+ decodeURIComponent(match[1].replace(/\+/g, ' '))
+ : null;
+ },
+
+ _drawExtentFromCoords: function(xmin, ymin, xmax, ymax) {
+ if ($.isArray(xmin)) {
+ var coords = xmin;
+ xmin = coords[0]; ymin = coords[1]; xmax = coords[2]; ymax = coords[3];
+ }
+ return new L.Rectangle([[ymin, xmin], [ymax, xmax]],
+ this.options.style);
+ },
+
+ _drawExtentFromGeoJSON: function(geom) {
+ return new L.GeoJSON(geom, {style: this.options.style});
+ },
+
+ _onReady: function() {
+ var module = this;
+ var map;
+ var extentLayer;
+ var previous_box;
+ var previous_extent;
+ var is_exanded = false;
+ var should_zoom = true;
+ var form = $("#dataset-search");
+ var buttons;
+
+ // Add necessary fields to the search form if not already created
+ $(['ext_bbox', 'ext_prev_extent']).each(function(index, item){
+ if ($("#" + item).length === 0) {
+ $('').attr({'id': item, 'name': item}).appendTo(form);
+ }
+ });
+
+ // OK map time
+ map = new L.Map('dataset-map-container', {attributionControl: false});
+
+ // MapQuest OpenStreetMap base map
+ map.addLayer(new L.TileLayer(
+ 'http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png',
+ {maxZoom: 18, subdomains: '1234'}
+ ));
+
+ // Initialize the draw control
+ map.addControl(new L.Control.Draw({
+ position: 'topright',
+ polyline: false, polygon: false,
+ circle: false, marker: false,
+ rectangle: {
+ shapeOptions: module.options.style,
+ title: 'Draw rectangle'
+ }
+ }));
+
+ // OK add the expander
+ $('.leaflet-control-draw a', module.el).on('click', function(e) {
+ if (!is_exanded) {
+ $('body').addClass('dataset-map-expanded');
+ if (should_zoom && !extentLayer) {
+ map.zoomIn();
+ }
+ resetMap();
+ is_exanded = true;
+ }
+ });
+
+ // Setup the expanded buttons
+ buttons = $(module.template.buttons).insertBefore('#dataset-map-attribution');
+
+ // Handle the cancel expanded action
+ $('.cancel', buttons).on('click', function() {
+ $('body').removeClass('dataset-map-expanded');
+ if (extentLayer) {
+ map.removeLayer(extentLayer);
+ }
+ setPreviousExtent();
+ setPreviousBBBox();
+ resetMap();
+ is_exanded = false;
+ });
+
+ // Handle the apply expanded action
+ $('.apply', buttons).on('click', function() {
+ if (extentLayer) {
+ $('body').removeClass('dataset-map-expanded');
+ is_exanded = false;
+ resetMap();
+ // Eugh, hacky hack.
+ setTimeout(function() {
+ map.fitBounds(extentLayer.getBounds());
+ submitForm();
+ }, 200);
+ }
+ });
+
+ // When user finishes drawing the box, record it and add it to the map
+ map.on('draw:rectangle-created', function (e) {
+ if (extentLayer) {
+ map.removeLayer(extentLayer);
+ }
+ extentLayer = e.rect;
+ $('#ext_bbox').val(extentLayer.getBounds().toBBoxString());
+ map.addLayer(extentLayer);
+ $('.apply', buttons).removeClass('disabled').addClass('btn-primary');
+ });
+
+ // Record the current map view so we can replicate it after submitting
+ map.on('moveend', function(e) {
+ $('#ext_prev_extent').val(map.getBounds().toBBoxString());
+ });
+
+ // Ok setup the default state for the map
+ var previous_bbox;
+ setPreviousBBBox();
+ setPreviousExtent();
+
+ // OK, when we expand we shouldn't zoom then
+ map.on('zoomstart', function(e) {
+ should_zoom = false;
+ });
+
+
+ // Is there an existing box from a previous search?
+ function setPreviousBBBox() {
+ previous_bbox = module._getParameterByName('ext_bbox');
+ if (previous_bbox) {
+ $('#ext_bbox').val(previous_bbox);
+ extentLayer = module._drawExtentFromCoords(previous_bbox.split(','))
+ map.addLayer(extentLayer);
+ map.fitBounds(extentLayer.getBounds());
+ }
+ }
+
+ // Is there an existing extent from a previous search?
+ function setPreviousExtent() {
+ previous_extent = module._getParameterByName('ext_prev_extent');
+ if (previous_extent) {
+ coords = previous_extent.split(',');
+ map.fitBounds([[coords[1], coords[0]], [coords[3], coords[2]]]);
+ } else {
+ if (!previous_bbox){
+ map.fitBounds(module.options.default_extent);
+ }
+ }
+ }
+
+ // Reset map view
+ function resetMap() {
+ L.Util.requestAnimFrame(map.invalidateSize, map, !1, map._container);
+ }
+
+ // Add the loading class and submit the form
+ function submitForm() {
+ setTimeout(function() {
+ form.submit();
+ }, 800);
+ }
+ }
+ }
+});
diff --git a/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-circle.png b/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-circle.png
new file mode 100644
index 0000000..60dff10
Binary files /dev/null and b/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-circle.png differ
diff --git a/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-marker-icon.png b/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-marker-icon.png
new file mode 100644
index 0000000..36de02d
Binary files /dev/null and b/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-marker-icon.png differ
diff --git a/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-polygon.png b/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-polygon.png
new file mode 100644
index 0000000..579102d
Binary files /dev/null and b/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-polygon.png differ
diff --git a/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-polyline.png b/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-polyline.png
new file mode 100644
index 0000000..18c9a27
Binary files /dev/null and b/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-polyline.png differ
diff --git a/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-rectangle.png b/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-rectangle.png
new file mode 100644
index 0000000..8f1cbc8
Binary files /dev/null and b/ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-rectangle.png differ
diff --git a/ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw-src.js b/ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw-src.js
new file mode 100644
index 0000000..2b7fdbe
--- /dev/null
+++ b/ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw-src.js
@@ -0,0 +1,955 @@
+/*
+ Copyright (c) 2012, Smartrak, Jacob Toye
+ Leaflet.draw is an open-source JavaScript library for drawing shapes/markers on leaflet powered maps.
+ https://github.com/jacobtoye/Leaflet.draw
+*/
+(function (window, undefined) {
+
+L.drawVersion = '0.1.6';
+
+L.Util.extend(L.LineUtil, {
+ // Checks to see if two line segments intersect. Does not handle degenerate cases.
+ // http://compgeom.cs.uiuc.edu/~jeffe/teaching/373/notes/x06-sweepline.pdf
+ segmentsIntersect: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2, /*Point*/ p3) {
+ return this._checkCounterclockwise(p, p2, p3) !==
+ this._checkCounterclockwise(p1, p2, p3) &&
+ this._checkCounterclockwise(p, p1, p2) !==
+ this._checkCounterclockwise(p, p1, p3);
+ },
+
+ // check to see if points are in counterclockwise order
+ _checkCounterclockwise: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) {
+ return (p2.y - p.y) * (p1.x - p.x) > (p1.y - p.y) * (p2.x - p.x);
+ }
+});
+
+L.Polyline.include({
+ // Check to see if this polyline has any linesegments that intersect.
+ // NOTE: does not support detecting intersection for degenerate cases.
+ intersects: function () {
+ var points = this._originalPoints,
+ len = points ? points.length : 0,
+ i, j, p, p1, p2, p3;
+
+ if (this._tooFewPointsForIntersection()) {
+ return false;
+ }
+
+ for (i = len - 1; i >= 3; i--) {
+ p = points[i - 1];
+ p1 = points[i];
+
+
+ if (this._lineSegmentsIntersectsRange(p, p1, i - 2)) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ // Check for intersection if new latlng was added to this polyline.
+ // NOTE: does not support detecting intersection for degenerate cases.
+ newLatLngIntersects: function (latlng, skipFirst) {
+ // Cannot check a polyline for intersecting lats/lngs when not added to the map
+ if (!this._map) {
+ return false;
+ }
+
+ return this.newPointIntersects(this._map.latLngToLayerPoint(latlng), skipFirst);
+ },
+
+ // Check for intersection if new point was added to this polyline.
+ // newPoint must be a layer point.
+ // NOTE: does not support detecting intersection for degenerate cases.
+ newPointIntersects: function (newPoint, skipFirst) {
+ var points = this._originalPoints,
+ len = points ? points.length : 0,
+ lastPoint = points ? points[len - 1] : null,
+ // The previous previous line segment. Previous line segement doesn't need testing.
+ maxIndex = len - 2;
+
+ if (this._tooFewPointsForIntersection(1)) {
+ return false;
+ }
+
+ return this._lineSegmentsIntersectsRange(lastPoint, newPoint, maxIndex, skipFirst ? 1 : 0);
+ },
+
+ // Polylines with 2 sides can only intersect in cases where points are collinear (we don't support detecting these).
+ // Cannot have intersection when < 3 line segments (< 4 points)
+ _tooFewPointsForIntersection: function (extraPoints) {
+ var points = this._originalPoints,
+ len = points ? points.length : 0;
+ // Increment length by extraPoints if present
+ len += extraPoints || 0;
+
+ return !this._originalPoints || len <= 3;
+ },
+
+ // Checks a line segment intersections with any line segements before its predecessor.
+ // Don't need to check the predecessor as will never intersect.
+ _lineSegmentsIntersectsRange: function (p, p1, maxIndex, minIndex) {
+ var points = this._originalPoints,
+ p2, p3;
+
+ minIndex = minIndex || 0;
+
+ // Check all previous line segments (beside the immediately previous) for intersections
+ for (var j = maxIndex; j > minIndex; j--) {
+ p2 = points[j - 1];
+ p3 = points[j];
+
+ if (L.LineUtil.segmentsIntersect(p, p1, p2, p3)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+});
+
+L.Polygon.include({
+ // Checks a polygon for any intersecting line segments. Ignores holes.
+ intersects: function () {
+ var polylineIntersects,
+ points = this._originalPoints,
+ len, firstPoint, lastPoint, maxIndex;
+
+ if (this._tooFewPointsForIntersection()) {
+ return false;
+ }
+
+ polylineIntersects = L.Polyline.prototype.intersects.call(this);
+
+ // If already found an intersection don't need to check for any more.
+ if (polylineIntersects) {
+ return true;
+ }
+
+ len = points.length;
+ firstPoint = points[0];
+ lastPoint = points[len - 1];
+ maxIndex = len - 2;
+
+ // Check the line segment between last and first point. Don't need to check the first line segment (minIndex = 1)
+ return this._lineSegmentsIntersectsRange(lastPoint, firstPoint, maxIndex, 1);
+ }
+});
+
+L.Handler.Draw = L.Handler.extend({
+ includes: L.Mixin.Events,
+
+ initialize: function (map, options) {
+ this._map = map;
+ this._container = map._container;
+ this._overlayPane = map._panes.overlayPane;
+ this._popupPane = map._panes.popupPane;
+
+ // Merge default shapeOptions options with custom shapeOptions
+ if (options && options.shapeOptions) {
+ options.shapeOptions = L.Util.extend({}, this.options.shapeOptions, options.shapeOptions);
+ }
+ L.Util.extend(this.options, options);
+ },
+
+ enable: function () {
+ this.fire('activated');
+ this._map.fire('drawing', { drawingType: this.type });
+ L.Handler.prototype.enable.call(this);
+ },
+
+ disable: function () {
+ this._map.fire('drawing-disabled', { drawingType: this.type });
+ L.Handler.prototype.disable.call(this);
+ },
+
+ addHooks: function () {
+ if (this._map) {
+ L.DomUtil.disableTextSelection();
+
+ this._label = L.DomUtil.create('div', 'leaflet-draw-label', this._popupPane);
+ this._singleLineLabel = false;
+
+ L.DomEvent.addListener(this._container, 'keyup', this._cancelDrawing, this);
+ }
+ },
+
+ removeHooks: function () {
+ if (this._map) {
+ L.DomUtil.enableTextSelection();
+
+ this._popupPane.removeChild(this._label);
+ delete this._label;
+
+ L.DomEvent.removeListener(this._container, 'keyup', this._cancelDrawing);
+ }
+ },
+
+ _updateLabelText: function (labelText) {
+ labelText.subtext = labelText.subtext || '';
+
+ // update the vertical position (only if changed)
+ if (labelText.subtext.length === 0 && !this._singleLineLabel) {
+ L.DomUtil.addClass(this._label, 'leaflet-draw-label-single');
+ this._singleLineLabel = true;
+ }
+ else if (labelText.subtext.length > 0 && this._singleLineLabel) {
+ L.DomUtil.removeClass(this._label, 'leaflet-draw-label-single');
+ this._singleLineLabel = false;
+ }
+
+ this._label.innerHTML =
+ (labelText.subtext.length > 0 ? '' + labelText.subtext + '' + '
' : '') +
+ '' + labelText.text + '';
+ },
+
+ _updateLabelPosition: function (pos) {
+ L.DomUtil.setPosition(this._label, pos);
+ },
+
+ // Cancel drawing when the escape key is pressed
+ _cancelDrawing: function (e) {
+ if (e.keyCode === 27) {
+ this.disable();
+ }
+ }
+});
+
+L.Polyline.Draw = L.Handler.Draw.extend({
+ Poly: L.Polyline,
+
+ type: 'polyline',
+
+ options: {
+ allowIntersection: true,
+ drawError: {
+ color: '#b00b00',
+ message: 'Error: shape edges cannot cross!',
+ timeout: 2500
+ },
+ icon: new L.DivIcon({
+ iconSize: new L.Point(8, 8),
+ className: 'leaflet-div-icon leaflet-editing-icon'
+ }),
+ guidelineDistance: 20,
+ shapeOptions: {
+ stroke: true,
+ color: '#f06eaa',
+ weight: 4,
+ opacity: 0.5,
+ fill: false,
+ clickable: true
+ },
+ zIndexOffset: 2000 // This should be > than the highest z-index any map layers
+ },
+
+ initialize: function (map, options) {
+ // Merge default drawError options with custom options
+ if (options && options.drawError) {
+ options.drawError = L.Util.extend({}, this.options.drawError, options.drawError);
+ }
+ L.Handler.Draw.prototype.initialize.call(this, map, options);
+ },
+
+ addHooks: function () {
+ L.Handler.Draw.prototype.addHooks.call(this);
+ if (this._map) {
+ this._markers = [];
+
+ this._markerGroup = new L.LayerGroup();
+ this._map.addLayer(this._markerGroup);
+
+ this._poly = new L.Polyline([], this.options.shapeOptions);
+
+ this._updateLabelText(this._getLabelText());
+
+ // Make a transparent marker that will used to catch click events. These click
+ // events will create the vertices. We need to do this so we can ensure that
+ // we can create vertices over other map layers (markers, vector layers). We
+ // also do not want to trigger any click handlers of objects we are clicking on
+ // while drawing.
+ if (!this._mouseMarker) {
+ this._mouseMarker = L.marker(this._map.getCenter(), {
+ icon: L.divIcon({
+ className: 'leaflet-mouse-marker',
+ iconAnchor: [20, 20],
+ iconSize: [40, 40]
+ }),
+ opacity: 0,
+ zIndexOffset: this.options.zIndexOffset
+ });
+ }
+
+ this._mouseMarker
+ .on('click', this._onClick, this)
+ .addTo(this._map);
+
+ this._map.on('mousemove', this._onMouseMove, this);
+ }
+ },
+
+ removeHooks: function () {
+ L.Handler.Draw.prototype.removeHooks.call(this);
+
+ this._clearHideErrorTimeout();
+
+ this._cleanUpShape();
+
+ // remove markers from map
+ this._map.removeLayer(this._markerGroup);
+ delete this._markerGroup;
+ delete this._markers;
+
+ this._map.removeLayer(this._poly);
+ delete this._poly;
+
+ this._mouseMarker.off('click', this._onClick);
+ this._map.removeLayer(this._mouseMarker);
+ delete this._mouseMarker;
+
+ // clean up DOM
+ this._clearGuides();
+
+ this._map.off('mousemove', this._onMouseMove);
+ },
+
+ _finishShape: function () {
+ if (!this.options.allowIntersection && this._poly.newLatLngIntersects(this._poly.getLatLngs()[0], true)) {
+ this._showErrorLabel();
+ return;
+ }
+ if (!this._shapeIsValid()) {
+ this._showErrorLabel();
+ return;
+ }
+
+ this._map.fire(
+ 'draw:poly-created',
+ { poly: new this.Poly(this._poly.getLatLngs(), this.options.shapeOptions) }
+ );
+ this.disable();
+ },
+
+ //Called to verify the shape is valid when the user tries to finish it
+ //Return false if the shape is not valid
+ _shapeIsValid: function () {
+ return true;
+ },
+
+ _onMouseMove: function (e) {
+ var newPos = e.layerPoint,
+ latlng = e.latlng,
+ markerCount = this._markers.length;
+
+ // Save latlng
+ this._currentLatLng = latlng;
+
+ // update the label
+ this._updateLabelPosition(newPos);
+
+ if (markerCount > 0) {
+ this._updateLabelText(this._getLabelText());
+ // draw the guide line
+ this._clearGuides();
+ this._drawGuide(
+ this._map.latLngToLayerPoint(this._markers[markerCount - 1].getLatLng()),
+ newPos
+ );
+ }
+
+ // Update the mouse marker position
+ this._mouseMarker.setLatLng(latlng);
+
+ L.DomEvent.preventDefault(e.originalEvent);
+ },
+
+ _onClick: function (e) {
+ var latlng = e.target.getLatLng(),
+ markerCount = this._markers.length;
+
+ if (markerCount > 0 && !this.options.allowIntersection && this._poly.newLatLngIntersects(latlng)) {
+ this._showErrorLabel();
+ return;
+ }
+ else if (this._errorShown) {
+ this._hideErrorLabel();
+ }
+
+ this._markers.push(this._createMarker(latlng));
+
+ this._poly.addLatLng(latlng);
+
+ if (this._poly.getLatLngs().length === 2) {
+ this._map.addLayer(this._poly);
+ }
+
+ this._updateMarkerHandler();
+
+ this._vertexAdded(latlng);
+ },
+
+ _updateMarkerHandler: function () {
+ // The last marker shold have a click handler to close the polyline
+ if (this._markers.length > 1) {
+ this._markers[this._markers.length - 1].on('click', this._finishShape, this);
+ }
+
+ // Remove the old marker click handler (as only the last point should close the polyline)
+ if (this._markers.length > 2) {
+ this._markers[this._markers.length - 2].off('click', this._finishShape);
+ }
+ },
+
+ _createMarker: function (latlng) {
+ var marker = new L.Marker(latlng, {
+ icon: this.options.icon,
+ zIndexOffset: this.options.zIndexOffset * 2
+ });
+
+ this._markerGroup.addLayer(marker);
+
+ return marker;
+ },
+
+ _drawGuide: function (pointA, pointB) {
+ var length = Math.floor(Math.sqrt(Math.pow((pointB.x - pointA.x), 2) + Math.pow((pointB.y - pointA.y), 2))),
+ i,
+ fraction,
+ dashPoint,
+ dash;
+
+ //create the guides container if we haven't yet (TODO: probaly shouldn't do this every time the user starts to draw?)
+ if (!this._guidesContainer) {
+ this._guidesContainer = L.DomUtil.create('div', 'leaflet-draw-guides', this._overlayPane);
+ }
+
+ //draw a dash every GuildeLineDistance
+ for (i = this.options.guidelineDistance; i < length; i += this.options.guidelineDistance) {
+ //work out fraction along line we are
+ fraction = i / length;
+
+ //calculate new x,y point
+ dashPoint = {
+ x: Math.floor((pointA.x * (1 - fraction)) + (fraction * pointB.x)),
+ y: Math.floor((pointA.y * (1 - fraction)) + (fraction * pointB.y))
+ };
+
+ //add guide dash to guide container
+ dash = L.DomUtil.create('div', 'leaflet-draw-guide-dash', this._guidesContainer);
+ dash.style.backgroundColor =
+ !this._errorShown ? this.options.shapeOptions.color : this.options.drawError.color;
+
+ L.DomUtil.setPosition(dash, dashPoint);
+ }
+ },
+
+ _updateGuideColor: function (color) {
+ if (this._guidesContainer) {
+ for (var i = 0, l = this._guidesContainer.childNodes.length; i < l; i++) {
+ this._guidesContainer.childNodes[i].style.backgroundColor = color;
+ }
+ }
+ },
+
+ // removes all child elements (guide dashes) from the guides container
+ _clearGuides: function () {
+ if (this._guidesContainer) {
+ while (this._guidesContainer.firstChild) {
+ this._guidesContainer.removeChild(this._guidesContainer.firstChild);
+ }
+ }
+ },
+
+ _updateLabelText: function (labelText) {
+ if (!this._errorShown) {
+ L.Handler.Draw.prototype._updateLabelText.call(this, labelText);
+ }
+ },
+
+ _getLabelText: function () {
+ var labelText,
+ distance,
+ distanceStr;
+
+ if (this._markers.length === 0) {
+ labelText = {
+ text: 'Click to start drawing line.'
+ };
+ } else {
+ // calculate the distance from the last fixed point to the mouse position
+ distance = this._measurementRunningTotal + this._currentLatLng.distanceTo(this._markers[this._markers.length - 1].getLatLng());
+ // show metres when distance is < 1km, then show km
+ distanceStr = distance > 1000 ? (distance / 1000).toFixed(2) + ' km' : Math.ceil(distance) + ' m';
+
+ if (this._markers.length === 1) {
+ labelText = {
+ text: 'Click to continue drawing line.',
+ subtext: distanceStr
+ };
+ } else {
+ labelText = {
+ text: 'Click last point to finish line.',
+ subtext: distanceStr
+ };
+ }
+ }
+ return labelText;
+ },
+
+ _showErrorLabel: function () {
+ this._errorShown = true;
+
+ // Update label
+ L.DomUtil.addClass(this._label, 'leaflet-error-draw-label');
+ L.DomUtil.addClass(this._label, 'leaflet-flash-anim');
+ L.Handler.Draw.prototype._updateLabelText.call(this, { text: this.options.drawError.message });
+
+ // Update shape
+ this._updateGuideColor(this.options.drawError.color);
+ this._poly.setStyle({ color: this.options.drawError.color });
+
+ // Hide the error after 2 seconds
+ this._clearHideErrorTimeout();
+ this._hideErrorTimeout = setTimeout(L.Util.bind(this._hideErrorLabel, this), this.options.drawError.timeout);
+ },
+
+ _hideErrorLabel: function () {
+ this._errorShown = false;
+
+ this._clearHideErrorTimeout();
+
+ // Revert label
+ L.DomUtil.removeClass(this._label, 'leaflet-error-draw-label');
+ L.DomUtil.removeClass(this._label, 'leaflet-flash-anim');
+ this._updateLabelText(this._getLabelText());
+
+ // Revert shape
+ this._updateGuideColor(this.options.shapeOptions.color);
+ this._poly.setStyle({ color: this.options.shapeOptions.color });
+ },
+
+ _clearHideErrorTimeout: function () {
+ if (this._hideErrorTimeout) {
+ clearTimeout(this._hideErrorTimeout);
+ this._hideErrorTimeout = null;
+ }
+ },
+
+ _vertexAdded: function (latlng) {
+ if (this._markers.length === 1) {
+ this._measurementRunningTotal = 0;
+ }
+ else {
+ this._measurementRunningTotal +=
+ latlng.distanceTo(this._markers[this._markers.length - 2].getLatLng());
+ }
+ },
+
+ _cleanUpShape: function () {
+ if (this._markers.length > 0) {
+ this._markers[this._markers.length - 1].off('click', this._finishShape);
+ }
+ }
+});
+
+L.Polygon.Draw = L.Polyline.Draw.extend({
+ Poly: L.Polygon,
+
+ type: 'polygon',
+
+ options: {
+ shapeOptions: {
+ stroke: true,
+ color: '#f06eaa',
+ weight: 4,
+ opacity: 0.5,
+ fill: true,
+ fillColor: null, //same as color by default
+ fillOpacity: 0.2,
+ clickable: false
+ }
+ },
+
+ _updateMarkerHandler: function () {
+ // The first marker shold have a click handler to close the polygon
+ if (this._markers.length === 1) {
+ this._markers[0].on('click', this._finishShape, this);
+ }
+ },
+
+ _getLabelText: function () {
+ var text;
+ if (this._markers.length === 0) {
+ text = 'Click to start drawing shape.';
+ } else if (this._markers.length < 3) {
+ text = 'Click to continue drawing shape.';
+ } else {
+ text = 'Click first point to close this shape.';
+ }
+ return {
+ text: text
+ };
+ },
+
+ _shapeIsValid: function () {
+ return this._markers.length >= 3;
+ },
+
+ _vertexAdded: function (latlng) {
+ //calc area here
+ },
+
+ _cleanUpShape: function () {
+ if (this._markers.length > 0) {
+ this._markers[0].off('click', this._finishShape);
+ }
+ }
+});
+
+L.SimpleShape = {};
+
+L.SimpleShape.Draw = L.Handler.Draw.extend({
+ addHooks: function () {
+ L.Handler.Draw.prototype.addHooks.call(this);
+ if (this._map) {
+ this._map.dragging.disable();
+ //TODO refactor: move cursor to styles
+ this._container.style.cursor = 'crosshair';
+
+ this._updateLabelText({ text: this._initialLabelText });
+
+ this._map
+ .on('mousedown', this._onMouseDown, this)
+ .on('mousemove', this._onMouseMove, this);
+
+ }
+ },
+
+ removeHooks: function () {
+ L.Handler.Draw.prototype.removeHooks.call(this);
+ if (this._map) {
+ this._map.dragging.enable();
+ //TODO refactor: move cursor to styles
+ this._container.style.cursor = '';
+
+ this._map
+ .off('mousedown', this._onMouseDown, this)
+ .off('mousemove', this._onMouseMove, this);
+
+ L.DomEvent.off(document, 'mouseup', this._onMouseUp);
+
+ // If the box element doesn't exist they must not have moved the mouse, so don't need to destroy/return
+ if (this._shape) {
+ this._map.removeLayer(this._shape);
+ delete this._shape;
+ }
+ }
+ this._isDrawing = false;
+ },
+
+ _onMouseDown: function (e) {
+ this._isDrawing = true;
+ this._startLatLng = e.latlng;
+
+ L.DomEvent
+ .on(document, 'mouseup', this._onMouseUp, this)
+ .preventDefault(e.originalEvent);
+ },
+
+ _onMouseMove: function (e) {
+ var layerPoint = e.layerPoint,
+ latlng = e.latlng;
+
+ this._updateLabelPosition(layerPoint);
+ if (this._isDrawing) {
+ this._updateLabelText({ text: 'Release mouse to finish drawing.' });
+ this._drawShape(latlng);
+ }
+ },
+
+ _onMouseUp: function (e) {
+ if (this._shape) {
+ this._fireCreatedEvent();
+ }
+
+ this.disable();
+ }
+});
+
+L.Circle.Draw = L.SimpleShape.Draw.extend({
+ type: 'circle',
+
+ options: {
+ shapeOptions: {
+ stroke: true,
+ color: '#f06eaa',
+ weight: 4,
+ opacity: 0.5,
+ fill: true,
+ fillColor: null, //same as color by default
+ fillOpacity: 0.2,
+ clickable: true
+ }
+ },
+
+ _initialLabelText: 'Click and drag to draw circle.',
+
+ _drawShape: function (latlng) {
+ if (!this._shape) {
+ this._shape = new L.Circle(this._startLatLng, this._startLatLng.distanceTo(latlng), this.options.shapeOptions);
+ this._map.addLayer(this._shape);
+ } else {
+ this._shape.setRadius(this._startLatLng.distanceTo(latlng));
+ }
+ },
+
+ _fireCreatedEvent: function () {
+ this._map.fire(
+ 'draw:circle-created',
+ { circ: new L.Circle(this._startLatLng, this._shape.getRadius(), this.options.shapeOptions) }
+ );
+ }
+});
+
+L.Rectangle.Draw = L.SimpleShape.Draw.extend({
+ type: 'rectangle',
+
+ options: {
+ shapeOptions: {
+ stroke: true,
+ color: '#f06eaa',
+ weight: 4,
+ opacity: 0.5,
+ fill: true,
+ fillColor: null, //same as color by default
+ fillOpacity: 0.2,
+ clickable: true
+ }
+ },
+
+ _initialLabelText: 'Click and drag to draw rectangle.',
+
+ _drawShape: function (latlng) {
+ if (!this._shape) {
+ this._shape = new L.Rectangle(new L.LatLngBounds(this._startLatLng, latlng), this.options.shapeOptions);
+ this._map.addLayer(this._shape);
+ } else {
+ this._shape.setBounds(new L.LatLngBounds(this._startLatLng, latlng));
+ }
+ },
+
+ _fireCreatedEvent: function () {
+ this._map.fire(
+ 'draw:rectangle-created',
+ { rect: new L.Rectangle(this._shape.getBounds(), this.options.shapeOptions) }
+ );
+ }
+});
+
+L.Marker.Draw = L.Handler.Draw.extend({
+ type: 'marker',
+
+ options: {
+ icon: new L.Icon.Default(),
+ zIndexOffset: 2000 // This should be > than the highest z-index any markers
+ },
+
+ addHooks: function () {
+ L.Handler.Draw.prototype.addHooks.call(this);
+
+ if (this._map) {
+ this._updateLabelText({ text: 'Click map to place marker.' });
+ this._map.on('mousemove', this._onMouseMove, this);
+ }
+ },
+
+ removeHooks: function () {
+ L.Handler.Draw.prototype.removeHooks.call(this);
+
+ if (this._map) {
+ if (this._marker) {
+ this._marker.off('click', this._onClick);
+ this._map
+ .off('click', this._onClick)
+ .removeLayer(this._marker);
+ delete this._marker;
+ }
+
+ this._map.off('mousemove', this._onMouseMove);
+ }
+ },
+
+ _onMouseMove: function (e) {
+ var newPos = e.layerPoint,
+ latlng = e.latlng;
+
+ this._updateLabelPosition(newPos);
+
+ if (!this._marker) {
+ this._marker = new L.Marker(latlng, {
+ icon: this.options.icon,
+ zIndexOffset: this.options.zIndexOffset
+ });
+ // Bind to both marker and map to make sure we get the click event.
+ this._marker.on('click', this._onClick, this);
+ this._map
+ .on('click', this._onClick, this)
+ .addLayer(this._marker);
+ }
+ else {
+ this._marker.setLatLng(latlng);
+ }
+ },
+
+ _onClick: function (e) {
+ this._map.fire(
+ 'draw:marker-created',
+ { marker: new L.Marker(this._marker.getLatLng(), { icon: this.options.icon }) }
+ );
+ this.disable();
+ }
+});
+
+L.Map.mergeOptions({
+ drawControl: false
+});
+
+L.Control.Draw = L.Control.extend({
+
+ options: {
+ position: 'topleft',
+ polyline: {
+ title: 'Draw a polyline'
+ },
+ polygon: {
+ title: 'Draw a polygon'
+ },
+ rectangle: {
+ title: 'Draw a rectangle'
+ },
+ circle: {
+ title: 'Draw a circle'
+ },
+ marker: {
+ title: 'Add a marker'
+ }
+ },
+
+ initialize: function (options) {
+ L.Util.extend(this.options, options);
+ },
+
+ onAdd: function (map) {
+ var drawName = 'leaflet-control-draw', //TODO
+ barName = 'leaflet-bar',
+ partName = barName + '-part',
+ container = L.DomUtil.create('div', drawName + ' ' + barName),
+ buttons = [];
+
+ this.handlers = {};
+
+ if (this.options.polyline) {
+ this.handlers.polyline = new L.Polyline.Draw(map, this.options.polyline);
+ buttons.push(this._createButton(
+ this.options.polyline.title,
+ drawName + '-polyline ' + partName,
+ container,
+ this.handlers.polyline.enable,
+ this.handlers.polyline
+ ));
+ this.handlers.polyline.on('activated', this._disableInactiveModes, this);
+ }
+
+ if (this.options.polygon) {
+ this.handlers.polygon = new L.Polygon.Draw(map, this.options.polygon);
+ buttons.push(this._createButton(
+ this.options.polygon.title,
+ drawName + '-polygon ' + partName,
+ container,
+ this.handlers.polygon.enable,
+ this.handlers.polygon
+ ));
+ this.handlers.polygon.on('activated', this._disableInactiveModes, this);
+ }
+
+ if (this.options.rectangle) {
+ this.handlers.rectangle = new L.Rectangle.Draw(map, this.options.rectangle);
+ buttons.push(this._createButton(
+ this.options.rectangle.title,
+ drawName + '-rectangle ' + partName,
+ container,
+ this.handlers.rectangle.enable,
+ this.handlers.rectangle
+ ));
+ this.handlers.rectangle.on('activated', this._disableInactiveModes, this);
+ }
+
+ if (this.options.circle) {
+ this.handlers.circle = new L.Circle.Draw(map, this.options.circle);
+ buttons.push(this._createButton(
+ this.options.circle.title,
+ drawName + '-circle ' + partName,
+ container,
+ this.handlers.circle.enable,
+ this.handlers.circle
+ ));
+ this.handlers.circle.on('activated', this._disableInactiveModes, this);
+ }
+
+ if (this.options.marker) {
+ this.handlers.marker = new L.Marker.Draw(map, this.options.marker);
+ buttons.push(this._createButton(
+ this.options.marker.title,
+ drawName + '-marker ' + partName,
+ container,
+ this.handlers.marker.enable,
+ this.handlers.marker
+ ));
+ this.handlers.marker.on('activated', this._disableInactiveModes, this);
+ }
+
+ // Add in the top and bottom classes so we get the border radius
+ L.DomUtil.addClass(buttons[0], partName + '-top');
+ L.DomUtil.addClass(buttons[buttons.length - 1], partName + '-bottom');
+
+ return container;
+ },
+
+ _createButton: function (title, className, container, fn, context) {
+ var link = L.DomUtil.create('a', className, container);
+ link.href = '#';
+ link.title = title;
+
+ L.DomEvent
+ .on(link, 'click', L.DomEvent.stopPropagation)
+ .on(link, 'mousedown', L.DomEvent.stopPropagation)
+ .on(link, 'dblclick', L.DomEvent.stopPropagation)
+ .on(link, 'click', L.DomEvent.preventDefault)
+ .on(link, 'click', fn, context);
+
+ return link;
+ },
+
+ // Need to disable the drawing modes if user clicks on another without disabling the current mode
+ _disableInactiveModes: function () {
+ for (var i in this.handlers) {
+ // Check if is a property of this object and is enabled
+ if (this.handlers.hasOwnProperty(i) && this.handlers[i].enabled()) {
+ this.handlers[i].disable();
+ }
+ }
+ }
+});
+
+L.Map.addInitHook(function () {
+ if (this.options.drawControl) {
+ this.drawControl = new L.Control.Draw();
+ this.addControl(this.drawControl);
+ }
+});
+
+
+
+
+}(this));
\ No newline at end of file
diff --git a/ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw.css b/ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw.css
new file mode 100644
index 0000000..c5ad833
--- /dev/null
+++ b/ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw.css
@@ -0,0 +1,118 @@
+/* Leaflet controls */
+
+.leaflet-container .leaflet-control-draw {
+ margin-left: 13px;
+ margin-top: 12px;
+}
+
+.leaflet-control-draw a {
+ background-position: 50% 50%;
+ background-repeat: no-repeat;
+ display: block;
+ width: 22px;
+ height: 22px;
+}
+
+.leaflet-control-draw a:hover {
+ background-color: #fff;
+}
+
+.leaflet-touch .leaflet-control-draw a {
+ width: 27px;
+ height: 27px;
+}
+
+.leaflet-control-draw-polyline {
+ background-image: url(images/draw-polyline.png);
+}
+
+.leaflet-control-draw-polygon {
+ background-image: url(images/draw-polygon.png);
+}
+
+.leaflet-control-draw-rectangle {
+ background-image: url(images/draw-rectangle.png);
+}
+
+.leaflet-control-draw-circle {
+ background-image: url(images/draw-circle.png);
+}
+
+.leaflet-control-draw-marker {
+ background-image: url(images/draw-marker-icon.png);
+}
+
+.leaflet-mouse-marker {
+ background-color: #fff;
+ cursor: crosshair;
+}
+
+.leaflet-draw-label {
+ background-color: #fff;
+ border: 1px solid #ccc;
+ color: #222;
+ font: 12px/18px "Helvetica Neue", Arial, Helvetica, sans-serif;
+ margin-left: 20px;
+ margin-top: -21px;
+ padding: 2px 4px;
+ position: absolute;
+ white-space: nowrap;
+ z-index: 6;
+}
+
+.leaflet-error-draw-label {
+ background-color: #F2DEDE;
+ border-color: #E6B6BD;
+ color: #B94A48;
+}
+
+.leaflet-draw-label-single {
+ margin-top: -12px
+}
+
+.leaflet-draw-label-subtext {
+ color: #999;
+}
+
+.leaflet-draw-guide-dash {
+ font-size: 1%;
+ opacity: 0.6;
+ position: absolute;
+ width: 5px;
+ height: 5px;
+}
+
+.leaflet-flash-anim {
+ -webkit-animation-duration: 0.66s;
+ -moz-animation-duration: 0.66s;
+ -o-animation-duration: 0.66s;
+ animation-duration: 0.66s;
+ -webkit-animation-fill-mode: both;
+ -moz-animation-fill-mode: both;
+ -o-animation-fill-mode: both;
+ animation-fill-mode: both;
+ -webkit-animation-name: leaflet-flash;
+ -moz-animation-name: leaflet-flash;
+ -o-animation-name: leaflet-flash;
+ animation-name: leaflet-flash;
+}
+
+@-webkit-keyframes leaflet-flash {
+ 0%, 50%, 100% { opacity: 1; }
+ 25%, 75% { opacity: 0.3; }
+}
+
+@-moz-keyframes leaflet-flash {
+ 0%, 50%, 100% { opacity: 1; }
+ 25%, 75% { opacity: 0.3; }
+}
+
+@-o-keyframes leaflet-flash {
+ 0%, 50%, 100% { opacity: 1; }
+ 25%, 75% { opacity: 0.3; }
+}
+
+@keyframes leaflet-flash {
+ 0%, 50%, 100% { opacity: 1; }
+ 25%, 75% { opacity: 0; }
+}
\ No newline at end of file
diff --git a/ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw.ie.css b/ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw.ie.css
new file mode 100644
index 0000000..a291d25
--- /dev/null
+++ b/ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw.ie.css
@@ -0,0 +1,12 @@
+/* Conditional stylesheet for IE. */
+.leaflet-control-draw {
+ border: 3px solid #999;
+}
+
+.leaflet-control-draw a {
+ background-color: #eee;
+}
+
+.leaflet-control-draw a:hover {
+ background-color: #fff;
+}
\ No newline at end of file
diff --git a/ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw.js b/ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw.js
new file mode 100644
index 0000000..49bb67b
--- /dev/null
+++ b/ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw.js
@@ -0,0 +1,6 @@
+/*
+ Copyright (c) 2012, Smartrak, Jacob Toye
+ Leaflet.draw is an open-source JavaScript library for drawing shapes/markers on leaflet powered maps.
+ https://github.com/jacobtoye/Leaflet.draw
+*/
+(function(e,t){L.drawVersion="0.1.6",L.Util.extend(L.LineUtil,{segmentsIntersect:function(e,t,n,r){return this._checkCounterclockwise(e,n,r)!==this._checkCounterclockwise(t,n,r)&&this._checkCounterclockwise(e,t,n)!==this._checkCounterclockwise(e,t,r)},_checkCounterclockwise:function(e,t,n){return(n.y-e.y)*(t.x-e.x)>(t.y-e.y)*(n.x-e.x)}}),L.Polyline.include({intersects:function(){var e=this._originalPoints,t=e?e.length:0,n,r,i,s,o,u;if(this._tooFewPointsForIntersection())return!1;for(n=t-1;n>=3;n--){i=e[n-1],s=e[n];if(this._lineSegmentsIntersectsRange(i,s,n-2))return!0}return!1},newLatLngIntersects:function(e,t){return this._map?this.newPointIntersects(this._map.latLngToLayerPoint(e),t):!1},newPointIntersects:function(e,t){var n=this._originalPoints,r=n?n.length:0,i=n?n[r-1]:null,s=r-2;return this._tooFewPointsForIntersection(1)?!1:this._lineSegmentsIntersectsRange(i,e,s,t?1:0)},_tooFewPointsForIntersection:function(e){var t=this._originalPoints,n=t?t.length:0;return n+=e||0,!this._originalPoints||n<=3},_lineSegmentsIntersectsRange:function(e,t,n,r){var i=this._originalPoints,s,o;r=r||0;for(var u=n;u>r;u--){s=i[u-1],o=i[u];if(L.LineUtil.segmentsIntersect(e,t,s,o))return!0}return!1}}),L.Polygon.include({intersects:function(){var e,t=this._originalPoints,n,r,i,s;return this._tooFewPointsForIntersection()?!1:(e=L.Polyline.prototype.intersects.call(this),e?!0:(n=t.length,r=t[0],i=t[n-1],s=n-2,this._lineSegmentsIntersectsRange(i,r,s,1)))}}),L.Handler.Draw=L.Handler.extend({includes:L.Mixin.Events,initialize:function(e,t){this._map=e,this._container=e._container,this._overlayPane=e._panes.overlayPane,this._popupPane=e._panes.popupPane,t&&t.shapeOptions&&(t.shapeOptions=L.Util.extend({},this.options.shapeOptions,t.shapeOptions)),L.Util.extend(this.options,t)},enable:function(){this.fire("activated"),this._map.fire("drawing",{drawingType:this.type}),L.Handler.prototype.enable.call(this)},disable:function(){this._map.fire("drawing-disabled",{drawingType:this.type}),L.Handler.prototype.disable.call(this)},addHooks:function(){this._map&&(L.DomUtil.disableTextSelection(),this._label=L.DomUtil.create("div","leaflet-draw-label",this._popupPane),this._singleLineLabel=!1,L.DomEvent.addListener(this._container,"keyup",this._cancelDrawing,this))},removeHooks:function(){this._map&&(L.DomUtil.enableTextSelection(),this._popupPane.removeChild(this._label),delete this._label,L.DomEvent.removeListener(this._container,"keyup",this._cancelDrawing))},_updateLabelText:function(e){e.subtext=e.subtext||"",e.subtext.length===0&&!this._singleLineLabel?(L.DomUtil.addClass(this._label,"leaflet-draw-label-single"),this._singleLineLabel=!0):e.subtext.length>0&&this._singleLineLabel&&(L.DomUtil.removeClass(this._label,"leaflet-draw-label-single"),this._singleLineLabel=!1),this._label.innerHTML=(e.subtext.length>0?''+e.subtext+""+"
":"")+""+e.text+""},_updateLabelPosition:function(e){L.DomUtil.setPosition(this._label,e)},_cancelDrawing:function(e){e.keyCode===27&&this.disable()}}),L.Polyline.Draw=L.Handler.Draw.extend({Poly:L.Polyline,type:"polyline",options:{allowIntersection:!0,drawError:{color:"#b00b00",message:"Error: shape edges cannot cross!",timeout:2500},icon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon"}),guidelineDistance:20,shapeOptions:{stroke:!0,color:"#f06eaa",weight:4,opacity:.5,fill:!1,clickable:!0},zIndexOffset:2e3},initialize:function(e,t){t&&t.drawError&&(t.drawError=L.Util.extend({},this.options.drawError,t.drawError)),L.Handler.Draw.prototype.initialize.call(this,e,t)},addHooks:function(){L.Handler.Draw.prototype.addHooks.call(this),this._map&&(this._markers=[],this._markerGroup=new L.LayerGroup,this._map.addLayer(this._markerGroup),this._poly=new L.Polyline([],this.options.shapeOptions),this._updateLabelText(this._getLabelText()),this._mouseMarker||(this._mouseMarker=L.marker(this._map.getCenter(),{icon:L.divIcon({className:"leaflet-mouse-marker",iconAnchor:[20,20],iconSize:[40,40]}),opacity:0,zIndexOffset:this.options.zIndexOffset})),this._mouseMarker.on("click",this._onClick,this).addTo(this._map),this._map.on("mousemove",this._onMouseMove,this))},removeHooks:function(){L.Handler.Draw.prototype.removeHooks.call(this),this._clearHideErrorTimeout(),this._cleanUpShape(),this._map.removeLayer(this._markerGroup),delete this._markerGroup,delete this._markers,this._map.removeLayer(this._poly),delete this._poly,this._mouseMarker.off("click",this._onClick),this._map.removeLayer(this._mouseMarker),delete this._mouseMarker,this._clearGuides(),this._map.off("mousemove",this._onMouseMove)},_finishShape:function(){if(!this.options.allowIntersection&&this._poly.newLatLngIntersects(this._poly.getLatLngs()[0],!0)){this._showErrorLabel();return}if(!this._shapeIsValid()){this._showErrorLabel();return}this._map.fire("draw:poly-created",{poly:new this.Poly(this._poly.getLatLngs(),this.options.shapeOptions)}),this.disable()},_shapeIsValid:function(){return!0},_onMouseMove:function(e){var t=e.layerPoint,n=e.latlng,r=this._markers.length;this._currentLatLng=n,this._updateLabelPosition(t),r>0&&(this._updateLabelText(this._getLabelText()),this._clearGuides(),this._drawGuide(this._map.latLngToLayerPoint(this._markers[r-1].getLatLng()),t)),this._mouseMarker.setLatLng(n),L.DomEvent.preventDefault(e.originalEvent)},_onClick:function(e){var t=e.target.getLatLng(),n=this._markers.length;if(n>0&&!this.options.allowIntersection&&this._poly.newLatLngIntersects(t)){this._showErrorLabel();return}this._errorShown&&this._hideErrorLabel(),this._markers.push(this._createMarker(t)),this._poly.addLatLng(t),this._poly.getLatLngs().length===2&&this._map.addLayer(this._poly),this._updateMarkerHandler(),this._vertexAdded(t)},_updateMarkerHandler:function(){this._markers.length>1&&this._markers[this._markers.length-1].on("click",this._finishShape,this),this._markers.length>2&&this._markers[this._markers.length-2].off("click",this._finishShape)},_createMarker:function(e){var t=new L.Marker(e,{icon:this.options.icon,zIndexOffset:this.options.zIndexOffset*2});return this._markerGroup.addLayer(t),t},_drawGuide:function(e,t){var n=Math.floor(Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2))),r,i,s,o;this._guidesContainer||(this._guidesContainer=L.DomUtil.create("div","leaflet-draw-guides",this._overlayPane));for(r=this.options.guidelineDistance;r1e3?(t/1e3).toFixed(2)+" km":Math.ceil(t)+" m",this._markers.length===1?e={text:"Click to continue drawing line.",subtext:n}:e={text:"Click last point to finish line.",subtext:n}),e},_showErrorLabel:function(){this._errorShown=!0,L.DomUtil.addClass(this._label,"leaflet-error-draw-label"),L.DomUtil.addClass(this._label,"leaflet-flash-anim"),L.Handler.Draw.prototype._updateLabelText.call(this,{text:this.options.drawError.message}),this._updateGuideColor(this.options.drawError.color),this._poly.setStyle({color:this.options.drawError.color}),this._clearHideErrorTimeout(),this._hideErrorTimeout=setTimeout(L.Util.bind(this._hideErrorLabel,this),this.options.drawError.timeout)},_hideErrorLabel:function(){this._errorShown=!1,this._clearHideErrorTimeout(),L.DomUtil.removeClass(this._label,"leaflet-error-draw-label"),L.DomUtil.removeClass(this._label,"leaflet-flash-anim"),this._updateLabelText(this._getLabelText()),this._updateGuideColor(this.options.shapeOptions.color),this._poly.setStyle({color:this.options.shapeOptions.color})},_clearHideErrorTimeout:function(){this._hideErrorTimeout&&(clearTimeout(this._hideErrorTimeout),this._hideErrorTimeout=null)},_vertexAdded:function(e){this._markers.length===1?this._measurementRunningTotal=0:this._measurementRunningTotal+=e.distanceTo(this._markers[this._markers.length-2].getLatLng())},_cleanUpShape:function(){this._markers.length>0&&this._markers[this._markers.length-1].off("click",this._finishShape)}}),L.Polygon.Draw=L.Polyline.Draw.extend({Poly:L.Polygon,type:"polygon",options:{shapeOptions:{stroke:!0,color:"#f06eaa",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!1}},_updateMarkerHandler:function(){this._markers.length===1&&this._markers[0].on("click",this._finishShape,this)},_getLabelText:function(){var e;return this._markers.length===0?e="Click to start drawing shape.":this._markers.length<3?e="Click to continue drawing shape.":e="Click first point to close this shape.",{text:e}},_shapeIsValid:function(){return this._markers.length>=3},_vertexAdded:function(e){},_cleanUpShape:function(){this._markers.length>0&&this._markers[0].off("click",this._finishShape)}}),L.SimpleShape={},L.SimpleShape.Draw=L.Handler.Draw.extend({addHooks:function(){L.Handler.Draw.prototype.addHooks.call(this),this._map&&(this._map.dragging.disable(),this._container.style.cursor="crosshair",this._updateLabelText({text:this._initialLabelText}),this._map.on("mousedown",this._onMouseDown,this).on("mousemove",this._onMouseMove,this))},removeHooks:function(){L.Handler.Draw.prototype.removeHooks.call(this),this._map&&(this._map.dragging.enable(),this._container.style.cursor="",this._map.off("mousedown",this._onMouseDown,this).off("mousemove",this._onMouseMove,this),L.DomEvent.off(document,"mouseup",this._onMouseUp),this._shape&&(this._map.removeLayer(this._shape),delete this._shape)),this._isDrawing=!1},_onMouseDown:function(e){this._isDrawing=!0,this._startLatLng=e.latlng,L.DomEvent.on(document,"mouseup",this._onMouseUp,this).preventDefault(e.originalEvent)},_onMouseMove:function(e){var t=e.layerPoint,n=e.latlng;this._updateLabelPosition(t),this._isDrawing&&(this._updateLabelText({text:"Release mouse to finish drawing."}),this._drawShape(n))},_onMouseUp:function(e){this._shape&&this._fireCreatedEvent(),this.disable()}}),L.Circle.Draw=L.SimpleShape.Draw.extend({type:"circle",options:{shapeOptions:{stroke:!0,color:"#f06eaa",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0}},_initialLabelText:"Click and drag to draw circle.",_drawShape:function(e){this._shape?this._shape.setRadius(this._startLatLng.distanceTo(e)):(this._shape=new L.Circle(this._startLatLng,this._startLatLng.distanceTo(e),this.options.shapeOptions),this._map.addLayer(this._shape))},_fireCreatedEvent:function(){this._map.fire("draw:circle-created",{circ:new L.Circle(this._startLatLng,this._shape.getRadius(),this.options.shapeOptions)})}}),L.Rectangle.Draw=L.SimpleShape.Draw.extend({type:"rectangle",options:{shapeOptions:{stroke:!0,color:"#f06eaa",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0}},_initialLabelText:"Click and drag to draw rectangle.",_drawShape:function(e){this._shape?this._shape.setBounds(new L.LatLngBounds(this._startLatLng,e)):(this._shape=new L.Rectangle(new L.LatLngBounds(this._startLatLng,e),this.options.shapeOptions),this._map.addLayer(this._shape))},_fireCreatedEvent:function(){this._map.fire("draw:rectangle-created",{rect:new L.Rectangle(this._shape.getBounds(),this.options.shapeOptions)})}}),L.Marker.Draw=L.Handler.Draw.extend({type:"marker",options:{icon:new L.Icon.Default,zIndexOffset:2e3},addHooks:function(){L.Handler.Draw.prototype.addHooks.call(this),this._map&&(this._updateLabelText({text:"Click map to place marker."}),this._map.on("mousemove",this._onMouseMove,this))},removeHooks:function(){L.Handler.Draw.prototype.removeHooks.call(this),this._map&&(this._marker&&(this._marker.off("click",this._onClick),this._map.off("click",this._onClick).removeLayer(this._marker),delete this._marker),this._map.off("mousemove",this._onMouseMove))},_onMouseMove:function(e){var t=e.layerPoint,n=e.latlng;this._updateLabelPosition(t),this._marker?this._marker.setLatLng(n):(this._marker=new L.Marker(n,{icon:this.options.icon,zIndexOffset:this.options.zIndexOffset}),this._marker.on("click",this._onClick,this),this._map.on("click",this._onClick,this).addLayer(this._marker))},_onClick:function(e){this._map.fire("draw:marker-created",{marker:new L.Marker(this._marker.getLatLng(),{icon:this.options.icon})}),this.disable()}}),L.Map.mergeOptions({drawControl:!1}),L.Control.Draw=L.Control.extend({options:{position:"topleft",polyline:{title:"Draw a polyline"},polygon:{title:"Draw a polygon"},rectangle:{title:"Draw a rectangle"},circle:{title:"Draw a circle"},marker:{title:"Add a marker"}},initialize:function(e){L.Util.extend(this.options,e)},onAdd:function(e){var t="leaflet-control-draw",n="leaflet-bar",r=n+"-part",i=L.DomUtil.create("div",t+" "+n),s=[];return this.handlers={},this.options.polyline&&(this.handlers.polyline=new L.Polyline.Draw(e,this.options.polyline),s.push(this._createButton(this.options.polyline.title,t+"-polyline "+r,i,this.handlers.polyline.enable,this.handlers.polyline)),this.handlers.polyline.on("activated",this._disableInactiveModes,this)),this.options.polygon&&(this.handlers.polygon=new L.Polygon.Draw(e,this.options.polygon),s.push(this._createButton(this.options.polygon.title,t+"-polygon "+r,i,this.handlers.polygon.enable,this.handlers.polygon)),this.handlers.polygon.on("activated",this._disableInactiveModes,this)),this.options.rectangle&&(this.handlers.rectangle=new L.Rectangle.Draw(e,this.options.rectangle),s.push(this._createButton(this.options.rectangle.title,t+"-rectangle "+r,i,this.handlers.rectangle.enable,this.handlers.rectangle)),this.handlers.rectangle.on("activated",this._disableInactiveModes,this)),this.options.circle&&(this.handlers.circle=new L.Circle.Draw(e,this.options.circle),s.push(this._createButton(this.options.circle.title,t+"-circle "+r,i,this.handlers.circle.enable,this.handlers.circle)),this.handlers.circle.on("activated",this._disableInactiveModes,this)),this.options.marker&&(this.handlers.marker=new L.Marker.Draw(e,this.options.marker),s.push(this._createButton(this.options.marker.title,t+"-marker "+r,i,this.handlers.marker.enable,this.handlers.marker)),this.handlers.marker.on("activated",this._disableInactiveModes,this)),L.DomUtil.addClass(s[0],r+"-top"),L.DomUtil.addClass(s[s.length-1],r+"-bottom"),i},_createButton:function(e,t,n,r,i){var s=L.DomUtil.create("a",t,n);return s.href="#",s.title=e,L.DomEvent.on(s,"click",L.DomEvent.stopPropagation).on(s,"mousedown",L.DomEvent.stopPropagation).on(s,"dblclick",L.DomEvent.stopPropagation).on(s,"click",L.DomEvent.preventDefault).on(s,"click",r,i),s},_disableInactiveModes:function(){for(var e in this.handlers)this.handlers.hasOwnProperty(e)&&this.handlers[e].enabled()&&this.handlers[e].disable()}}),L.Map.addInitHook(function(){this.options.drawControl&&(this.drawControl=new L.Control.Draw,this.addControl(this.drawControl))})})(this);
\ No newline at end of file
diff --git a/ckanext/spatial/public/resource.config b/ckanext/spatial/public/resource.config
index c79db57..d4bc955 100644
--- a/ckanext/spatial/public/resource.config
+++ b/ckanext/spatial/public/resource.config
@@ -18,6 +18,17 @@ dataset_map =
js/vendor/leaflet/leaflet.css
css/dataset_map.css
+spatial_query =
+
+ js/vendor/leaflet/leaflet.js
+ js/vendor/leaflet.draw/leaflet.draw.js
+ js/spatial_query.js
+
+ js/vendor/leaflet/leaflet.css
+ js/vendor/leaflet/leaflet.ie.css
+ js/vendor/leaflet.draw/leaflet.draw.css
+ js/vendor/leaflet.draw/leaflet.draw.ie.css
+ css/spatial_query.css
wms =
diff --git a/ckanext/spatial/templates/spatial/snippets/spatial_query.html b/ckanext/spatial/templates/spatial/snippets/spatial_query.html
new file mode 100644
index 0000000..15be7a6
--- /dev/null
+++ b/ckanext/spatial/templates/spatial/snippets/spatial_query.html
@@ -0,0 +1,17 @@
+
+
+{% resource 'ckanext-spatial/spatial_query' %}
+