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: [ + '
', + 'Cancel ', + 'Apply', + '
' + ].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 @@ +
+

+ + {{ _('Filter by location') }} + {{ _('Clear') }} +

+
+
+
+
+
Map data CC-BY-SA by OpenStreetMap
+
Tiles by MapQuest
+
+
+ +{% resource 'ckanext-spatial/spatial_query' %} +