[#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.
This commit is contained in:
amercader 2013-05-13 16:14:48 +01:00
parent 45f4f4da57
commit af253d0f89
15 changed files with 1411 additions and 8 deletions

View File

@ -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
+++++++++++++++++++++++++++++++++++++++++++++++++++

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

View File

@ -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: [
'<div id="dataset-map-edit-buttons">',
'<a href="javascript:;" class="btn cancel">Cancel</a> ',
'<a href="javascript:;" class="btn apply disabled">Apply</a>',
'</div>'
].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) {
$('<input type="hidden" />').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);
}
}
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

View File

@ -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 ? '<span class="leaflet-draw-label-subtext">' + labelText.subtext + '</span>' + '<br />' : '') +
'<span>' + labelText.text + '</span>';
},
_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: '<strong>Error:</strong> 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));

View File

@ -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; }
}

View File

@ -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;
}

File diff suppressed because one or more lines are too long

View File

@ -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 =

View File

@ -0,0 +1,17 @@
<section id="dataset-map" class="module module-narrow module-shallow">
<h2 class="module-heading">
<i class="icon-medium icon-globe"></i>
{{ _('Filter by location') }}
<a href="{{ h.remove_url_param(['ext_bbox','ext_prev_extent', 'ext_location']) }}" class="action">{{ _('Clear') }}</a>
</h2>
<div class="dataset-map" data-module="spatial-query">
<div id="dataset-map-container"></div>
</div>
<div id="dataset-map-attribution">
<div>Map data CC-BY-SA by <a href="http://openstreetmap.org">OpenStreetMap</a></div>
<div>Tiles by <a href="http://www.mapquest.com">MapQuest</a></div>
</div>
</section>
{% resource 'ckanext-spatial/spatial_query' %}