From d5f4a3408dccab1111f5a0f80614bd7727df703c Mon Sep 17 00:00:00 2001 From: Homme Zwaagstra Date: Fri, 20 Apr 2012 16:17:32 +0100 Subject: [PATCH] Allow for multiple bounding box display on the map on the records page This fixes issue #24. This required a fundamental change to the metadata object model and portal querying capabilities to support multiple bounding boxes throughout the application. --- html/js/full/search.js | 22 +++++--- python/medin/dws.py | 30 +++++++---- python/medin/metadata.py | 60 +++++++++++----------- python/medin/query.py | 61 ++++++++++++----------- python/medin/spatial.py | 59 ++++++++++++---------- python/medin/views.py | 27 ++++++---- templates/atom/catalogue/base.xml | 6 +-- templates/common/feeds.xml | 15 ++++++ templates/full/metadata.html | 35 +++++++------ templates/full/search-common.html | 2 +- templates/full/search.html | 17 +++++-- templates/kml/catalogue/base.xml | 26 +++++++--- templates/kml/catalogue/metadata-base.kml | 34 +++++++------ templates/light/metadata.html | 39 +++++++++------ templates/light/search.html | 11 +++- templates/rss/catalogue/base.xml | 6 +-- 16 files changed, 271 insertions(+), 179 deletions(-) diff --git a/html/js/full/search.js b/html/js/full/search.js index 885a432..36837ad 100644 --- a/html/js/full/search.js +++ b/html/js/full/search.js @@ -196,7 +196,11 @@ function populate_areas() { // if there's an existing area, select it if (area) { select.filter(':visible').change(); - } else if (bbox) { + } else if (bboxes) { + var bbox = new OpenLayers.Bounds(); + for (var i = 0; i < bboxes.length; i++) { + bbox.extend(bboxes[i]); + } add_box(bbox); map.zoomToExtent(bbox); // zoom to the box } else { @@ -522,7 +526,7 @@ function check_query() { area.empty(); if (!criteria['terms'].length && !criteria['dates'].start && !criteria['dates'].end && - !criteria['area'] && !criteria['bbox']) { + !criteria['area'] && !criteria['bbox'].length) { term.append('everything in the catalogue.'); return; } else if (!criteria['terms'].length) @@ -550,12 +554,14 @@ function check_query() { if (criteria['area']) area.append(' in '+criteria['area']+''); - else if (criteria['bbox']) { - var n = criteria['bbox'][3].toFixed(2); - var s = criteria['bbox'][1].toFixed(2); - var e = criteria['bbox'][2].toFixed(2); - var w = criteria['bbox'][0].toFixed(2); - area.append(' in '+n+'N '+s+'S '+e+'E '+w+'W'); + else if (criteria['bbox'].length) { + for (var i = 0; i < criteria['bbox'].length; i++) { + var n = criteria['bbox'][i][3].toFixed(2); + var s = criteria['bbox'][i][1].toFixed(2); + var e = criteria['bbox'][i][2].toFixed(2); + var w = criteria['bbox'][i][0].toFixed(2); + area.append(' in '+n+'N '+s+'S '+e+'E '+w+'W'); + } } }, complete: function(req, status) { diff --git a/python/medin/dws.py b/python/medin/dws.py index 8a371d3..a5312df 100644 --- a/python/medin/dws.py +++ b/python/medin/dws.py @@ -218,14 +218,16 @@ class SummaryResponse(BriefResponse): def _processDocument(self, doc): - try: - extent = doc.Spatial[0].BoundingBox - bbox = [extent.LimitWest, extent.LimitSouth, extent.LimitEast, extent.LimitNorth] - except AttributeError, IndexError: - bbox = None + bboxes = [] + for extent in doc.Spatial: + try: + extent = extent.BoundingBox + bboxes.append([extent.LimitWest, extent.LimitSouth, extent.LimitEast, extent.LimitNorth]) + except (AttributeError, IndexError): + pass ret = super(SummaryResponse, self)._processDocument(doc) - ret['bbox'] = bbox + ret['bbox'] = bboxes ret['abstract'] = doc.Abstract return ret @@ -368,11 +370,21 @@ def prepareCaller(self, query, result_type, logger): # add the spatial criteria aid = query.getArea(cast=False) + boxes = [] if aid: - bbox = query.areas.getBBOX(aid) + boxes.append(query.areas.getBBOX(aid)) else: - bbox = query.getBBOX() - if bbox: + boxes.extend(query.getBoxes()) + + if boxes: + # get the total extent, as the DWS does not currently + # support multiple bounding boxes + bbox = [ + min((box[0] for box in boxes)), + min((box[1] for box in boxes)), + max((box[2] for box in boxes)), + max((box[3] for box in boxes))] + (search.SpatialSearch.BoundingBox.LimitWest, search.SpatialSearch.BoundingBox.LimitSouth, search.SpatialSearch.BoundingBox.LimitEast, diff --git a/python/medin/metadata.py b/python/medin/metadata.py index 9b4cb2e..8949ede 100644 --- a/python/medin/metadata.py +++ b/python/medin/metadata.py @@ -55,7 +55,7 @@ class Metadata(object): topic_category = None # element 9 service_type = None # element 10 keywords = None # element 11 - bbox = None # element 12 + bboxes = None # element 12 extents = None # element 13 vertical_extent = None # element 14 reference_system = None # element 15 @@ -453,7 +453,7 @@ def parse(self): m.topic_category = self.topicCategory() # element 9 m.service_type = self.serviceType() # element 10 m.keywords = self.keywords() # element 11 - m.bbox = self.bbox() # element 12 + m.bboxes = self.bboxes() # element 12 m.extents = self.extents() # element 13 m.vertical_extent = self.verticalExtent() # element 14 m.reference_system = self.referenceSystem() # element 15 @@ -647,26 +647,24 @@ def keywords(self): return keywords @_assignContext - def bbox(self): + def bboxes(self): """Element 12: Geographic Bounding Box""" - - try: - node = self.xpath.xpathEval('//gmd:EX_Extent/gmd:geographicElement/gmd:EX_GeographicBoundingBox')[0] - except IndexError: - return [] - self.xpath.setContextNode(node) - - ordinates = [] - for direction, latlon in (('west', 'longitude'), ('south', 'latitude'), ('east', 'longitude'), ('north', 'latitude')): - try: - - ordinate = self.xpath.xpathEval('./gmd:%sBound%s/gco:Decimal/text()' % (direction, latlon.capitalize()))[0].content.strip() - except IndexError: - return [] - ordinates.append(float(ordinate)) - - return ordinates + boxes = [] + for node in self.xpath.xpathEval('//gmd:EX_Extent/gmd:geographicElement/gmd:EX_GeographicBoundingBox'): + self.xpath.setContextNode(node) + + ordinates = [] + + for direction, latlon in (('west', 'longitude'), ('south', 'latitude'), ('east', 'longitude'), ('north', 'latitude')): + try: + + ordinate = self.xpath.xpathEval('./gmd:%sBound%s/gco:Decimal/text()' % (direction, latlon.capitalize()))[0].content.strip() + except IndexError: + return [] + ordinates.append(float(ordinate)) + boxes.append(tuple(ordinates)) + return boxes @_assignContext def extents(self): @@ -1252,13 +1250,17 @@ def vocab2row(vocab, default=None): writer.writerows(iter_element_values(11, 'Keywords', metadata.keywords)) - row = metadata.bbox + row = metadata.bboxes + boxes = [] if row and not isinstance(row, Exception): - row = [['West', metadata.bbox[0]], - ['South', metadata.bbox[1]], - ['East', metadata.bbox[2]], - ['North', metadata.bbox[3]]] - writer.writerows(iter_element_values(12, 'Geographic extent', row)) + for box in row: + boxes.extend( + [['West', box[0]], + ['South', box[1]], + ['East', box[2]], + ['North', box[3]]] + ) + writer.writerows(iter_element_values(12, 'Geographic extent', boxes)) row = metadata.extents if row and not isinstance(row, Exception): @@ -1270,10 +1272,10 @@ def vocab2row(vocab, default=None): row = metadata.reference_system if row and not isinstance(row, Exception): tmp = [] - for key in ('identifier', 'source', 'url', 'name', 'scope'): - if not row[key]: + for key in ('identifier', 'name', 'type', 'scope'): + if not hasattr(row, key): continue - tmp.append([key.capitalize(), row[key]]) + tmp.append([key.capitalize(), getattr(row, key)]) row = tmp writer.writerows(iter_element_values(15, 'Spatial reference system', row)) diff --git a/python/medin/query.py b/python/medin/query.py index bf26cba..eca09e1 100644 --- a/python/medin/query.py +++ b/python/medin/query.py @@ -119,7 +119,7 @@ def verify(self): errors.append('The start date cannot be greater than the end date') try: - self.getBBOX() + self.getBoxes() except QueryError, e: errors.append(str(e)) @@ -201,41 +201,44 @@ def asDate(self, key, cast, default, is_start): if cast: return dt return date - def getBBOX(self, cast=True, default=''): + def getBoxes(self, cast=True, default=''): try: - bbox = self['bbox'][0] + bboxes = self['bbox'] except KeyError, AttributeError: return default if not cast: - return bbox - - bbox = bbox.split(',', 3) - if not len(bbox) == 4: - if self.raise_errors: - raise QueryError('The bounding box must be in the format minx,miny,maxx,maxy') - return default - try: - bbox = [float(n) for n in bbox] - except ValueError: - if self.raise_errors: - raise QueryError('The bounding box must consist of numbers') - return default + return bboxes + + boxes = [] + for bbox in bboxes: + bbox = bbox.split(',', 3) + if not len(bbox) == 4: + if self.raise_errors: + raise QueryError('The bounding box must be in the format minx,miny,maxx,maxy') + return default + try: + bbox = [float(n) for n in bbox] + except ValueError: + if self.raise_errors: + raise QueryError('The bounding box must consist of numbers') + return default - if bbox[0] > bbox[2]: - if self.raise_errors: - raise QueryError('The bounding box east value is less than the west') - return default + if bbox[0] > bbox[2]: + if self.raise_errors: + raise QueryError('The bounding box east value is less than the west') + return default - if bbox[1] > bbox[3]: - if self.raise_errors: - raise QueryError('The bounding box north value is less than the south') - return default + if bbox[1] > bbox[3]: + if self.raise_errors: + raise QueryError('The bounding box north value is less than the south') + return default - return tuple(bbox) + boxes.append(tuple(bbox)) + return boxes - def setBBOX(self, bbox): - self['bbox'] = ','.join((str(i) for i in box)) + def setBoxes(self, bboxes): + self['bbox'] = [','.join((str(i) for i in box)) for box in bboxes] def getSort(self, cast=True, default=''): try: @@ -376,8 +379,8 @@ def asDict(self, verify=True): a['dates'] = dates # add the area - bbox = self.getBBOX(default=False) - a['bbox'] = bbox + bboxes = self.getBoxes(default=False) + a['bbox'] = bboxes a['area'] = self.getArea(default=None) return a diff --git a/python/medin/spatial.py b/python/medin/spatial.py index 7ab909a..d924ad5 100644 --- a/python/medin/spatial.py +++ b/python/medin/spatial.py @@ -198,40 +198,47 @@ def tilecache (environ, start_response): return wsgiHandler(environ, start_response, _tilecache_service) -def metadata_image(bbox, mapfile): +def metadata_image(bboxes, mapfile): """Create a metadata image""" from json import dumps as tojson import mapnik - minx, miny, maxx, maxy = bbox - - # create the bounding box as a json string - width, height = (maxx - minx, maxy - miny) - min_dim = 0.0125 # minimum dimension for display as a rectangle (in degrees) - if width < min_dim or height < min_dim: # it should be a point - feature = { "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [minx, miny] - }, - "properties": { - "type": "point" + features = [] + + for bbox in bboxes: + minx, miny, maxx, maxy = bbox + + # create the bounding box as a json string + width, height = (maxx - minx, maxy - miny) + min_dim = 0.0125 # minimum dimension for display as a rectangle (in degrees) + if width < min_dim or height < min_dim: # it should be a point + feature = { "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [minx, miny] + }, + "properties": { + "type": "point" + } } - } - width, height = (9, 9) - else: - feature = { "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [[[minx, miny], [maxx, miny], [maxx, maxy], [minx, maxy], [minx, miny]]] - }, - "properties": { - "type": "bbox" + width, height = (9, 9) + else: + feature = { "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [[[minx, miny], [maxx, miny], [maxx, maxy], [minx, maxy], [minx, miny]]] + }, + "properties": { + "type": "bbox" + } } - } + features.append(feature) - json = tojson(feature) + json = tojson({ + "type": "FeatureCollection", + "features": features + }) # instantiate the map m = mapnik.Map(250, 250) diff --git a/python/medin/views.py b/python/medin/views.py index 3fd88e9..1fde5de 100644 --- a/python/medin/views.py +++ b/python/medin/views.py @@ -349,7 +349,7 @@ def setup(self, environ): count = q.getCount(default=None) # We need to get the number of hits for the query. search_term = q.getSearchTerm(cast=False) sort = q.getSort(cast=False) - bbox = q.getBBOX() + bboxes = q.getBoxes() start_date = q.getStartDate(cast=False) end_date = q.getEndDate(cast=False) area = q.getArea(cast=False) @@ -402,7 +402,7 @@ def setup(self, environ): data_formats=formats, resource_types=resources, access_types=access, - bbox=bbox) + bboxes=bboxes) headers = [('Etag', etag), # propagate the result update time to the HTTP layer ('Cache-Control', 'no-cache, must-revalidate')] @@ -712,11 +712,18 @@ def __call__(self, environ, start_response): ] if result['bbox']: + # get the total extent of multiple bounding boxes + bbox = [ + min((box[0] for box in result['bbox'])), + min((box[1] for box in result['bbox'])), + max((box[2] for box in result['bbox'])), + max((box[3] for box in result['bbox']))] + row.extend([ - result['bbox'][0], - result['bbox'][2], - result['bbox'][1], - result['bbox'][3]]) + bbox[0], + bbox[2], + bbox[1], + bbox[3]]) else: row.extend(['', '', '', '']) @@ -919,7 +926,7 @@ def setup(self, environ): if parser: title = parser.title() tvars = dict(gid=parser.uid, - bbox=parser.bbox(), + bboxes=parser.bboxes(), author=parser.author(), abstract=parser.abstract()) else: @@ -995,8 +1002,8 @@ def __call__(self, environ, start_response): headers.extend([('Etag', etag), ('Cache-Control', 'no-cache, must-revalidate')]) - bbox = parser.bbox() - if not bbox: + bboxes = parser.bboxes() + if not bboxes: raise HTTPError('404 Not Found', 'The metadata record does not have a geographic bounding box') # ensure the background raster datasource has been created @@ -1010,7 +1017,7 @@ def __call__(self, environ, start_response): mapfile = template.render(root_dir=environ.root) # create the image - image = medin.spatial.metadata_image(bbox, mapfile) + image = medin.spatial.metadata_image(bboxes, mapfile) # serialise the image bytes = image.tostring('png') diff --git a/templates/atom/catalogue/base.xml b/templates/atom/catalogue/base.xml index b5dfe19..1668712 100644 --- a/templates/atom/catalogue/base.xml +++ b/templates/atom/catalogue/base.xml @@ -23,7 +23,7 @@ You can obtain a full copy of the RPL from http://opensource.org/licenses/rpl1.5.txt or geodata@soton.ac.uk \ -<%namespace import="isoformat, content, description" file="/common/feeds.xml"/><%! +<%namespace import="isoformat, content, description, bboxes2georss" file="/common/feeds.xml"/><%! template_ = "" %> ${script_root}/${self.attr.template}/catalogue/${result['id'] | x} ${content(result)} - %if result['bbox']: - ${result['bbox'][1]} ${result['bbox'][0]} ${result['bbox'][1]} ${result['bbox'][2]} ${result['bbox'][3]} ${result['bbox'][2]} ${result['bbox'][3]} ${result['bbox'][0]} ${result['bbox'][1]} ${result['bbox'][0]} - %endif + ${bboxes2georss(result['bbox'])} % endfor % if not results: diff --git a/templates/common/feeds.xml b/templates/common/feeds.xml index 7bacc23..0d0fbb1 100644 --- a/templates/common/feeds.xml +++ b/templates/common/feeds.xml @@ -49,6 +49,21 @@ MEDIN catalogue entries. % endif +<%def name="bboxes2georss(bboxes)"> +<% +bbox = None +if bboxes: + bbox = [ + min((box[0] for box in bboxes)), + min((box[1] for box in bboxes)), + max((box[2] for box in bboxes)), + max((box[3] for box in bboxes))] +%> + %if bbox: + ${bbox[1]} ${bbox[0]} ${bbox[1]} ${bbox[2]} ${bbox[3]} ${bbox[2]} ${bbox[3]} ${bbox[0]} ${bbox[1]} ${bbox[0]} + %endif + + <%def name="content(entry)"> <strong>Originator:</strong> ${entry['originator'] | x}<br/> <strong>Last updated:</strong> ${dateformat(entry['updated']) | x} diff --git a/templates/full/metadata.html b/templates/full/metadata.html index 8cd8549..4b9bbfd 100644 --- a/templates/full/metadata.html +++ b/templates/full/metadata.html @@ -301,21 +301,30 @@

Coordinate Reference System

% endif \ -% if metadata.bbox: +<%def name="format_bboxes(bboxes)">\ + The metadata covers the following areas: + + + % if len(bboxes) > 1: + Search for all areas listed above. + % endif +\ + +% if metadata.bboxes:

Geographic Extent

Geographic bounding box

- The metadata covers the area West - East from ${metadata.bbox[0]}° to - ${metadata.bbox[2]}° and North - South from ${metadata.bbox[3]}° to - ${metadata.bbox[1]}°. - - Search for all metadata within - this area. + ${format_bboxes(metadata.bboxes)}

Details

% endif\ - % if metadata.bbox: + % if metadata.bboxes: @@ -565,11 +574,7 @@

Details

<%self:output_element element="${metadata.online_resource}" number="${12}"> - ${metadata.bbox[0]}°W, - ${metadata.bbox[2]}°E, ${metadata.bbox[3]}°N, - ${metadata.bbox[1]}°S + ${format_bboxes(metadata.bboxes)}
diff --git a/templates/full/search-common.html b/templates/full/search-common.html index b61b8cb..00388b6 100644 --- a/templates/full/search-common.html +++ b/templates/full/search-common.html @@ -56,7 +56,7 @@

You are searching for...

%if criteria['area']: in ${criteria['area']} %elif criteria['bbox']: - in ${'%.2f' % criteria['bbox'][3]}N ${'%.2f' % criteria['bbox'][1]}S ${'%.2f' % criteria['bbox'][2]}E ${'%.2f' % criteria['bbox'][0]}W + in ${', '.join(['%.2fN %.2fS %.2fE %.2fW' % (box[3], box[1], box[2], box[0]) for box in criteria['bbox']])} %endif

${hits} diff --git a/templates/full/search.html b/templates/full/search.html index ab6bab7..13dec4d 100644 --- a/templates/full/search.html +++ b/templates/full/search.html @@ -80,10 +80,10 @@