[merge] from feature-1272-store-extents-from-form
This commit is contained in:
commit
3a8f6c9cf6
100
README.rst
100
README.rst
|
@ -3,8 +3,11 @@ ckanext-spatial - Geo related plugins for CKAN
|
|||
==============================================
|
||||
|
||||
This extension contains plugins that add geospatial capabilities to CKAN.
|
||||
Currently, there are a WMS previewer (`wms_preview`) and a spatial query
|
||||
API call (`spatial_query`) available.
|
||||
The following plugins are currently 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
|
||||
============
|
||||
|
@ -13,6 +16,12 @@ 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
|
||||
`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
|
||||
and enable the spatial features of your PostgreSQL database. See the
|
||||
"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
|
||||
|
||||
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::
|
||||
|
||||
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.
|
||||
"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.
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
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]
|
||||
- Creates the necessary tables. You must have PostGIS installed
|
||||
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
|
||||
- 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
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
forms:
|
||||
|
||||
- urn:ogc:def:crs:EPSG::4258
|
||||
- EPSG:4258
|
||||
- 4258
|
||||
- urn:ogc:def:crs:EPSG::4326
|
||||
- EPSG:4326
|
||||
- 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
|
||||
|
@ -158,7 +215,7 @@ Setting up a spatial table
|
|||
--------------------------
|
||||
|
||||
**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.
|
||||
|
||||
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];
|
||||
|
||||
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
|
||||
``the_geom``, with the spatial reference system EPSG:4258. The stored
|
||||
geometries will be polygons, with 2 dimensions.
|
||||
``the_geom``, with the spatial reference system EPSG:4326. The stored
|
||||
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
|
||||
three constraints to ensure that the geometries follow the parameters
|
||||
|
@ -193,4 +251,4 @@ defined in the geometry column creation::
|
|||
Check constraints:
|
||||
"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_srid_the_geom" CHECK (st_srid(the_geom) = 4258)
|
||||
"enforce_srid_the_geom" CHECK (st_srid(the_geom) = 4326)
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import sys
|
||||
import re
|
||||
from pprint import pprint
|
||||
import logging
|
||||
|
||||
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):
|
||||
'''Performs spatially related operations.
|
||||
|
@ -12,11 +15,11 @@ class Spatial(CkanCommand):
|
|||
spatial initdb [srid]
|
||||
Creates the necessary tables. You must have PostGIS installed
|
||||
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
|
||||
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
|
||||
a development.ini file to be present. Most of the time you will
|
||||
|
@ -63,19 +66,32 @@ class Spatial(CkanCommand):
|
|||
conn = Session.connection()
|
||||
packages = [extra.package \
|
||||
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:
|
||||
try:
|
||||
save_extent(package)
|
||||
except:
|
||||
errors = True
|
||||
value = package.extras['spatial']
|
||||
log.debug('Received: %r' % value)
|
||||
geometry = json.loads(value)
|
||||
|
||||
if error:
|
||||
msg = "There was an error saving the package extent. Have you set up the package_extent table in the DB?"
|
||||
else:
|
||||
msg = "Done. Extents generated for %i packages" % len(packages)
|
||||
count += 1
|
||||
except ValueError,e:
|
||||
errors.append(u'Package %s - Error decoding JSON object: %s' % (package.id,str(e)))
|
||||
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
|
||||
|
||||
|
|
|
@ -1,64 +1,52 @@
|
|||
from ckan.lib.helpers import json
|
||||
import ckan.lib.helpers as h
|
||||
from ckan.lib.base import c, g, request, \
|
||||
response, session, render, config, abort, redirect
|
||||
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
|
||||
|
||||
from ckanext.spatial.lib import get_srid
|
||||
from ckanext.spatial.model import PackageExtent
|
||||
|
||||
from geoalchemy import WKTSpatialElement, functions
|
||||
|
||||
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):
|
||||
|
||||
error_400_msg = 'Please provide a suitable bbox parameter [minx,miny,maxx,maxy]'
|
||||
|
||||
if not 'bbox' in request.params:
|
||||
abort(400)
|
||||
abort(400,error_400_msg)
|
||||
|
||||
bbox = request.params['bbox'].split(',')
|
||||
if len(bbox) is not 4:
|
||||
abort(400)
|
||||
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)
|
||||
|
||||
params = {'minx':minx,'miny':miny,'maxx':maxx,'maxy':maxy,'db_srid':self.db_srid}
|
||||
|
||||
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:
|
||||
# The input bounding box is defined in another projection, we need
|
||||
# to transform it
|
||||
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})
|
||||
# Input geometry needs to be transformed to the one used on the database
|
||||
input_geometry = functions.transform(WKTSpatialElement(wkt,srid),self.db_srid)
|
||||
else:
|
||||
statement = """SELECT package_id FROM package_extent WHERE
|
||||
ST_Intersects(
|
||||
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),
|
||||
the_geom)"""
|
||||
conn = Session.connection()
|
||||
rows = conn.execute(statement,params)
|
||||
ids = [row['package_id'] for row in rows]
|
||||
input_geometry = WKTSpatialElement(wkt,self.db_srid)
|
||||
|
||||
extents = Session.query(PackageExtent).filter(PackageExtent.the_geom.intersects(input_geometry))
|
||||
ids = [extent.package_id for extent in extents]
|
||||
|
||||
output = dict(count=len(ids),results=ids)
|
||||
|
||||
return self._finish_ok(output)
|
||||
|
||||
|
||||
|
|
|
@ -10,14 +10,6 @@ from ckan.model import Package
|
|||
|
||||
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):
|
||||
#check if package exists
|
||||
c.pkg = Package.get(id)
|
||||
|
@ -26,10 +18,10 @@ class ViewController(BaseController):
|
|||
|
||||
for res in c.pkg.resources:
|
||||
if res.format == "WMS":
|
||||
c.wms = res
|
||||
c.wms_url = res.url if not '?' in res.url else res.url.split('?')[0]
|
||||
break
|
||||
if not c.wms:
|
||||
abort(400, 'This package does not have a WMS')
|
||||
if not c.wms_url:
|
||||
abort(400, 'This package does not have a WMS resource')
|
||||
|
||||
return render('ckanext/spatial/wms_preview.html')
|
||||
|
||||
|
|
|
@ -3,3 +3,30 @@ MAP_VIEW="""
|
|||
<a href="/package/%(name)s/map">View available WMS layers »</a>
|
||||
</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>
|
||||
|
||||
|
||||
"""
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
from ckan.model import Session, repo
|
||||
from ckan.model import Package
|
||||
import logging
|
||||
|
||||
from ckan.model import Session
|
||||
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):
|
||||
"""Returns the SRID for the provided CRS definition
|
||||
The CRS can be defined in the following formats
|
||||
- urn:ogc:def:crs:EPSG::4258
|
||||
- EPSG:4258
|
||||
- 4258
|
||||
- urn:ogc:def:crs:EPSG::4326
|
||||
- EPSG:4326
|
||||
- 4326
|
||||
"""
|
||||
|
||||
if ':' in crs:
|
||||
|
@ -20,95 +24,51 @@ def get_srid(crs):
|
|||
else:
|
||||
srid = crs
|
||||
|
||||
return srid
|
||||
return int(srid)
|
||||
|
||||
def save_extent(package,extent=False):
|
||||
'''Updates the package extent in the package_extent geometry column
|
||||
If no extent provided (as a dict with minx,miny,maxx,maxy and srid keys),
|
||||
the values stored in the package extras are used'''
|
||||
def save_package_extent(package_id, geometry = None, srid = None):
|
||||
'''Adds, updates or deletes the package extent geometry.
|
||||
|
||||
db_srid = int(config.get('ckan.spatial.srid', '4258'))
|
||||
conn = Session.connection()
|
||||
package_id: Package unique identifier
|
||||
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
|
||||
if extent:
|
||||
minx = extent['minx']
|
||||
miny = extent['miny']
|
||||
maxx = extent['maxx']
|
||||
maxy = extent['maxy']
|
||||
if 'srid' in extent:
|
||||
srid = extent['srid']
|
||||
Will throw ValueError if the geometry object does not provide a geo interface.
|
||||
|
||||
'''
|
||||
db_srid = int(config.get('ckan.spatial.srid', '4326'))
|
||||
|
||||
|
||||
existing_package_extent = Session.query(PackageExtent).filter(PackageExtent.package_id==package_id).first()
|
||||
|
||||
if geometry:
|
||||
shape = asShape(geometry)
|
||||
|
||||
if not srid:
|
||||
srid = db_srid
|
||||
|
||||
package_extent = PackageExtent(package_id=package_id,the_geom=WKTSpatialElement(shape.wkt, srid))
|
||||
|
||||
# Check if extent exists
|
||||
if existing_package_extent:
|
||||
|
||||
# If extent exists but we received no geometry, we'll delete the existing one
|
||||
if not geometry:
|
||||
existing_package_extent.delete()
|
||||
log.debug('Deleted extent for package %s' % package_id)
|
||||
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)
|
||||
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}
|
||||
|
||||
if update:
|
||||
# Update
|
||||
if srid and srid != db_srid:
|
||||
# We need to reproject the input geometry
|
||||
statement = """UPDATE package_extent SET
|
||||
the_geom = 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)
|
||||
WHERE package_id = %(id)s
|
||||
"""
|
||||
params.update({'srid': srid})
|
||||
# Check if extent changed
|
||||
if Session.scalar(package_extent.the_geom.wkt) <> Session.scalar(existing_package_extent.the_geom.wkt):
|
||||
# Update extent
|
||||
existing_package_extent.the_geom = package_extent.the_geom
|
||||
existing_package_extent.save()
|
||||
log.debug('Updated 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:
|
||||
# Insert
|
||||
if srid and srid != db_srid:
|
||||
# We need to reproject the input geometry
|
||||
statement = """INSERT INTO package_extent (package_id,the_geom) VALUES (
|
||||
%(id)s,
|
||||
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))
|
||||
)"""
|
||||
params.update({'srid': srid})
|
||||
else:
|
||||
statement = """INSERT INTO package_extent (package_id,the_geom) VALUES (
|
||||
%(id)s,
|
||||
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))"""
|
||||
msg = 'Created new extent for package %s'
|
||||
log.debug('Extent for package %s unchanged' % package_id)
|
||||
elif geometry:
|
||||
# Insert extent
|
||||
Session.add(package_extent)
|
||||
log.debug('Created new extent for package %s' % package_id)
|
||||
|
||||
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
|
||||
|
|
|
@ -1,20 +1,41 @@
|
|||
from ckan.lib.base import config
|
||||
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:
|
||||
srid = DEFAULT_SRID
|
||||
|
||||
srid = str(srid)
|
||||
|
||||
connection = Session.connection()
|
||||
connection.execute("""CREATE TABLE package_extent(
|
||||
package_id text PRIMARY KEY
|
||||
);""")
|
||||
connection.execute('CREATE TABLE package_extent(package_id text PRIMARY KEY)')
|
||||
|
||||
#connection.execute('ALTER TABLE package_extent OWNER TO ?',user_name);
|
||||
|
||||
connection.execute('SELECT AddGeometryColumn(\'package_extent\',\'the_geom\', %s, \'POLYGON\', 2)',srid)
|
||||
connection.execute('SELECT AddGeometryColumn(\'package_extent\',\'the_geom\', %s, \'GEOMETRY\', 2)',srid)
|
||||
|
||||
Session.commit()
|
||||
|
|
|
@ -1,22 +1,31 @@
|
|||
import os
|
||||
from logging import getLogger
|
||||
|
||||
from pylons.i18n import _
|
||||
from genshi.input import HTML
|
||||
from genshi.filters import Transformer
|
||||
|
||||
import ckan.lib.helpers as h
|
||||
|
||||
from ckan.lib.helpers import json
|
||||
|
||||
from ckan.plugins import implements, SingletonPlugin
|
||||
from ckan.plugins import IRoutes, IConfigurer
|
||||
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
|
||||
|
||||
from ckanext.spatial.lib import save_package_extent
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
class SpatialQuery(SingletonPlugin):
|
||||
|
||||
implements(IRoutes, inherit=True)
|
||||
implements(IPackageController, inherit=True)
|
||||
|
||||
def before_map(self, map):
|
||||
|
||||
|
@ -26,6 +35,88 @@ class SpatialQuery(SingletonPlugin):
|
|||
|
||||
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):
|
||||
|
@ -41,14 +132,15 @@ class WMSPreview(SingletonPlugin):
|
|||
if routes.get('controller') == 'package' and \
|
||||
routes.get('action') == 'read' and c.pkg.id:
|
||||
|
||||
is_inspire = (c.pkg.extras.get('INSPIRE') == 'True')
|
||||
# TODO: What about WFS, WCS...
|
||||
is_wms = (c.pkg.extras.get('resource-type') == 'service')
|
||||
if is_inspire and is_wms:
|
||||
for res in c.pkg.resources:
|
||||
if res.format == "WMS":
|
||||
data = {'name': c.pkg.name}
|
||||
stream = stream | Transformer('body//div[@class="resources subsection"]')\
|
||||
.append(HTML(html.MAP_VIEW % data))
|
||||
|
||||
break
|
||||
|
||||
|
||||
|
||||
return stream
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
.dataset-map {
|
||||
padding-bottom: 10px
|
||||
}
|
||||
|
||||
.olControlAttribution {
|
||||
left: 5px;
|
||||
right: inherit;
|
||||
bottom: 3px !important;
|
||||
}
|
|
@ -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
|
@ -1,20 +1,30 @@
|
|||
This is a custom build of the OpenLayers Javascript mapping library,
|
||||
slimmed down to only the features we need.
|
||||
These are custom builds of the OpenLayers Javascript mapping library,
|
||||
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
|
||||
build profile, and then run the build command from the OpenLayers
|
||||
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::
|
||||
|
||||
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
|
||||
by Development Seed under the BSD License:
|
||||
|
||||
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/
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -98,9 +98,8 @@ CKAN.WMSPreview = function($){
|
|||
olLayers.push(dummyLayer);
|
||||
|
||||
// Setup some sizes
|
||||
var w = $("#container").width() * 0.50;
|
||||
w = $("#content").width();
|
||||
if (w > 1024) w = 1024;
|
||||
$("#content").width($("#container").width());
|
||||
$("#map").width(w);
|
||||
$("#map").height(500);
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 973 B |
|
@ -6,9 +6,20 @@
|
|||
<py:def function="page_title">${c.pkg.title or c.pkg.name} - WMS preview</py:def>
|
||||
|
||||
<py:def function="optional_head">
|
||||
<link type="text/css" rel="stylesheet" media="all" href="/ckanext/spatial/wms_preview.css" />
|
||||
<script type="text/javascript" src="/ckanext/spatial/js/openlayers/OpenLayers_ckan.js"></script>
|
||||
<link type="text/css" rel="stylesheet" media="all" href="/ckanext/spatial/css/wms_preview.css" />
|
||||
</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">
|
||||
|
@ -18,9 +29,9 @@
|
|||
</h2>
|
||||
|
||||
<!-- Source URL -->
|
||||
<div class="url" py:if="c.pkg.url">
|
||||
<div class="url" py:if="c.wms_url">
|
||||
<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>
|
||||
</div>
|
||||
<p>
|
||||
|
@ -31,13 +42,6 @@
|
|||
</p>
|
||||
<div class="package subsection">
|
||||
<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="layers"></div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
GeoAlchemy>=0.6
|
||||
Shapely>=1.2.13
|
3
setup.py
3
setup.py
|
@ -1,7 +1,7 @@
|
|||
from setuptools import setup, find_packages
|
||||
import sys, os
|
||||
|
||||
version = '0.1'
|
||||
version = '0.2'
|
||||
|
||||
setup(
|
||||
name='ckanext-spatial',
|
||||
|
@ -28,6 +28,7 @@ setup(
|
|||
# Add plugins here, eg
|
||||
wms_preview=ckanext.spatial.plugin:WMSPreview
|
||||
spatial_query=ckanext.spatial.plugin:SpatialQuery
|
||||
dataset_extent_map=ckanext.spatial.plugin:DatasetExtentMap
|
||||
[paste.paster_command]
|
||||
spatial=ckanext.spatial.commands.spatial:Spatial
|
||||
""",
|
||||
|
|
Loading…
Reference in New Issue