Merge branch 'feature-default-search-integration'
This commit is contained in:
commit
35ec44a038
60
README.rst
60
README.rst
|
@ -5,10 +5,15 @@ ckanext-spatial - Geo related plugins for CKAN
|
|||
This extension contains plugins that add geospatial capabilities to CKAN.
|
||||
The following plugins are currently available:
|
||||
|
||||
* Automatic geo-indexing and spatial API call (`spatial_query`).
|
||||
* Spatial model for CKAN datasets and automatic geo-indexing (`spatial_metadata`)
|
||||
* Spatial search integration and API call (`spatial_query`).
|
||||
* Map widget integrated on the search form (`spatial_query_widget`).
|
||||
* Map widget showing a dataset extent (`dataset_extent_map`).
|
||||
* A Web Map Service (WMS) previewer (`wms_preview`).
|
||||
|
||||
All plugins except the WMS previewer require the `spatial_metadata` plugin.
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
|
@ -67,15 +72,26 @@ permissions) of the geometry_columns and spatial_ref_sys tables
|
|||
Plugins are configured as follows in the CKAN ini file (Add only the ones you
|
||||
are interested in)::
|
||||
|
||||
ckan.plugins = wms_preview spatial_query dataset_extent_map
|
||||
ckan.plugins = spatial_metadata spatial_query spatial_query_widget dataset_extent_map wms_preview
|
||||
|
||||
If you are using the spatial search feature, you can define the projection
|
||||
When enabling the spatial metadata, you can define the projection
|
||||
in which extents are stored in the database with the following option. Use
|
||||
the EPSG code as an integer (e.g 4326, 4258, 27700, etc). It defaults to
|
||||
4326::
|
||||
|
||||
ckan.spatial.srid = 4326
|
||||
|
||||
If you want to define a default map extent for the different map widgets,
|
||||
(e.g. if you are running a national instace of CKAN) you can do so adding
|
||||
this configuration option::
|
||||
|
||||
ckan.spatial.default_map_extent=<minx>,<miny>,<maxx>,<maxy>
|
||||
|
||||
Coordinates must be in latitude/longitude, e.g.::
|
||||
|
||||
ckan.spatial.default_map_extent=-6.88,49.74,0.50,59.2
|
||||
|
||||
|
||||
Tests
|
||||
=====
|
||||
|
||||
|
@ -111,7 +127,8 @@ Spatial Query
|
|||
=============
|
||||
|
||||
To enable the spatial query you need to add the `spatial_query` plugin to your
|
||||
ini file (See 'Configuration').
|
||||
ini file (See `Configuration`_). This plugin requires the `spatial_metadata`
|
||||
plugin.
|
||||
|
||||
The extension adds the following call to the CKAN search API, which returns
|
||||
datasets with an extent that intersects with the bounding box provided::
|
||||
|
@ -126,6 +143,20 @@ forms:
|
|||
- EPSG:4326
|
||||
- 4326
|
||||
|
||||
As of CKAN 1.6, you can integrate your spatial query in the full CKAN
|
||||
search, via the web interface (see the `Spatial Query Widget`_) or
|
||||
via the `action API`__, e.g.::
|
||||
|
||||
POST http://localhost:5000/api/action/package_search
|
||||
{
|
||||
"q": "Pollution",
|
||||
"extras": {
|
||||
"ext_bbox": "-7.535093,49.208494,3.890688,57.372349"
|
||||
}
|
||||
}
|
||||
|
||||
__ http://docs.ckan.org/en/latest/apiv3.html
|
||||
|
||||
Geo-Indexing your datasets
|
||||
--------------------------
|
||||
|
||||
|
@ -145,11 +176,24 @@ Every time a dataset is created, updated or deleted, the extension will synchron
|
|||
the information stored in the extra with the geometry table.
|
||||
|
||||
|
||||
Spatial Query Widget
|
||||
====================
|
||||
|
||||
**Note**: this plugin requires CKAN 1.6 or higher.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Dataset Map Widget
|
||||
==================
|
||||
|
||||
To enable the dataset map you need to add the `dataset_map` plugin to your
|
||||
ini file (See 'Configuration'). You need to load the `spatial_query` plugin also.
|
||||
ini file (See `Configuration`_). You need to load the `spatial_metadata` plugin also.
|
||||
|
||||
When the plugin is enabled, if datasets contain a 'spatial' extra like the one
|
||||
described in the previous section, a map will be shown on the dataset details page.
|
||||
|
@ -159,7 +203,7 @@ WMS Previewer
|
|||
=============
|
||||
|
||||
To enable the WMS previewer you need to add the `wms_preview` plugin to your
|
||||
ini file (See 'Configuration').
|
||||
ini file (See `Configuration`_).
|
||||
|
||||
Please note that this is an experimental plugin and may be unstable.
|
||||
|
||||
|
@ -173,8 +217,8 @@ layers, based on the GetCapabilities response.
|
|||
Setting up PostGIS
|
||||
==================
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
PostGIS Configuration
|
||||
---------------------
|
||||
|
||||
* Install PostGIS::
|
||||
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
from string import Template
|
||||
|
||||
from ckan.lib.base import request, config, abort
|
||||
from ckan.controllers.api import ApiController as BaseApiController
|
||||
from ckan.model import Session, Package
|
||||
|
||||
from ckanext.spatial.lib import get_srid
|
||||
from ckanext.spatial.model import PackageExtent
|
||||
from ckanext.spatial.lib import get_srid, validate_bbox, bbox_query
|
||||
|
||||
from geoalchemy import WKTSpatialElement, functions
|
||||
|
||||
class ApiController(BaseApiController):
|
||||
|
||||
db_srid = int(config.get('ckan.spatial.srid', '4326'))
|
||||
|
||||
bbox_template = Template('POLYGON (($minx $miny, $minx $maxy, $maxx $maxy, $maxx $miny, $minx $miny))')
|
||||
|
||||
def spatial_query(self):
|
||||
|
||||
error_400_msg = 'Please provide a suitable bbox parameter [minx,miny,maxx,maxy]'
|
||||
|
@ -22,32 +15,20 @@ class ApiController(BaseApiController):
|
|||
if not 'bbox' in request.params:
|
||||
abort(400,error_400_msg)
|
||||
|
||||
bbox = request.params['bbox'].split(',')
|
||||
if len(bbox) is not 4:
|
||||
bbox = validate_bbox(request.params['bbox'])
|
||||
|
||||
if not bbox:
|
||||
abort(400,error_400_msg)
|
||||
|
||||
try:
|
||||
minx = float(bbox[0])
|
||||
miny = float(bbox[1])
|
||||
maxx = float(bbox[2])
|
||||
maxy = float(bbox[3])
|
||||
except ValueError,e:
|
||||
abort(400,error_400_msg)
|
||||
|
||||
|
||||
wkt = self.bbox_template.substitute(minx=minx,miny=miny,maxx=maxx,maxy=maxy)
|
||||
|
||||
srid = get_srid(request.params.get('crs')) if 'crs' in request.params else None
|
||||
if srid and srid != self.db_srid:
|
||||
# Input geometry needs to be transformed to the one used on the database
|
||||
input_geometry = functions.transform(WKTSpatialElement(wkt,srid),self.db_srid)
|
||||
else:
|
||||
input_geometry = WKTSpatialElement(wkt,self.db_srid)
|
||||
|
||||
extents = Session.query(PackageExtent) \
|
||||
.filter(PackageExtent.package_id==Package.id) \
|
||||
.filter(PackageExtent.the_geom.intersects(input_geometry)) \
|
||||
.filter(Package.state==u'active')
|
||||
extents = bbox_query(bbox,srid)
|
||||
|
||||
format = request.params.get('format','')
|
||||
|
||||
return self._output_results(extents,format)
|
||||
|
||||
def _output_results(self,extents,format=None):
|
||||
|
||||
ids = [extent.package_id for extent in extents]
|
||||
|
||||
|
|
|
@ -31,3 +31,39 @@ PACKAGE_MAP_EXTRA_FOOTER="""
|
|||
|
||||
|
||||
"""
|
||||
|
||||
SPATIAL_SEARCH_FORM_EXTRA_HEADER="""
|
||||
<link type="text/css" rel="stylesheet" media="all" href="/ckanext/spatial/css/spatial_search_form.css" />
|
||||
"""
|
||||
|
||||
SPATIAL_SEARCH_FORM_EXTRA_FOOTER="""
|
||||
<script type="text/javascript" src="/ckanext/spatial/js/openlayers/OpenLayers_dataset_map.js"></script>
|
||||
<script type="text/javascript" src="/ckanext/spatial/js/spatial_search_form.js"></script>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
$(document).ready(function(){
|
||||
CKAN.SpatialSearchForm.bbox = '%(bbox)s';
|
||||
CKAN.SpatialSearchForm.defaultExtent = '%(default_extent)s';
|
||||
CKAN.SpatialSearchForm.setup();
|
||||
})
|
||||
//]]>
|
||||
</script>
|
||||
"""
|
||||
|
||||
SPATIAL_SEARCH_FORM="""
|
||||
<input type="hidden" id="ext_bbox" name="ext_bbox" value="%(bbox)s" />
|
||||
<input type="hidden" id="ext_prev_extent" name="ext_prev_extent" value="" />
|
||||
|
||||
<div id="spatial-search-show"><a href="#" class="more">Filter by location</a></div>
|
||||
<div id="spatial-search-container">
|
||||
<div id="spatial-search-map-container">
|
||||
<div id="spatial-search-map"></div>
|
||||
<div id="spatial-search-toolbar">
|
||||
<input type="button" id="draw-box" value="Select an area" class="pretty-button"/>
|
||||
<input type="button" id="clear-box" value="Clear" class="pretty-button"/>
|
||||
<div class="helper">Click on the 'Select' button to draw an area of interest. Use the map controls or the mouse wheel to zoom. Drag to pan the map.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="spatial-search-map-attribution">Map data CC-BY-SA by <a href="http://openstreetmap.org">OpenStreetMap</a> | Tiles by <a href="http://www.mapquest.com">MapQuest</a></div>
|
||||
</div>
|
||||
"""
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
from string import Template
|
||||
|
||||
from ckan.model import Session
|
||||
from ckan.model import Session, Package
|
||||
from ckan.lib.base import config
|
||||
|
||||
from ckanext.spatial.model import PackageExtent
|
||||
|
@ -72,3 +73,46 @@ def save_package_extent(package_id, geometry = None, srid = None):
|
|||
Session.add(package_extent)
|
||||
log.debug('Created new extent for package %s' % package_id)
|
||||
|
||||
def validate_bbox(bbox_values):
|
||||
|
||||
if isinstance(bbox_values,basestring):
|
||||
bbox_values = bbox_values.split(',')
|
||||
|
||||
if len(bbox_values) is not 4:
|
||||
return None
|
||||
|
||||
try:
|
||||
bbox = {}
|
||||
bbox['minx'] = float(bbox_values[0])
|
||||
bbox['miny'] = float(bbox_values[1])
|
||||
bbox['maxx'] = float(bbox_values[2])
|
||||
bbox['maxy'] = float(bbox_values[3])
|
||||
except ValueError,e:
|
||||
return None
|
||||
|
||||
return bbox
|
||||
|
||||
def bbox_query(bbox,srid=None):
|
||||
|
||||
db_srid = int(config.get('ckan.spatial.srid', '4326'))
|
||||
|
||||
bbox_template = Template('POLYGON (($minx $miny, $minx $maxy, $maxx $maxy, $maxx $miny, $minx $miny))')
|
||||
|
||||
wkt = bbox_template.substitute(minx=bbox['minx'],
|
||||
miny=bbox['miny'],
|
||||
maxx=bbox['maxx'],
|
||||
maxy=bbox['maxy'])
|
||||
|
||||
if srid and srid != db_srid:
|
||||
# Input geometry needs to be transformed to the one used on the database
|
||||
input_geometry = functions.transform(WKTSpatialElement(wkt,srid),db_srid)
|
||||
else:
|
||||
input_geometry = WKTSpatialElement(wkt,db_srid)
|
||||
|
||||
extents = Session.query(PackageExtent) \
|
||||
.filter(PackageExtent.package_id==Package.id) \
|
||||
.filter(PackageExtent.the_geom.intersects(input_geometry)) \
|
||||
.filter(Package.state==u'active')
|
||||
|
||||
return extents
|
||||
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import os
|
||||
from logging import getLogger
|
||||
from pylons import config
|
||||
from pylons.i18n import _
|
||||
from genshi.input import HTML
|
||||
from genshi.filters import Transformer
|
||||
|
||||
import ckan.lib.helpers as h
|
||||
|
||||
from ckan.lib.search import SearchError
|
||||
from ckan.lib.helpers import json
|
||||
|
||||
from ckan import model
|
||||
|
||||
from ckan.plugins import implements, SingletonPlugin
|
||||
from ckan.plugins import IRoutes
|
||||
from ckan.plugins import IConfigurable, IConfigurer
|
||||
|
@ -19,15 +23,14 @@ from ckan.logic.action.update import package_error_summary
|
|||
|
||||
import html
|
||||
|
||||
from ckanext.spatial.lib import save_package_extent
|
||||
from ckanext.spatial.lib import save_package_extent,validate_bbox, bbox_query
|
||||
from ckanext.spatial.model import setup as setup_model
|
||||
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
class SpatialQuery(SingletonPlugin):
|
||||
class SpatialMetadata(SingletonPlugin):
|
||||
|
||||
implements(IRoutes, inherit=True)
|
||||
implements(IPackageController, inherit=True)
|
||||
implements(IConfigurable, inherit=True)
|
||||
|
||||
|
@ -36,12 +39,6 @@ class SpatialQuery(SingletonPlugin):
|
|||
if not config.get('ckan.spatial.testing',False):
|
||||
setup_model()
|
||||
|
||||
def before_map(self, map):
|
||||
|
||||
map.connect('api_spatial_query', '/api/2/search/{register:dataset|package}/geo',
|
||||
controller='ckanext.spatial.controllers.api:ApiController',
|
||||
action='spatial_query')
|
||||
return map
|
||||
|
||||
def create(self, package):
|
||||
self.check_spatial_extra(package)
|
||||
|
@ -88,6 +85,67 @@ class SpatialQuery(SingletonPlugin):
|
|||
def delete(self, package):
|
||||
save_package_extent(package.id,None)
|
||||
|
||||
class SpatialQuery(SingletonPlugin):
|
||||
|
||||
implements(IRoutes, inherit=True)
|
||||
implements(IPackageController, inherit=True)
|
||||
|
||||
def before_map(self, map):
|
||||
|
||||
map.connect('api_spatial_query', '/api/2/search/{register:dataset|package}/geo',
|
||||
controller='ckanext.spatial.controllers.api:ApiController',
|
||||
action='spatial_query')
|
||||
return map
|
||||
|
||||
def before_search(self,search_params):
|
||||
if 'extras' in search_params and 'ext_bbox' in search_params['extras'] \
|
||||
and search_params['extras']['ext_bbox']:
|
||||
|
||||
bbox = validate_bbox(search_params['extras']['ext_bbox'])
|
||||
if not bbox:
|
||||
raise SearchError('Wrong bounding box provided')
|
||||
|
||||
extents = bbox_query(bbox)
|
||||
|
||||
if extents.count() == 0:
|
||||
# We don't need to perform the search
|
||||
search_params['abort_search'] = True
|
||||
else:
|
||||
# We'll perform the existing search but also filtering by the ids
|
||||
# of datasets within the bbox
|
||||
bbox_query_ids = [extent.package_id for extent in extents]
|
||||
|
||||
q = search_params.get('q','')
|
||||
new_q = '%s AND ' % q if q else ''
|
||||
new_q += '(%s)' % ' OR '.join(['id:%s' % id for id in bbox_query_ids])
|
||||
|
||||
search_params['q'] = new_q
|
||||
|
||||
return search_params
|
||||
|
||||
class SpatialQueryWidget(SingletonPlugin):
|
||||
|
||||
implements(IGenshiStreamFilter)
|
||||
|
||||
def filter(self, stream):
|
||||
from pylons import request, tmpl_context as c
|
||||
routes = request.environ.get('pylons.routes_dict')
|
||||
if routes.get('controller') == 'package' and \
|
||||
routes.get('action') == 'search':
|
||||
|
||||
data = {
|
||||
'bbox': request.params.get('ext_bbox',''),
|
||||
'default_extent': config.get('ckan.spatial.default_map_extent','')
|
||||
}
|
||||
stream = stream | Transformer('body//div[@id="dataset-search-ext"]')\
|
||||
.append(HTML(html.SPATIAL_SEARCH_FORM % data))
|
||||
stream = stream | Transformer('head')\
|
||||
.append(HTML(html.SPATIAL_SEARCH_FORM_EXTRA_HEADER % data))
|
||||
stream = stream | Transformer('body')\
|
||||
.append(HTML(html.SPATIAL_SEARCH_FORM_EXTRA_FOOTER % data))
|
||||
|
||||
return stream
|
||||
|
||||
|
||||
class DatasetExtentMap(SingletonPlugin):
|
||||
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
#spatial-search-show{
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#spatial-search-show a.more:after {
|
||||
content: ' »';
|
||||
font-size: 150%;
|
||||
position: relative;
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
#spatial-search-show a.less:before {
|
||||
content: '« ';
|
||||
font-size: 150%;
|
||||
position: relative;
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
|
||||
#spatial-search-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
#spatial-search-map-container {
|
||||
float:left;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#spatial-search-map {
|
||||
float:left;
|
||||
width: 300px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
#spatial-search-toolbar{
|
||||
float:left;
|
||||
margin-left: 20px;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
#clear-box {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
#spatial-search-toolbar div{
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
#spatial-search-toolbar .helper{
|
||||
font-style: italic;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#spatial-search-map-attribution{
|
||||
font-size: x-small;
|
||||
}
|
||||
|
||||
/* OpenLayers overrides */
|
||||
.olControlZoomPanel {
|
||||
top: 10px !important;
|
||||
}
|
||||
|
||||
.olControlZoomPanel div {
|
||||
background-image: url(/ckanext/spatial/js/openlayers/img/zoom-panel.png) !important;
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
|
@ -21,6 +21,7 @@ OpenLayers/Layer/Vector.js
|
|||
OpenLayers/Control/Navigation.js
|
||||
OpenLayers/Control/PanZoom.js
|
||||
OpenLayers/Control/PanZoomBar.js
|
||||
OpenLayers/Control/ZoomPanel.js
|
||||
OpenLayers/Control/Scale.js
|
||||
OpenLayers/Control/MousePosition.js
|
||||
OpenLayers/Control/LayerSwitcher.js
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 641 B |
|
@ -0,0 +1,224 @@
|
|||
var CKAN = CKAN || {};
|
||||
|
||||
CKAN.SpatialSearchForm = function($){
|
||||
|
||||
// Projections
|
||||
var proj4326 = new OpenLayers.Projection("EPSG:4326");
|
||||
var proj900913 = new OpenLayers.Projection("EPSG:900913");
|
||||
|
||||
var getGeomType = function(feature){
|
||||
return feature.geometry.CLASS_NAME.split(".").pop().toLowerCase()
|
||||
}
|
||||
|
||||
var getStyle = function(geom_type){
|
||||
var styles = CKAN.DatasetMap.styles;
|
||||
var style = (styles[geom_type]) ? styles[geom_type] : styles["default"];
|
||||
|
||||
return new OpenLayers.StyleMap(OpenLayers.Util.applyDefaults(
|
||||
style, OpenLayers.Feature.Vector.style["default"]))
|
||||
}
|
||||
|
||||
var getParameterByName = function (name) {
|
||||
|
||||
var match = RegExp('[?&]' + name + '=([^&]*)')
|
||||
.exec(window.location.search);
|
||||
|
||||
return match ?
|
||||
decodeURIComponent(match[1].replace(/\+/g, ' '))
|
||||
: null;
|
||||
|
||||
}
|
||||
|
||||
var getBoundsFromBbox = function(bbox){
|
||||
var coords = bbox.split(",");
|
||||
var bounds = new OpenLayers.Bounds(coords[0],coords[1],coords[2],coords[3]).transform(proj4326,proj900913);
|
||||
return bounds;
|
||||
}
|
||||
|
||||
// Public
|
||||
return {
|
||||
map: null,
|
||||
|
||||
mapInitialized: false,
|
||||
|
||||
bbox: null,
|
||||
|
||||
defaultExtent: null,
|
||||
|
||||
styles: {
|
||||
"default":{
|
||||
"fillColor":"#FCF6CF",
|
||||
"strokeColor":"#B52",
|
||||
"strokeWidth":2,
|
||||
"fillOpacity":0.4,
|
||||
}
|
||||
},
|
||||
|
||||
setup: function(){
|
||||
$("#spatial-search-show").click(this.toggleDiv);
|
||||
// There is a bbox available from a previous search
|
||||
if (CKAN.SpatialSearchForm.bbox){
|
||||
this.toggleDiv();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
toggleDiv: function(){
|
||||
$("#spatial-search-show>a").toggleClass("more less");
|
||||
$("#spatial-search-container").toggle();
|
||||
if (!CKAN.SpatialSearchForm.mapInitialized){
|
||||
CKAN.SpatialSearchForm.mapSetup()
|
||||
}
|
||||
},
|
||||
|
||||
mapSetup: function(){
|
||||
|
||||
|
||||
var mapquestTiles = [
|
||||
"http://otile1.mqcdn.com/tiles/1.0.0/osm/${z}/${x}/${y}.jpg",
|
||||
"http://otile2.mqcdn.com/tiles/1.0.0/osm/${z}/${x}/${y}.jpg",
|
||||
"http://otile3.mqcdn.com/tiles/1.0.0/osm/${z}/${x}/${y}.jpg",
|
||||
"http://otile4.mqcdn.com/tiles/1.0.0/osm/${z}/${x}/${y}.jpg"];
|
||||
|
||||
var layers = [
|
||||
//new OpenLayers.Layer.OSM()
|
||||
new OpenLayers.Layer.OSM("MapQuest-OSM Tiles", mapquestTiles)
|
||||
]
|
||||
|
||||
// Create a new map
|
||||
this.map = new OpenLayers.Map("spatial-search-map" ,
|
||||
{
|
||||
"projection": proj900913,
|
||||
"displayProjection": proj4326,
|
||||
"units": "m",
|
||||
"numZoomLevels": 18,
|
||||
"maxResolution": 156543.0339,
|
||||
"maxExtent": new OpenLayers.Bounds(-20037508, -20037508, 20037508, 20037508.34),
|
||||
"controls": [
|
||||
new OpenLayers.Control.ZoomPanel(),
|
||||
new OpenLayers.Control.Navigation()
|
||||
],
|
||||
"theme":"/ckanext/spatial/js/openlayers/theme/default/style.css"
|
||||
});
|
||||
|
||||
var query = new OpenLayers.Control.BoxQuery();
|
||||
this.map.addControl(query);
|
||||
|
||||
this.map.addLayers(layers);
|
||||
|
||||
var vector_layer = new OpenLayers.Layer.Vector("Bounding Box",
|
||||
{
|
||||
"projection": proj4326,
|
||||
"styleMap": new OpenLayers.StyleMap(this.styles["default"])
|
||||
}
|
||||
);
|
||||
|
||||
// Setup buttons events
|
||||
$("#draw-box").click(function(){
|
||||
if (!query.active){
|
||||
query.activate();
|
||||
$("#draw-box").addClass("depressed");
|
||||
} else {
|
||||
$("#draw-box").removeClass("depressed");
|
||||
query.deactivate();
|
||||
}
|
||||
});
|
||||
|
||||
$("#clear-box").click(function(){
|
||||
if (query.active){
|
||||
$("#draw-box").removeClass("depressed");
|
||||
query.deactivate();
|
||||
}
|
||||
vector_layer.destroyFeatures();
|
||||
$("#ext_bbox").val('');
|
||||
});
|
||||
|
||||
var bounds;
|
||||
// Check if there's a bbox from a previous search or a default
|
||||
// extent defined
|
||||
if (this.bbox) {
|
||||
var bboxBounds = getBoundsFromBbox(this.bbox);
|
||||
var feature = new OpenLayers.Feature.Vector(
|
||||
bboxBounds.toGeometry()
|
||||
);
|
||||
vector_layer.addFeatures([feature]);
|
||||
bounds = bboxBounds;
|
||||
}
|
||||
|
||||
var previousExtent = getParameterByName("ext_prev_extent");
|
||||
if (previousExtent && this.bbox){
|
||||
bounds = getBoundsFromBbox(previousExtent);
|
||||
} else if (this.defaultExtent) {
|
||||
bounds = getBoundsFromBbox(this.defaultExtent);
|
||||
} else {
|
||||
bounds = this.map.maxExtent;
|
||||
}
|
||||
|
||||
this.map.zoomToExtent(bounds,true);
|
||||
|
||||
this.map.addLayer(vector_layer);
|
||||
|
||||
this.map.events.register("moveend",this,function(e){
|
||||
$("#ext_prev_extent").val(e.object.getExtent().transform(proj900913,proj4326).toBBOX());
|
||||
});
|
||||
|
||||
CKAN.SpatialSearchForm.mapInitialized = true;
|
||||
|
||||
this.map.events.triggerEvent("moveend");
|
||||
}
|
||||
}
|
||||
}(jQuery)
|
||||
|
||||
// Custom control to handle clicks on the map
|
||||
OpenLayers.Control.BoxQuery = OpenLayers.Class(OpenLayers.Control, {
|
||||
type: OpenLayers.Control.TYPE_TOOL,
|
||||
|
||||
boxLayer: null,
|
||||
|
||||
draw: function() {
|
||||
this.boxLayer = this.map.getLayersByName("Bounding Box")[0];
|
||||
this.handler = new OpenLayers.Handler.Box( this,
|
||||
{done: this.done}
|
||||
);
|
||||
},
|
||||
|
||||
done: function(position){
|
||||
this.boxLayer = this.map.getLayersByName("Bounding Box")[0];
|
||||
// We need a bounding box
|
||||
if (position instanceof OpenLayers.Bounds) {
|
||||
var bounds;
|
||||
var minXY = this.map.getLonLatFromPixel(
|
||||
new OpenLayers.Pixel(position.left, position.bottom));
|
||||
var maxXY = this.map.getLonLatFromPixel(
|
||||
new OpenLayers.Pixel(position.right, position.top));
|
||||
bounds = new OpenLayers.Bounds(minXY.lon, minXY.lat,
|
||||
maxXY.lon, maxXY.lat);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.boxLayer.destroyFeatures();
|
||||
|
||||
// Add new query extent
|
||||
this.boxLayer.addFeatures([
|
||||
new OpenLayers.Feature.Vector(bounds.toGeometry())
|
||||
]);
|
||||
|
||||
// Transform bounds to wgs84
|
||||
bounds.transform(this.map.getProjectionObject(),
|
||||
new OpenLayers.Projection("EPSG:4326"));
|
||||
|
||||
// Store the coordinates in the hidden bbox field, so they are sent
|
||||
// when user submits the form
|
||||
$("#ext_bbox").val(bounds.toBBOX());
|
||||
|
||||
$("#draw-box").removeClass("depressed");
|
||||
this.deactivate();
|
||||
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
OpenLayers.ImgPath = "/ckanext/spatial/js/openlayers/img/";
|
||||
|
|
@ -23,6 +23,7 @@ class SpatialTestBase:
|
|||
|
||||
geojson_examples = {
|
||||
'point':'{"type":"Point","coordinates":[100.0,0.0]}',
|
||||
'point_2':'{"type":"Point","coordinates":[20,10]}',
|
||||
'line':'{"type":"LineString","coordinates":[[100.0,0.0],[101.0,1.0]]}',
|
||||
'polygon':'{"type":"Polygon","coordinates":[[[100.0,0.0],[101.0,0.0],[101.0,1.0],[100.0,1.0],[100.0,0.0]]]}',
|
||||
'polygon_holes':'{"type":"Polygon","coordinates":[[[100.0,0.0],[101.0,0.0],[101.0,1.0],[100.0,1.0],[100.0,0.0]],[[100.2,0.2],[100.8,0.2],[100.8,0.8],[100.2,0.8],[100.2,0.2]]]}',
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import logging
|
||||
import json
|
||||
from pprint import pprint
|
||||
|
||||
from nose.tools import assert_equal, assert_raises
|
||||
from ckan.logic.action.create import package_create
|
||||
from ckan.logic.action.delete import package_delete
|
||||
from ckan import model
|
||||
|
||||
from ckan.model import Package, Session
|
||||
import ckan.lib.search as search
|
||||
from ckan.tests import CreateTestData, setup_test_search_index
|
||||
from ckan.tests import CreateTestData, setup_test_search_index,WsgiAppCase
|
||||
from ckan.tests.functional.api.base import ApiTestCase
|
||||
from ckan.tests import TestController as ControllerTestCase
|
||||
from ckanext.spatial.tests import SpatialTestBase
|
||||
|
@ -72,3 +73,52 @@ class TestSpatialApi(ApiTestCase,SpatialTestBase,ControllerTestCase):
|
|||
assert res_dict['count'] == 0
|
||||
assert res_dict['results'] == []
|
||||
|
||||
|
||||
|
||||
class TestActionPackageSearch(SpatialTestBase,WsgiAppCase):
|
||||
|
||||
@classmethod
|
||||
def setup_class(self):
|
||||
super(TestActionPackageSearch,self).setup_class()
|
||||
setup_test_search_index()
|
||||
self.package_fixture_data_1 = {
|
||||
'name' : u'test-spatial-dataset-search-point-1',
|
||||
'title': 'Some Title 1',
|
||||
'extras': [{'key':'spatial','value':self.geojson_examples['point']}]
|
||||
}
|
||||
self.package_fixture_data_2 = {
|
||||
'name' : u'test-spatial-dataset-search-point-2',
|
||||
'title': 'Some Title 2',
|
||||
'extras': [{'key':'spatial','value':self.geojson_examples['point_2']}]
|
||||
}
|
||||
|
||||
CreateTestData.create()
|
||||
|
||||
@classmethod
|
||||
def teardown_class(self):
|
||||
model.repo.rebuild_db()
|
||||
|
||||
def test_1_basic(self):
|
||||
context = {'model':model,'session':Session,'user':'tester','extras_as_string':True}
|
||||
package_dict_1 = package_create(context,self.package_fixture_data_1)
|
||||
del context['package']
|
||||
package_dict_2 = package_create(context,self.package_fixture_data_2)
|
||||
|
||||
postparams = '%s=1' % json.dumps({
|
||||
'q': 'test',
|
||||
'facet.field': ('groups', 'tags', 'res_format', 'license'),
|
||||
'rows': 20,
|
||||
'start': 0,
|
||||
'extras': {
|
||||
'ext_bbox': '%s,%s,%s,%s' % (10,10,40,40)
|
||||
}
|
||||
})
|
||||
res = self.app.post('/api/action/package_search', params=postparams)
|
||||
res = json.loads(res.body)
|
||||
result = res['result']
|
||||
|
||||
# Only one dataset returned
|
||||
assert_equal(res['success'], True)
|
||||
assert_equal(result['count'], 1)
|
||||
assert_equal(result['results'][0]['name'], 'test-spatial-dataset-search-point-2')
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import logging
|
||||
from pylons import config
|
||||
|
||||
from ckan.lib.helpers import url_for
|
||||
|
||||
from ckan.tests.functional.base import FunctionalTestCase
|
||||
|
||||
from ckanext.spatial.tests import SpatialTestBase
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestSpatialQueryWidget(FunctionalTestCase,SpatialTestBase):
|
||||
|
||||
def test_widget_shown(self):
|
||||
# Load the dataset search page and check if the libraries have been loaded
|
||||
offset = url_for(controller='package', action='search')
|
||||
res = self.app.get(offset)
|
||||
|
||||
assert '<div id="spatial-search-container">' in res, res
|
||||
assert '<script type="text/javascript" src="/ckanext/spatial/js/spatial_search_form.js"></script>' in res
|
||||
assert config.get('ckan.spatial.default_extent') in res
|
8
setup.py
8
setup.py
|
@ -26,9 +26,11 @@ setup(
|
|||
"""
|
||||
[ckan.plugins]
|
||||
# Add plugins here, eg
|
||||
wms_preview=ckanext.spatial.nongeos_plugin:WMSPreview
|
||||
spatial_query=ckanext.spatial.plugin:SpatialQuery
|
||||
dataset_extent_map=ckanext.spatial.plugin:DatasetExtentMap
|
||||
spatial_metadata=ckanext.spatial.plugin:SpatialMetadata
|
||||
spatial_query=ckanext.spatial.plugin:SpatialQuery
|
||||
spatial_query_widget=ckanext.spatial.plugin:SpatialQueryWidget
|
||||
dataset_extent_map=ckanext.spatial.plugin:DatasetExtentMap
|
||||
wms_preview=ckanext.spatial.nongeos_plugin:WMSPreview
|
||||
[paste.paster_command]
|
||||
spatial=ckanext.spatial.commands.spatial:Spatial
|
||||
""",
|
||||
|
|
|
@ -15,8 +15,9 @@ port = 5000
|
|||
use = config:../ckan/test-core.ini
|
||||
# Here we hard-code the database and a flag to make default tests
|
||||
# run fast.
|
||||
ckan.plugins = spatial_query dataset_extent_map wms_preview
|
||||
ckan.plugins = spatial_metadata spatial_query spatial_query_widget dataset_extent_map wms_preview synchronous_search
|
||||
ckan.spatial.srid = 4326
|
||||
ckan.spatial.default_map_extent=-6.88,49.74,0.50,59.2
|
||||
ckan.spatial.testing = true
|
||||
# NB: other test configuration should go in test-core.ini, which is
|
||||
# what the postgres tests use.
|
||||
|
@ -38,13 +39,13 @@ handlers = console
|
|||
|
||||
[logger_ckan]
|
||||
qualname = ckan
|
||||
handlers =
|
||||
handlers =
|
||||
level = INFO
|
||||
|
||||
[logger_sqlalchemy]
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
level = WARN
|
||||
level = WARN
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
|
|
Loading…
Reference in New Issue