[merge] from feature-1272-store-extents-from-form

This commit is contained in:
Adrià Mercader 2011-09-30 18:09:21 +01:00
commit 3a8f6c9cf6
22 changed files with 1764 additions and 1053 deletions

View File

@ -3,8 +3,11 @@ ckanext-spatial - Geo related plugins for CKAN
============================================== ==============================================
This extension contains plugins that add geospatial capabilities to CKAN. This extension contains plugins that add geospatial capabilities to CKAN.
Currently, there are a WMS previewer (`wms_preview`) and a spatial query The following plugins are currently available:
API call (`spatial_query`) available.
* Automatic geo-indexing and spatial API call (`spatial_query`).
* Map widget showing a package extent (`dataset_extent_map`).
* A Web Map Service (WMS) previewer (`wms_preview`).
Dependencies Dependencies
============ ============
@ -12,7 +15,13 @@ Dependencies
You will need CKAN installed. The present module should be installed at least You will need CKAN installed. The present module should be installed at least
with `setup.py develop` if not installed in the normal way with with `setup.py develop` if not installed in the normal way with
`setup.py install` or using pip or easy_install. `setup.py install` or using pip or easy_install.
The extension uses the GeoAlchemy_ and Shapely_ libraries. You can install them
via `pip install -r pip-requirements.txt` from the extension directory.
.. _GeoAlchemy: http://www.geoalchemy.org
.. _Shapely: https://github.com/sgillies/shapely
If you want to use the spatial search API, you will need PostGIS installed If you want to use the spatial search API, you will need PostGIS installed
and enable the spatial features of your PostgreSQL database. See the and enable the spatial features of your PostgreSQL database. See the
"Setting up PostGIS" section for details. "Setting up PostGIS" section for details.
@ -25,14 +34,15 @@ DB tables running the following command (with your python env activated)::
paster spatial initdb [srid] --config=../ckan/development.ini paster spatial initdb [srid] --config=../ckan/development.ini
You can define the SRID of the geometry column. Default is 4258. You can define the SRID of the geometry column. Default is 4326. If you are not
familiar with projections, we recommend to use the default value.
Problems you may find:: Problems you may find::
LINE 1: SELECT AddGeometryColumn('package_extent','the_geom', E'4258... LINE 1: SELECT AddGeometryColumn('package_extent','the_geom', E'4326...
^ ^
HINT: No function matches the given name and argument types. You might need to add explicit type casts. HINT: No function matches the given name and argument types. You might need to add explicit type casts.
"SELECT AddGeometryColumn('package_extent','the_geom', %s, 'POLYGON', 2)" ('4258',) "SELECT AddGeometryColumn('package_extent','the_geom', %s, 'GEOMETRY', 2)" ('4326',)
PostGIS was not installed correctly. Please check the "Setting up PostGIS" section. PostGIS was not installed correctly. Please check the "Setting up PostGIS" section.
@ -43,16 +53,17 @@ The user accessing the ckan database needs to be owner (or have
permissions) of the geometry_columns and spatial_ref_sys tables permissions) of the geometry_columns and spatial_ref_sys tables
Plugins are configured as follows in the CKAN ini file:: Plugins are configured as follows in the CKAN ini file (Add only the ones you
are interested in)::
ckan.plugins = wms_preview spatial_query ckan.plugins = wms_preview spatial_query dataset_extent_map
If you are using the spatial search feature, you can define the projection If you are using the spatial search feature, you can define the projection
in which extents are stored in the database with the following option. Use 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 the EPSG code as an integer (e.g 4326, 4258, 27700, etc). It defaults to
4258:: 4326::
ckan.spatial.srid = 4258 ckan.spatial.srid = 4326
@ -65,11 +76,11 @@ The following operations can be run from the command line using the
initdb [srid] initdb [srid]
- Creates the necessary tables. You must have PostGIS installed - Creates the necessary tables. You must have PostGIS installed
and configured in the database. and configured in the database.
You can privide the SRID of the geometry column. Default is 4258. You can privide the SRID of the geometry column. Default is 4326.
extents extents
- creates or updates the extent geometry column for packages with - creates or updates the extent geometry column for packages with
a bounding box defined in extras an extent defined in the 'spatial' extra.
The commands should be run from the ckanext-spatial directory and expect The commands should be run from the ckanext-spatial directory and expect
a development.ini file to be present. Most of the time you will specify a development.ini file to be present. Most of the time you will specify
@ -78,8 +89,11 @@ the config explicitly though::
paster extents update --config=../ckan/development.ini paster extents update --config=../ckan/development.ini
API Spatial Query
=== =============
To enable the spatial query you need to add the `spatial_query` plugin to your
ini file (See 'Configuration').
The extension adds the following call to the CKAN search API, which returns The extension adds the following call to the CKAN search API, which returns
packages with an extent that intersects with the bounding box provided:: packages with an extent that intersects with the bounding box provided::
@ -90,9 +104,52 @@ If the bounding box coordinates are not in the same projection as the one
defined in the database, a CRS must be provided, in one of the following defined in the database, a CRS must be provided, in one of the following
forms: forms:
- urn:ogc:def:crs:EPSG::4258 - urn:ogc:def:crs:EPSG::4326
- EPSG:4258 - EPSG:4326
- 4258 - 4326
Geo-Indexing your packages
--------------------------
In order to make a package queryable by location, an special extra must
be defined, with its key named 'spatial'. The value must be a valid GeoJSON_
geometry, for example::
{"type":"Polygon","coordinates":[[[2.05827, 49.8625],[2.05827, 55.7447], [-6.41736, 55.7447], [-6.41736, 49.8625], [2.05827, 49.8625]]]}
or::
{ "type": "Point", "coordinates": [-3.145,53.078] }
.. _GeoJSON: http://geojson.org
Every time a package is created, updated or deleted, the extension will synchronize
the information stored in the extra with the geometry table.
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.
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.
WMS Previewer
=============
To enable the WMS previewer you need to add the `wms_preview` plugin to your
ini file (See 'Configuration').
Please note that this is an experimental plugin and may be unstable.
When the plugin is enabled, if datasets contain a resource that has 'WMS' format,
a 'View available WMS layers' link will be displayed on the dataset details page.
It forwards to a simple map viewer that will attempt to load the remote service
layers, based on the GetCapabilities response.
Setting up PostGIS Setting up PostGIS
@ -158,7 +215,7 @@ Setting up a spatial table
-------------------------- --------------------------
**Note:** If you run the ``initdb`` command, the table was already created for **Note:** If you run the ``initdb`` command, the table was already created for
you. This sections just describes what's going on for those who want to know you. This section just describes what's going on for those who want to know
more. more.
To be able to store geometries and perform spatial operations, PostGIS To be able to store geometries and perform spatial operations, PostGIS
@ -171,11 +228,12 @@ added via the ``AddGeometryColumn`` function::
ALTER TABLE package_extent OWNER TO [your_user]; ALTER TABLE package_extent OWNER TO [your_user];
SELECT AddGeometryColumn('package_extent','the_geom', 4258, 'POLYGON', 2); SELECT AddGeometryColumn('package_extent','the_geom', 4326, 'POLYGON', 2);
This will add a geometry column in the ``package_extent`` table called This will add a geometry column in the ``package_extent`` table called
``the_geom``, with the spatial reference system EPSG:4258. The stored ``the_geom``, with the spatial reference system EPSG:4326. The stored
geometries will be polygons, with 2 dimensions. geometries will be polygons, with 2 dimensions (The actual table on CKAN
uses the GEOMETRY type to support multiple geometry types).
Have a look a the table definition, and see how PostGIS has created Have a look a the table definition, and see how PostGIS has created
three constraints to ensure that the geometries follow the parameters three constraints to ensure that the geometries follow the parameters
@ -193,4 +251,4 @@ defined in the geometry column creation::
Check constraints: Check constraints:
"enforce_dims_the_geom" CHECK (st_ndims(the_geom) = 2) "enforce_dims_the_geom" CHECK (st_ndims(the_geom) = 2)
"enforce_geotype_the_geom" CHECK (geometrytype(the_geom) = 'POLYGON'::text OR the_geom IS NULL) "enforce_geotype_the_geom" CHECK (geometrytype(the_geom) = 'POLYGON'::text OR the_geom IS NULL)
"enforce_srid_the_geom" CHECK (st_srid(the_geom) = 4258) "enforce_srid_the_geom" CHECK (st_srid(the_geom) = 4326)

View File

@ -1,9 +1,12 @@
import sys import sys
import re import re
from pprint import pprint from pprint import pprint
import logging
from ckan.lib.cli import CkanCommand from ckan.lib.cli import CkanCommand
from ckanext.spatial.lib import save_extent from ckan.lib.helpers import json
from ckanext.spatial.lib import save_package_extent
log = logging.getLogger(__name__)
class Spatial(CkanCommand): class Spatial(CkanCommand):
'''Performs spatially related operations. '''Performs spatially related operations.
@ -12,11 +15,11 @@ class Spatial(CkanCommand):
spatial initdb [srid] spatial initdb [srid]
Creates the necessary tables. You must have PostGIS installed Creates the necessary tables. You must have PostGIS installed
and configured in the database. and configured in the database.
You can provide the SRID of the geometry column. Default is 4258. You can provide the SRID of the geometry column. Default is 4326.
spatial extents spatial extents
Creates or updates the extent geometry column for packages with Creates or updates the extent geometry column for packages with
a bounding box defined in extras an extent defined in the 'spatial' extra.
The commands should be run from the ckanext-spatial directory and expect The commands should be run from the ckanext-spatial directory and expect
a development.ini file to be present. Most of the time you will a development.ini file to be present. Most of the time you will
@ -63,19 +66,32 @@ class Spatial(CkanCommand):
conn = Session.connection() conn = Session.connection()
packages = [extra.package \ packages = [extra.package \
for extra in \ for extra in \
Session.query(PackageExtra).filter(PackageExtra.key == 'bbox-east-long').all()] Session.query(PackageExtra).filter(PackageExtra.key == 'spatial').all()]
error = False errors = []
count = 0
for package in packages: for package in packages:
try: try:
save_extent(package) value = package.extras['spatial']
except: log.debug('Received: %r' % value)
errors = True geometry = json.loads(value)
if error: count += 1
msg = "There was an error saving the package extent. Have you set up the package_extent table in the DB?" except ValueError,e:
else: errors.append(u'Package %s - Error decoding JSON object: %s' % (package.id,str(e)))
msg = "Done. Extents generated for %i packages" % len(packages) except TypeError,e:
errors.append(u'Package %s - Error decoding JSON object: %s' % (package.id,str(e)))
save_package_extent(package.id,geometry)
Session.commit()
if errors:
msg = 'Errors were found:\n%s' % '\n'.join(errors)
print msg
msg = "Done. Extents generated for %i out of %i packages" % (count,len(packages))
print msg print msg

View File

@ -1,64 +1,52 @@
from ckan.lib.helpers import json from string import Template
import ckan.lib.helpers as h
from ckan.lib.base import c, g, request, \
response, session, render, config, abort, redirect
from ckan.lib.base import request, config, abort
from ckan.controllers.api import ApiController as BaseApiController from ckan.controllers.api import ApiController as BaseApiController
from ckan.model import Session from ckan.model import Session
from ckanext.spatial.lib import get_srid from ckanext.spatial.lib import get_srid
from ckanext.spatial.model import PackageExtent
from geoalchemy import WKTSpatialElement, functions
class ApiController(BaseApiController): class ApiController(BaseApiController):
db_srid = int(config.get('ckan.spatial.srid', '4258')) 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): def spatial_query(self):
error_400_msg = 'Please provide a suitable bbox parameter [minx,miny,maxx,maxy]'
if not 'bbox' in request.params: if not 'bbox' in request.params:
abort(400) abort(400,error_400_msg)
bbox = request.params['bbox'].split(',') bbox = request.params['bbox'].split(',')
if len(bbox) is not 4: if len(bbox) is not 4:
abort(400) abort(400,error_400_msg)
minx = float(bbox[0]) try:
miny = float(bbox[1]) minx = float(bbox[0])
maxx = float(bbox[2]) miny = float(bbox[1])
maxy = float(bbox[3]) 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)
params = {'minx':minx,'miny':miny,'maxx':maxx,'maxy':maxy,'db_srid':self.db_srid}
srid = get_srid(request.params.get('crs')) if 'crs' in request.params else None srid = get_srid(request.params.get('crs')) if 'crs' in request.params else None
if srid and srid != self.db_srid: if srid and srid != self.db_srid:
# The input bounding box is defined in another projection, we need # Input geometry needs to be transformed to the one used on the database
# to transform it input_geometry = functions.transform(WKTSpatialElement(wkt,srid),self.db_srid)
statement = """SELECT package_id FROM package_extent WHERE
ST_Intersects(
ST_Transform(
ST_GeomFromText('POLYGON ((%(minx)s %(miny)s,
%(maxx)s %(miny)s,
%(maxx)s %(maxy)s,
%(minx)s %(maxy)s,
%(minx)s %(miny)s))',%(srid)s),
%(db_srid)s)
,the_geom)"""
params.update({'srid': srid})
else: else:
statement = """SELECT package_id FROM package_extent WHERE input_geometry = WKTSpatialElement(wkt,self.db_srid)
ST_Intersects(
ST_GeomFromText('POLYGON ((%(minx)s %(miny)s, extents = Session.query(PackageExtent).filter(PackageExtent.the_geom.intersects(input_geometry))
%(maxx)s %(miny)s, ids = [extent.package_id for extent in extents]
%(maxx)s %(maxy)s,
%(minx)s %(maxy)s,
%(minx)s %(miny)s))',%(db_srid)s),
the_geom)"""
conn = Session.connection()
rows = conn.execute(statement,params)
ids = [row['package_id'] for row in rows]
output = dict(count=len(ids),results=ids) output = dict(count=len(ids),results=ids)
return self._finish_ok(output) return self._finish_ok(output)

View File

@ -10,14 +10,6 @@ from ckan.model import Package
class ViewController(BaseController): class ViewController(BaseController):
def __before__(self, action, **env):
super(ViewController, self).__before__(action, **env)
# All calls to this controller must be with a sysadmin key
if not self.authorizer.is_sysadmin(c.user):
response_msg = _('Not authorized to see this page')
status = 401
abort(status, response_msg)
def wms_preview(self,id): def wms_preview(self,id):
#check if package exists #check if package exists
c.pkg = Package.get(id) c.pkg = Package.get(id)
@ -26,10 +18,10 @@ class ViewController(BaseController):
for res in c.pkg.resources: for res in c.pkg.resources:
if res.format == "WMS": if res.format == "WMS":
c.wms = res c.wms_url = res.url if not '?' in res.url else res.url.split('?')[0]
break break
if not c.wms: if not c.wms_url:
abort(400, 'This package does not have a WMS') abort(400, 'This package does not have a WMS resource')
return render('ckanext/spatial/wms_preview.html') return render('ckanext/spatial/wms_preview.html')

View File

@ -3,3 +3,30 @@ MAP_VIEW="""
<a href="/package/%(name)s/map">View available WMS layers &raquo;</a> <a href="/package/%(name)s/map">View available WMS layers &raquo;</a>
</div> </div>
""" """
PACKAGE_MAP="""
<hr class="cleared" />
<div class="dataset-map subsection">
<h3>%(title)s</h3>
<div id="dataset-map-container"></div>
</div>
"""
PACKAGE_MAP_EXTRA_HEADER="""
<link type="text/css" rel="stylesheet" media="all" href="/ckanext/spatial/css/dataset_map.css" />
"""
PACKAGE_MAP_EXTRA_FOOTER="""
<script type="text/javascript" src="/ckanext/spatial/js/openlayers/OpenLayers_dataset_map.js"></script>
<script type="text/javascript" src="/ckanext/spatial/js/dataset_map.js"></script>
<script type="text/javascript">
//<![CDATA[
$(document).ready(function(){
CKAN.DatasetMap.extent = '%(extent)s';
CKAN.DatasetMap.setup();
})
//]]>
</script>
"""

View File

@ -1,17 +1,21 @@
from ckan.model import Session, repo import logging
from ckan.model import Package
from ckan.model import Session
from ckan.lib.base import config from ckan.lib.base import config
from ckanext.spatial.model import PackageExtent
from shapely.geometry import asShape
log = __import__("logging").getLogger(__name__) from geoalchemy import WKTSpatialElement
log = logging.getLogger(__name__)
def get_srid(crs): def get_srid(crs):
"""Returns the SRID for the provided CRS definition """Returns the SRID for the provided CRS definition
The CRS can be defined in the following formats The CRS can be defined in the following formats
- urn:ogc:def:crs:EPSG::4258 - urn:ogc:def:crs:EPSG::4326
- EPSG:4258 - EPSG:4326
- 4258 - 4326
""" """
if ':' in crs: if ':' in crs:
@ -20,95 +24,51 @@ def get_srid(crs):
else: else:
srid = crs srid = crs
return srid return int(srid)
def save_extent(package,extent=False): def save_package_extent(package_id, geometry = None, srid = None):
'''Updates the package extent in the package_extent geometry column '''Adds, updates or deletes the package extent geometry.
If no extent provided (as a dict with minx,miny,maxx,maxy and srid keys),
the values stored in the package extras are used'''
db_srid = int(config.get('ckan.spatial.srid', '4258')) package_id: Package unique identifier
conn = Session.connection() geometry: a Python object implementing the Python Geo Interface
(i.e a loaded GeoJSON object)
srid: The spatial reference in which the geometry is provided.
If None, it defaults to the DB srid.
srid = None Will throw ValueError if the geometry object does not provide a geo interface.
if extent:
minx = extent['minx']
miny = extent['miny']
maxx = extent['maxx']
maxy = extent['maxy']
if 'srid' in extent:
srid = extent['srid']
else:
minx = float(package.extras.get('bbox-east-long'))
miny = float(package.extras.get('bbox-south-lat'))
maxx = float(package.extras.get('bbox-west-long'))
maxy = float(package.extras.get('bbox-north-lat'))
if srid: '''
srid = str(srid) db_srid = int(config.get('ckan.spatial.srid', '4326'))
try:
# Check if extent already exists
rows = conn.execute('SELECT package_id FROM package_extent WHERE package_id = %s',package.id).fetchall()
update =(len(rows) > 0)
params = {'id':package.id, 'minx':minx,'miny':miny,'maxx':maxx,'maxy':maxy, 'db_srid': db_srid} existing_package_extent = Session.query(PackageExtent).filter(PackageExtent.package_id==package_id).first()
if update: if geometry:
# Update shape = asShape(geometry)
if srid and srid != db_srid:
# We need to reproject the input geometry if not srid:
statement = """UPDATE package_extent SET srid = db_srid
the_geom = ST_Transform(
ST_GeomFromText('POLYGON ((%(minx)s %(miny)s, package_extent = PackageExtent(package_id=package_id,the_geom=WKTSpatialElement(shape.wkt, srid))
%(maxx)s %(miny)s,
%(maxx)s %(maxy)s, # Check if extent exists
%(minx)s %(maxy)s, if existing_package_extent:
%(minx)s %(miny)s))',%(srid)s),
%(db_srid)s) # If extent exists but we received no geometry, we'll delete the existing one
WHERE package_id = %(id)s if not geometry:
""" existing_package_extent.delete()
params.update({'srid': srid}) log.debug('Deleted extent for package %s' % package_id)
else:
statement = """UPDATE package_extent SET
the_geom = ST_GeomFromText('POLYGON ((%(minx)s %(miny)s,
%(maxx)s %(miny)s,
%(maxx)s %(maxy)s,
%(minx)s %(maxy)s,
%(minx)s %(miny)s))',%(db_srid)s)
WHERE package_id = %(id)s
"""
msg = 'Updated extent for package %s'
else: else:
# Insert # Check if extent changed
if srid and srid != db_srid: if Session.scalar(package_extent.the_geom.wkt) <> Session.scalar(existing_package_extent.the_geom.wkt):
# We need to reproject the input geometry # Update extent
statement = """INSERT INTO package_extent (package_id,the_geom) VALUES ( existing_package_extent.the_geom = package_extent.the_geom
%(id)s, existing_package_extent.save()
ST_Transform( log.debug('Updated extent for package %s' % package_id)
ST_GeomFromText('POLYGON ((%(minx)s %(miny)s,
%(maxx)s %(miny)s,
%(maxx)s %(maxy)s,
%(minx)s %(maxy)s,
%(minx)s %(miny)s))',%(srid)s),
%(db_srid))
)"""
params.update({'srid': srid})
else: else:
statement = """INSERT INTO package_extent (package_id,the_geom) VALUES ( log.debug('Extent for package %s unchanged' % package_id)
%(id)s, elif geometry:
ST_GeomFromText('POLYGON ((%(minx)s %(miny)s, # Insert extent
%(maxx)s %(miny)s, Session.add(package_extent)
%(maxx)s %(maxy)s, log.debug('Created new extent for package %s' % package_id)
%(minx)s %(maxy)s,
%(minx)s %(miny)s))',%(db_srid)s))"""
msg = 'Created new extent for package %s'
conn.execute(statement,params)
Session.commit()
log.info(msg, package.id)
return package
except Exception,e:
log.error('An error occurred when saving the extent for package %s: %r' % (package.id,e))
raise Exception

View File

@ -1,20 +1,41 @@
from ckan.lib.base import config
from ckan.model import Session from ckan.model import Session
from ckan.model.meta import *
from ckan.model.domain_object import DomainObject
from geoalchemy import *
from geoalchemy.postgis import PGComparator
DEFAULT_SRID = 4258 db_srid = int(config.get('ckan.spatial.srid', '4326'))
package_extent_table = Table('package_extent', metadata,
Column('package_id', types.UnicodeText, primary_key=True),
GeometryExtensionColumn('the_geom', Geometry(2,srid=db_srid)))
def setup(srid=4258): class PackageExtent(DomainObject):
def __init__(self, package_id=None, the_geom=None):
self.package_id = package_id
self.the_geom = the_geom
mapper(PackageExtent, package_extent_table, properties={
'the_geom': GeometryColumn(package_extent_table.c.the_geom,
comparator=PGComparator)})
# enable the DDL extension
GeometryDDL(package_extent_table)
DEFAULT_SRID = 4326
def setup(srid=None):
if not srid: if not srid:
srid = DEFAULT_SRID srid = DEFAULT_SRID
srid = str(srid) srid = str(srid)
connection = Session.connection() connection = Session.connection()
connection.execute("""CREATE TABLE package_extent( connection.execute('CREATE TABLE package_extent(package_id text PRIMARY KEY)')
package_id text PRIMARY KEY
);""")
#connection.execute('ALTER TABLE package_extent OWNER TO ?',user_name); connection.execute('SELECT AddGeometryColumn(\'package_extent\',\'the_geom\', %s, \'GEOMETRY\', 2)',srid)
connection.execute('SELECT AddGeometryColumn(\'package_extent\',\'the_geom\', %s, \'POLYGON\', 2)',srid)
Session.commit() Session.commit()

View File

@ -1,39 +1,130 @@
import os import os
from logging import getLogger from logging import getLogger
from pylons.i18n import _
from genshi.input import HTML from genshi.input import HTML
from genshi.filters import Transformer from genshi.filters import Transformer
import ckan.lib.helpers as h import ckan.lib.helpers as h
from ckan.lib.helpers import json
from ckan.plugins import implements, SingletonPlugin from ckan.plugins import implements, SingletonPlugin
from ckan.plugins import IRoutes, IConfigurer from ckan.plugins import IRoutes, IConfigurer
from ckan.plugins import IConfigurable, IGenshiStreamFilter from ckan.plugins import IConfigurable, IGenshiStreamFilter
from ckan.plugins import IPackageController
from ckan.logic import ValidationError
from ckan.logic.action.update import package_error_summary
import html import html
from ckanext.spatial.lib import save_package_extent
log = getLogger(__name__) log = getLogger(__name__)
class SpatialQuery(SingletonPlugin): class SpatialQuery(SingletonPlugin):
implements(IRoutes, inherit=True) implements(IRoutes, inherit=True)
implements(IPackageController, inherit=True)
def before_map(self, map): def before_map(self, map):
map.connect('api_spatial_query', '/api/2/search/package/geo', map.connect('api_spatial_query', '/api/2/search/package/geo',
controller='ckanext.spatial.controllers.api:ApiController', controller='ckanext.spatial.controllers.api:ApiController',
action='spatial_query') action='spatial_query')
return map return map
def create(self, package):
self.check_spatial_extra(package)
def edit(self, package):
self.check_spatial_extra(package)
def check_spatial_extra(self,package):
for extra in package.extras_list:
if extra.key == 'spatial':
if extra.state == 'active':
try:
log.debug('Received: %r' % extra.value)
geometry = json.loads(extra.value)
except ValueError,e:
error_dict = {'spatial':[u'Error decoding JSON object: %s' % str(e)]}
raise ValidationError(error_dict, error_summary=package_error_summary(error_dict))
except TypeError,e:
error_dict = {'spatial':[u'Error decoding JSON object: %s' % str(e)]}
raise ValidationError(error_dict, error_summary=package_error_summary(error_dict))
try:
save_package_extent(package.id,geometry)
except ValueError,e:
error_dict = {'spatial':[u'Error creating geometry: %s' % str(e)]}
raise ValidationError(error_dict, error_summary=package_error_summary(error_dict))
except Exception, e:
error_dict = {'spatial':[u'Error: %s' % str(e)]}
raise ValidationError(error_dict, error_summary=package_error_summary(error_dict))
elif extra.state == 'deleted':
# Delete extent from table
save_package_extent(package.id,None)
break
def delete(self, package):
save_package_extent(package.id,None)
class DatasetExtentMap(SingletonPlugin):
implements(IGenshiStreamFilter)
implements(IConfigurer, inherit=True)
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') == 'read' and c.pkg.id:
extent = c.pkg.extras.get('spatial',None)
if extent:
data = {'extent': extent,
'title': _('Geographic extent')}
stream = stream | Transformer('body//div[@class="dataset"]')\
.append(HTML(html.PACKAGE_MAP % data))
stream = stream | Transformer('head')\
.append(HTML(html.PACKAGE_MAP_EXTRA_HEADER % data))
stream = stream | Transformer('body')\
.append(HTML(html.PACKAGE_MAP_EXTRA_FOOTER % data))
return stream
def update_config(self, config):
here = os.path.dirname(__file__)
template_dir = os.path.join(here, 'templates')
public_dir = os.path.join(here, 'public')
if config.get('extra_template_paths'):
config['extra_template_paths'] += ','+template_dir
else:
config['extra_template_paths'] = template_dir
if config.get('extra_public_paths'):
config['extra_public_paths'] += ','+public_dir
else:
config['extra_public_paths'] = public_dir
class WMSPreview(SingletonPlugin): class WMSPreview(SingletonPlugin):
implements(IGenshiStreamFilter) implements(IGenshiStreamFilter)
implements(IRoutes, inherit=True) implements(IRoutes, inherit=True)
implements(IConfigurer, inherit=True) implements(IConfigurer, inherit=True)
def filter(self, stream): def filter(self, stream):
from pylons import request, tmpl_context as c from pylons import request, tmpl_context as c
routes = request.environ.get('pylons.routes_dict') routes = request.environ.get('pylons.routes_dict')
@ -41,13 +132,14 @@ class WMSPreview(SingletonPlugin):
if routes.get('controller') == 'package' and \ if routes.get('controller') == 'package' and \
routes.get('action') == 'read' and c.pkg.id: routes.get('action') == 'read' and c.pkg.id:
is_inspire = (c.pkg.extras.get('INSPIRE') == 'True') for res in c.pkg.resources:
# TODO: What about WFS, WCS... if res.format == "WMS":
is_wms = (c.pkg.extras.get('resource-type') == 'service') data = {'name': c.pkg.name}
if is_inspire and is_wms: stream = stream | Transformer('body//div[@class="resources subsection"]')\
data = {'name': c.pkg.name} .append(HTML(html.MAP_VIEW % data))
stream = stream | Transformer('body//div[@class="resources subsection"]')\
.append(HTML(html.MAP_VIEW % data)) break
return stream return stream
@ -65,7 +157,7 @@ class WMSPreview(SingletonPlugin):
map.connect('api_spatial_query', '/api/2/search/package/geo', map.connect('api_spatial_query', '/api/2/search/package/geo',
controller='ckanext.spatial.controllers.api:ApiController', controller='ckanext.spatial.controllers.api:ApiController',
action='spatial_query') action='spatial_query')
return map return map
def update_config(self, config): def update_config(self, config):

View File

@ -0,0 +1,9 @@
.dataset-map {
padding-bottom: 10px
}
.olControlAttribution {
left: 5px;
right: inherit;
bottom: 3px !important;
}

View File

@ -0,0 +1,104 @@
var CKAN = CKAN || {};
CKAN.DatasetMap = function($){
// Private
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"]))
}
// Public
return {
map: null,
extent: null,
styles: {
"point":{
"externalGraphic": "/ckanext/spatial/marker.png",
"graphicWidth":14,
"graphicHeight":25,
"fillOpacity":1
},
"default":{
// "fillColor":"#ee9900",
"fillColor":"#FCF6CF",
"strokeColor":"#B52",
"strokeWidth":2,
"fillOpacity":0.4,
// "pointRadius":7
}
},
setup: function(){
if (!this.extent)
return false;
// Setup some sizes
var w = $("#dataset").width();
if (w > 1024) w = 1024;
$("#dataset-map-container").width(w);
$("#dataset-map-container").height(w/3);
var layers = [
new OpenLayers.Layer.OSM()
]
// Create a new map
this.map = new OpenLayers.Map("dataset-map-container" ,
{
"projection": new OpenLayers.Projection("EPSG:900913"),
"displayProjection": new OpenLayers.Projection("EPSG:4326"),
"units": "m",
"numZoomLevels": 18,
"maxResolution": 156543.0339,
"maxExtent": new OpenLayers.Bounds(-20037508, -20037508, 20037508, 20037508.34),
"controls": [
new OpenLayers.Control.Attribution(),
new OpenLayers.Control.PanZoom(),
new OpenLayers.Control.Navigation()
],
"theme":"/ckanext/spatial/js/openlayers/theme/default/style.css"
});
this.map.addLayers(layers);
var geojson_format = new OpenLayers.Format.GeoJSON({
"internalProjection": new OpenLayers.Projection("EPSG:900913"),
"externalProjection": new OpenLayers.Projection("EPSG:4326")
});
var features = geojson_format.read(this.extent)
var geom_type = getGeomType(features[0])
var vector_layer = new OpenLayers.Layer.Vector("Dataset Extent",
{
"projection": new OpenLayers.Projection("EPSG:4326"),
"styleMap": getStyle(geom_type)
}
);
this.map.addLayer(vector_layer);
vector_layer.addFeatures(features);
if (geom_type == "point"){
this.map.setCenter(new OpenLayers.LonLat(features[0].geometry.x,features[0].geometry.y),
this.map.numZoomLevels/2)
} else {
this.map.zoomToExtent(vector_layer.getDataExtent());
}
}
}
}(jQuery)
OpenLayers.ImgPath = "/ckanext/spatial/js/openlayers/img/";

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,20 +1,30 @@
This is a custom build of the OpenLayers Javascript mapping library, These are custom builds of the OpenLayers Javascript mapping library,
slimmed down to only the features we need. slimmed down to only contain the features we need.
The file ckan.cfg contains the build profile used to build OpenLayers. The files *.cfg contain the build profile used to build OpenLayers.
In order to add more functionality, new classes must be added in the In order to add more functionality, new classes must be added in the
build profile, and then run the build command from the OpenLayers build profile, and then run the build command from the OpenLayers
distribution: distribution:
1. svn co http://svn.openlayers.org/trunk/openlayers 1. Download OpenLayers source code from http://openlayers.org
2. Modify ckan.cfg 2. Modify the cfg file
3. Go to build/ and execute:: 3. Go to build/ and execute::
python build.py {path-to-ckan.cfg} {output-file} python build.py {path-to-ckan.cfg} {output-file}
These builds have been obtained using the Closure Compiler. Please refer
to the build/README.txt on the OpenLayers Source Code for more details.
python build.py -c closure {path-to-ckan.cfg} {output-file}
The theme used for the OpenLayers controls is the "dark" theme made available The theme used for the OpenLayers controls is the "dark" theme made available
by Development Seed under the BSD License: by Development Seed under the BSD License:
https://github.com/developmentseed/openlayers_themes/blob/master/LICENSE.txt https://github.com/developmentseed/openlayers_themes/blob/master/LICENSE.txt
The default map marker is derived from an icon available at The Noun Project:
http://thenounproject.com/

View File

@ -0,0 +1,38 @@
[first]
OpenLayers/SingleFile.js
OpenLayers.js
OpenLayers/BaseTypes.js
OpenLayers/BaseTypes/Class.js
OpenLayers/Util.js
[last]
[include]
OpenLayers/Console.js
OpenLayers/Ajax.js
OpenLayers/Events.js
OpenLayers/Map.js
OpenLayers/Renderer/SVG.js
OpenLayers/Renderer/VML.js
OpenLayers/Layer.js
OpenLayers/Layer/XYZ.js
OpenLayers/Layer/SphericalMercator.js
OpenLayers/Layer/Vector.js
OpenLayers/Control/Navigation.js
OpenLayers/Control/PanZoom.js
OpenLayers/Control/PanZoomBar.js
OpenLayers/Control/Scale.js
OpenLayers/Control/MousePosition.js
OpenLayers/Control/LayerSwitcher.js
OpenLayers/Control/ArgParser.js
OpenLayers/Control/Attribution.js
OpenLayers/Format/GeoJSON.js
[exclude]
Rico/Corner.js
Firebug/firebug.js
Firebug/firebugx.js
OpenLayers/Lang/de.js
OpenLayers/Lang/en-CA.js
OpenLayers/Lang/fr.js
OpenLayers/Lang/cs-CZ.js

View File

@ -0,0 +1,40 @@
[first]
OpenLayers/SingleFile.js
OpenLayers.js
OpenLayers/BaseTypes.js
OpenLayers/BaseTypes/Class.js
OpenLayers/Util.js
Rico/Corner.js
[last]
[include]
OpenLayers/Console.js
OpenLayers/Ajax.js
OpenLayers/Events.js
OpenLayers/Map.js
OpenLayers/Layer.js
OpenLayers/Layer/Grid.js
OpenLayers/Layer/HTTPRequest.js
OpenLayers/Layer/WMS.js
OpenLayers/Layer/WMS/Untiled.js
OpenLayers/Tile.js
OpenLayers/Tile/Image.js
OpenLayers/Control/Navigation.js
OpenLayers/Control/PanZoom.js
OpenLayers/Control/PanZoomBar.js
OpenLayers/Control/Scale.js
OpenLayers/Control/MousePosition.js
OpenLayers/Control/LayerSwitcher.js
OpenLayers/Format/XML.js
OpenLayers/Format/WMSCapabilities/v1_1_1.js
OpenLayers/Format/WMSCapabilities/v1_1_1_WMSC.js
OpenLayers/Format/WMSCapabilities/v1_3_0.js
[exclude]
Firebug/firebug.js
Firebug/firebugx.js
OpenLayers/Lang/de.js
OpenLayers/Lang/en-CA.js
OpenLayers/Lang/fr.js
OpenLayers/Lang/cs-CZ.js

View File

@ -98,9 +98,8 @@ CKAN.WMSPreview = function($){
olLayers.push(dummyLayer); olLayers.push(dummyLayer);
// Setup some sizes // Setup some sizes
var w = $("#container").width() * 0.50; w = $("#content").width();
if (w > 1024) w = 1024; if (w > 1024) w = 1024;
$("#content").width($("#container").width());
$("#map").width(w); $("#map").width(w);
$("#map").height(500); $("#map").height(500);

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

View File

@ -6,11 +6,22 @@
<py:def function="page_title">${c.pkg.title or c.pkg.name} - WMS preview</py:def> <py:def function="page_title">${c.pkg.title or c.pkg.name} - WMS preview</py:def>
<py:def function="optional_head"> <py:def function="optional_head">
<link type="text/css" rel="stylesheet" media="all" href="/ckanext/spatial/wms_preview.css" /> <link type="text/css" rel="stylesheet" media="all" href="/ckanext/spatial/css/wms_preview.css" />
<script type="text/javascript" src="/ckanext/spatial/js/openlayers/OpenLayers_ckan.js"></script>
<script type="text/javascript" src="/ckanext/spatial/js/wms_preview.js"></script>
</py:def> </py:def>
<py:def function="optional_footer">
<link type="text/css" rel="stylesheet" media="all" href="/ckanext/spatial/css/wms_preview.css" />
<script type="text/javascript" src="/ckanext/spatial/js/openlayers/OpenLayers_wms_preview.js"></script>
<script type="text/javascript" src="/ckanext/spatial/js/wms_preview.js"></script>
<script type="text/javascript">
//<![CDATA[
$(document).ready(function(){
CKAN.WMSPreview.setup("${c.wms_url}");
})
//]]>
</script>
</py:def>
<div py:match="content"> <div py:match="content">
<div class="map-view-content"> <div class="map-view-content">
<h2 class="head"> <h2 class="head">
@ -18,9 +29,9 @@
</h2> </h2>
<!-- Source URL --> <!-- Source URL -->
<div class="url" py:if="c.pkg.url"> <div class="url" py:if="c.wms_url">
<p> <p>
Source: <a href="${c.wms.url}" target="_blank">${c.wms.url}</a> Source: <a href="${c.wms_url}" target="_blank">${c.wms_url}</a>
</p> </p>
</div> </div>
<p> <p>
@ -31,13 +42,6 @@
</p> </p>
<div class="package subsection"> <div class="package subsection">
<h3>WMS preview</h3> <h3>WMS preview</h3>
<script type="text/javascript">
//<![CDATA[
$(document).ready(function(){
CKAN.WMSPreview.setup("${c.wms.url}");
})
//]]>
</script>
<div id="map"></div> <div id="map"></div>
<div id="layers"></div> <div id="layers"></div>
</div> </div>

2
pip-requirements.txt Normal file
View File

@ -0,0 +1,2 @@
GeoAlchemy>=0.6
Shapely>=1.2.13

View File

@ -1,7 +1,7 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
import sys, os import sys, os
version = '0.1' version = '0.2'
setup( setup(
name='ckanext-spatial', name='ckanext-spatial',
@ -28,6 +28,7 @@ setup(
# Add plugins here, eg # Add plugins here, eg
wms_preview=ckanext.spatial.plugin:WMSPreview wms_preview=ckanext.spatial.plugin:WMSPreview
spatial_query=ckanext.spatial.plugin:SpatialQuery spatial_query=ckanext.spatial.plugin:SpatialQuery
dataset_extent_map=ckanext.spatial.plugin:DatasetExtentMap
[paste.paster_command] [paste.paster_command]
spatial=ckanext.spatial.commands.spatial:Spatial spatial=ckanext.spatial.commands.spatial:Spatial
""", """,