import os import re import mimetypes from logging import getLogger from pylons import config from ckan import plugins as p from ckan.lib.helpers import json def check_geoalchemy_requirement(): '''Checks if a suitable geoalchemy version installed Checks if geoalchemy2 is present when using CKAN >= 2.3, and raises an ImportError otherwise so users can upgrade manually. ''' msg = ('This version of ckanext-spatial requires {0}. ' + 'Please install it by running `pip install {0}`.\n' + 'For more details see the "Troubleshooting" section of the ' + 'install documentation') if p.toolkit.check_ckan_version(min_version='2.3'): try: import geoalchemy2 except ImportError: raise ImportError(msg.format('geoalchemy2')) else: try: import geoalchemy except ImportError: raise ImportError(msg.format('geoalchemy')) check_geoalchemy_requirement() log = getLogger(__name__) def package_error_summary(error_dict): ''' Do some i18n stuff on the error_dict keys ''' def prettify(field_name): field_name = re.sub('(? 180): bbox['minx'] -= 360 bbox['maxx'] -= 360 if self.search_backend == 'solr': search_params = self._params_for_solr_search(bbox, search_params) elif self.search_backend == 'solr-spatial-field': search_params = self._params_for_solr_spatial_field_search(bbox, search_params) elif self.search_backend == 'postgis': search_params = self._params_for_postgis_search(bbox, search_params) return search_params def _params_for_solr_search(self, bbox, search_params): ''' This will add the following parameters to the query: defType - edismax (We need to define EDisMax to use bf) bf - {function} A boost function to influence the score (thus influencing the sorting). The algorithm can be basically defined as: 2 * X / Q + T Where X is the intersection between the query area Q and the target geometry T. It gives a ratio from 0 to 1 where 0 means no overlap at all and 1 a perfect fit fq - Adds a filter that force the value returned by the previous function to be between 0 and 1, effectively applying the spatial filter. ''' variables =dict( x11=bbox['minx'], x12=bbox['maxx'], y11=bbox['miny'], y12=bbox['maxy'], x21='minx', x22='maxx', y21='miny', y22='maxy', area_search = abs(bbox['maxx'] - bbox['minx']) * abs(bbox['maxy'] - bbox['miny']) ) bf = '''div( mul( mul(max(0, sub(min({x12},{x22}) , max({x11},{x21}))), max(0, sub(min({y12},{y22}) , max({y11},{y21}))) ), 2), add({area_search}, mul(sub({y22}, {y21}), sub({x22}, {x21}))) )'''.format(**variables).replace('\n','').replace(' ','') search_params['fq_list'] = ['{!frange incl=false l=0 u=1}%s' % bf] search_params['bf'] = bf search_params['defType'] = 'edismax' return search_params def _params_for_solr_spatial_field_search(self, bbox, search_params): ''' This will add an fq filter with the form: +spatial_geom:"Intersects(ENVELOPE({minx}, {miny}, {maxx}, {maxy})) ''' search_params['fq_list'] = search_params.get('fq_list', []) search_params['fq_list'].append('+spatial_geom:"Intersects(ENVELOPE({minx}, {maxx}, {maxy}, {miny}))"' .format(minx=bbox['minx'], miny=bbox['miny'], maxx=bbox['maxx'], maxy=bbox['maxy'])) return search_params def _params_for_postgis_search(self, bbox, search_params): from ckanext.spatial.lib import bbox_query, bbox_query_ordered from ckan.lib.search import SearchError # Note: This will be deprecated at some point in favour of the # Solr 4 spatial sorting capabilities if search_params.get('sort') == 'spatial desc' and \ p.toolkit.asbool(config.get('ckanext.spatial.use_postgis_sorting', 'False')): if search_params['q'] or search_params['fq']: raise SearchError('Spatial ranking cannot be mixed with other search parameters') # ...because it is too inefficient to use SOLR to filter # results and return the entire set to this class and # after_search do the sorting and paging. extents = bbox_query_ordered(bbox) are_no_results = not extents search_params['extras']['ext_rows'] = search_params['rows'] search_params['extras']['ext_start'] = search_params['start'] # this SOLR query needs to return no actual results since # they are in the wrong order anyway. We just need this SOLR # query to get the count and facet counts. rows = 0 search_params['sort'] = None # SOLR should not sort. # Store the rankings of the results for this page, so for # after_search to construct the correctly sorted results rows = search_params['extras']['ext_rows'] = search_params['rows'] start = search_params['extras']['ext_start'] = search_params['start'] search_params['extras']['ext_spatial'] = [ (extent.package_id, extent.spatial_ranking) \ for extent in extents[start:start+rows]] else: extents = bbox_query(bbox) are_no_results = extents.count() == 0 if are_no_results: # 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','').strip() or '""' 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 def after_search(self, search_results, search_params): from ckan.lib.search import PackageSearchQuery # Note: This will be deprecated at some point in favour of the # Solr 4 spatial sorting capabilities if search_params.get('extras', {}).get('ext_spatial') and \ p.toolkit.asbool(config.get('ckanext.spatial.use_postgis_sorting', 'False')): # Apply the spatial sort querier = PackageSearchQuery() pkgs = [] for package_id, spatial_ranking in search_params['extras']['ext_spatial']: # get package from SOLR pkg = querier.get_index(package_id)['data_dict'] pkgs.append(json.loads(pkg)) search_results['results'] = pkgs return search_results class HarvestMetadataApi(p.SingletonPlugin): ''' Harvest Metadata API (previously called "InspireApi") A way for a user to view the harvested metadata XML, either as a raw file or styled to view in a web browser. ''' p.implements(p.IRoutes) def before_map(self, route_map): controller = "ckanext.spatial.controllers.api:HarvestMetadataApiController" # Showing the harvest object content is an action of the default # harvest plugin, so just redirect there route_map.redirect('/api/2/rest/harvestobject/{id:.*}/xml', '/harvest/object/{id}', _redirect_code='301 Moved Permanently') route_map.connect('/harvest/object/{id}/original', controller=controller, action='display_xml_original') route_map.connect('/harvest/object/{id}/html', controller=controller, action='display_html') route_map.connect('/harvest/object/{id}/html/original', controller=controller, action='display_html_original') # Redirect old URL to a nicer and unversioned one route_map.redirect('/api/2/rest/harvestobject/:id/html', '/harvest/object/{id}/html', _redirect_code='301 Moved Permanently') return route_map def after_map(self, route_map): return route_map