From af253d0f895a81550d30fef87987d77979babee5 Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 13 May 2013 16:14:48 +0100 Subject: [PATCH] [#7] Spatial search widget for 2.0 Adds a new map widget to the 2.0 search templates. It is shown initially in the sidebar but it is expanded when the user needs to draw an area. It uses Leaflet and Leaflet.draw. --- README.rst | 20 +- ckanext/spatial/public/css/spatial_query.css | 82 ++ ckanext/spatial/public/img/pencil.png | Bin 0 -> 291 bytes ckanext/spatial/public/js/spatial_query.js | 198 ++++ .../leaflet.draw/images/draw-circle.png | Bin 0 -> 1145 bytes .../leaflet.draw/images/draw-marker-icon.png | Bin 0 -> 378 bytes .../leaflet.draw/images/draw-polygon.png | Bin 0 -> 318 bytes .../leaflet.draw/images/draw-polyline.png | Bin 0 -> 266 bytes .../leaflet.draw/images/draw-rectangle.png | Bin 0 -> 138 bytes .../vendor/leaflet.draw/leaflet.draw-src.js | 955 ++++++++++++++++++ .../js/vendor/leaflet.draw/leaflet.draw.css | 118 +++ .../vendor/leaflet.draw/leaflet.draw.ie.css | 12 + .../js/vendor/leaflet.draw/leaflet.draw.js | 6 + ckanext/spatial/public/resource.config | 11 + .../spatial/snippets/spatial_query.html | 17 + 15 files changed, 1411 insertions(+), 8 deletions(-) create mode 100644 ckanext/spatial/public/css/spatial_query.css create mode 100644 ckanext/spatial/public/img/pencil.png create mode 100644 ckanext/spatial/public/js/spatial_query.js create mode 100644 ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-circle.png create mode 100644 ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-marker-icon.png create mode 100644 ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-polygon.png create mode 100644 ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-polyline.png create mode 100644 ckanext/spatial/public/js/vendor/leaflet.draw/images/draw-rectangle.png create mode 100644 ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw-src.js create mode 100644 ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw.css create mode 100644 ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw.ie.css create mode 100644 ckanext/spatial/public/js/vendor/leaflet.draw/leaflet.draw.js create mode 100644 ckanext/spatial/templates/spatial/snippets/spatial_query.html 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 0000000000000000000000000000000000000000..3ef9c8bb367cfe3f523bc537872ced51bd4a1a4e GIT binary patch literal 291 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1OBuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrH1%IR*HHxLR0PWM*dm`Sa)a@#95BMJXvMj*gDO!NJAF#WQBi zXlZG&w6vT#bLP*VKlkt7-`w1slarI0nwp)R{o}_EV`Jn0|Np;S_+c~9G_jH(zhEF2 z6EHAdI`9OjBGl8xF{I*_$(eAW0}33jfhK!zJlObu?XiYWrTm8-UQK?d', + '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 0000000000000000000000000000000000000000..60dff106feb5d4dad9c15cbbbca7b0c0035ca883 GIT binary patch literal 1145 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4uAB#T}@sR2@wC9V-A!TD(=<%vb942~)JNvR5+ zxryniL8*x;m4zo$Z5SAs6*5C2N+NuHtdjF{^%6m9^eS=-fVvqNZ0suv5|gu2OB9k) z(=+pImEP~(ucVNfVyhHx>TBRz;GCL~=}}db8eHWUl3bOYY?-2DZ>L~WVFffGH?<^D zp&~aYuh^=>Rtapb6_5=Q)>l#hD=EpgRf0Gw!Z$#{Ilm}X!Bo#cH`&0({$jZP#0Sc6WwiTtMSp~VcLG1$aY?U%fN(!v>^~=l4 z^~#O)@{7{-4J|D#L1q{k=>k>g7FXt#Bv$C=6)VF`a7isrF3Kz@$;{7F0GXJWlwVq6 zs|0i@#0$9vaAWg|p}_Eaktacjz@ z{oIU+GRF(myX+fWFP`l>dqhEJ*QOurPxuqUef}{lws)8*lDb*4r%Ob&xfO^H7Ti1k z)#m!t(;u&_e*SXj`{mECXD-Y9^)F9p$%PW00)hD(T;&DR=f=01`mVmT_Jad=Wr^US zw)c^{!&GKoNL|po^mH573PsnyzO8OPF$RB%CRwe!Vt)0TmqfKpz1r)Vx-ibm>>s~y z+&$VFp*T(WsD6vBQ=rV+33Fx~_`vbhK(x7G&WjVbt&`sgX)NwoBAe7*eJ(6fBhRqW z)UQKXm$_|6-tW0G{4$HP6_o$?$%?&LdBFX}^>1Pc3}@sb*J?Jj?gSMEp00i_>zopr E0K%+@I{*Lx literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..36de02d7ad80271528044d51f93c2e692235508e GIT binary patch literal 378 zcmV-=0fqjFP)8up2gx&_1E5Xs1qB7C0GS~Y5)$`;{Qn>t#0QCkh40{K(H3RqaUf!G1a z?7?Q!1V%>2-vR;x-pGCh@j>Dsc??a!;QRojCbF`!dI6my3U!t!hz}Bn%466Bv+XrM zKYt>K1EN8EWLr=o7b)mL?%xjz1gKvefwmlAAOwIK`$6#pqOr-d;Lz6yjh9Ajk^lh) Y08~J2lck^*Z2$lO07*qoM6N<$f(@CKcK`qY literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..579102ddb69e7ead8e7e8f757dfc487de367acb9 GIT binary patch literal 318 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4uAB#T}@sR2@wC9V-A!TD(=<%vb942~)JNvR5+ zxryniL8*x;m4zo$ZGeg%c)B=-RNR^}aUtI!2cEY6#^fZS&I87sa~wW%JrL|Mm&j+} zoW{U+yrNmsq~gc~VU=hmCieQ1^(RvcPG)@4-5d9R+jY&ax!GTrcRpZs3uT>D!R|dF zOz?*SyCTPdqnCV>CR)f@H1e`C9<_XLqqbbewnU;p^HLdG_=faPENc!hA5rRZWZ&Yo zIOm}7nFAsz57dsn`XM5-kiV}@O69ItLWbZDof$Kf zHdhZbIovJpu>^C#$ptWRk1 zIrhnZ#TSho>eqc>{WHb*SPop1z5KuAMWW*)3!#OH_8kdI$14}ldao (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' %} +