source: DPPP/kml/csml2kml/python/pylonsstack/pylonsstack/controllers/csmlGrapher.py @ 3581

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/DPPP/kml/csml2kml/python/pylonsstack/pylonsstack/controllers/csmlGrapher.py@3581
Revision 3581, 11.2 KB checked in by mkochan, 13 years ago (diff)

Added tabular formatting to the csmlGrapher lister. Added testing of existence of CSML features per station -- this is neccessary at the moment, due to a "feature" in GeoServer?.

Line 
1# Pylons-specific imports
2import logging
3from pylonsstack.lib.base import *
4
5# Other imports
6import Image
7import pylab
8from pylab import *
9from datetime import datetime, timedelta
10import re
11import urllib
12from cStringIO import StringIO
13from tempfile import NamedTemporaryFile
14from cElementTree import Element, SubElement, ElementTree, XML
15from urllib import quote
16
17import csml
18import csml2kml.Station
19from csml2kml.utils import wget
20
21log = logging.getLogger(__name__)
22
23# [TODO] . Add security (especially checking of input parameters).
24#        . Add configurable URL for the data source web service (preferably explicitly on localhost)
25class CsmlgrapherController(BaseController):
26
27    def _retrieveCsmlPointSeriesFeature(self, station_name, feature_id):
28        '''
29        @return: A PointSeriesFeature object representing the single CSML feature.
30        @throws: A LookupError exception on retrieval error.
31        '''
32
33        # The time boundaries are hard-coded now, i.e. we run the service for demonstration purposes.
34        lowerTimeBoundary = '1-JAN-2003'
35        upperTimeBoundary = '31-JAN-2003'
36
37        # Prepare the request for the GeoServer.
38        geoServerRequestUrl = urllib.quote('http://130.246.76.98:8084/geoserver/wfs?request=getfeature&service=wfs&version=1.1.0&typename=csml:PointSeriesFeature&filter=<ogc:Filter xmlns:ogc="http://www.opengis.net/ogc" xmlns:gml="http://www.opengis.net/gml"><ogc:And><ogc:PropertyIsEqualTo><ogc:PropertyName>gml:description</ogc:PropertyName><ogc:Literal>'+ station_name +'</ogc:Literal></ogc:PropertyIsEqualTo><ogc:PropertyIsEqualTo><ogc:PropertyName>csml:parameter/swe:Phenomenon/gml:name</ogc:PropertyName><ogc:Literal>'+ feature_id +'</ogc:Literal></ogc:PropertyIsEqualTo><ogc:PropertyIsBetween><ogc:PropertyName>csml:value/csml:PointSeriesCoverage/csml:pointSeriesDomain/csml:TimeSeries/csml:timePositionList</ogc:PropertyName><ogc:LowerBoundary><ogc:Literal>'+ lowerTimeBoundary +'</ogc:Literal></ogc:LowerBoundary><ogc:UpperBoundary><ogc:Literal>'+ upperTimeBoundary +'</ogc:Literal></ogc:UpperBoundary></ogc:PropertyIsBetween></ogc:And></ogc:Filter>', '/._:?&=')
39
40        # Acquire the GeoServer response and parse it into a CSMLFeatureCollection object
41        geoServerResponse = wget(geoServerRequestUrl)
42        if not geoServerResponse:
43            raise LookupError('Cannot acquire response from server (wrong URL or server down)')
44        csmlFeatureCollection = csml.parser.CSMLFeatureCollection()
45        csmlFeatureCollection.fromXML( XML(geoServerResponse) )
46
47        # Now, csmlFeatureCollection should only contain a single CSML feature (if the feature was found).
48        # Isolate that CSML feature.
49        try:
50            csmlFeature = csmlFeatureCollection.featureMembers
51        except AttributeError:
52            raise LookupError('No PointSeriesFeature with ID "' + feature_id + '" found in the collection.')
53
54        # Make sure that the feature is a PointSeriesFeature
55        if not isinstance(csmlFeature, csml.parser.PointSeriesFeature):
56            raise TypeError('CSML feature not a PointSeriesFeature')
57        csmlPointSeriesFeature = csmlFeature
58
59        return csmlPointSeriesFeature
60
61    def plot(self):
62        '''
63        Handler for plotting a specific PointSeries feature given by URL parameter "feature_id".
64        Retrieves the data from a web service, which should preferably run on the same machine.
65
66        Request parameters:    EITHER "feature_id" OR "station_id"
67        where:
68                               feature_id    Unique identifier of a requested CSML PointSeries feature.
69                               station_id    Unique identifier of a requested station.
70
71        Response:              If feature_id is supplied, an image/png of the time series for the CSML PointSeries feature;
72                               if station_id is supplied, nothing.
73        '''
74        def _plot_feature(feature):
75            '''
76            Plot a PointSeries feature using matplotlib, into a temporary file
77            '''
78           
79            # Make sure that time is considered to be UTC time. This is because all CSML data is supposed to be in UTC,
80            # but actually pylab is "time zone aware" and will interpret the time as local time zone time.
81            # Not quite sure if this would be a problem, but rather enforce it is UTC.
82            def _enforce_UTC_timezone(datestr):
83                if datestr[-1] == 'Z':
84                    return datestr
85                else:
86                    return datestr + 'Z'
87           
88            # Isolate time points   
89            times=feature.value.pointSeriesDomain.timePositionList.CONTENT.split()
90            times = map(_enforce_UTC_timezone, times)
91           
92            # Note the start and end date (they are actually date+time), and time span between them
93            start_date = dates.dateutil.parser.parse(times[0])
94            end_date = dates.dateutil.parser.parse(times[-1])
95            span = datestr2num(times[-1]) - datestr2num(times[0])
96
97            # Make a list of times as float numbers being days passed since start of epoch (here 01-01-0001)
98            elapsed_times = map(datestr2num, times)
99
100            # Generate tick locator and formatter; these determine the time axis: locator determines tick positions
101            # and formatter format of time
102            (tickLocator, tickFormatter) = dates.date_ticker_factory(span, numticks=8)
103
104            # We may need to do this for bodc data (i.e. not inline data)
105            # vals=feature.value.rangeSet.valueArray.valueComponent.insertedExtract.components.getDataFromChunks(0,19)
106
107            # Get values of the measured quantity
108            ql = feature.value.rangeSet.quantityList
109            vals = map(float, ql.CONTENT.split())
110
111            # Determine the units of measurement
112            uom=ql.uom.title()
113            if ql.uom.islower():
114                uom = uom.lower()
115            if ql.uom.isupper():
116                uom = uom.upper()
117
118            # Prepare the plot the figure (actual plotting actions are postponed until save)
119            fig = figure()
120            plot_date(elapsed_times, vals, 'b-', xdate=True, lw=1)
121            plot_date(elapsed_times, vals, 'go', markeredgecolor = 'g', xdate=True, lw=2)
122            ax = gca()
123            ax.xaxis.set_major_locator(tickLocator)
124            ax.xaxis.set_major_formatter(tickFormatter)
125            fig.autofmt_xdate()  # show times without overlaps
126            time_format = '%d-%b-%Y %H:%M:%S UTC'
127            xlabel('Times between %s and %s ' % (start_date.strftime(time_format), end_date.strftime(time_format)))
128            ylabel('Values [%s]' % uom)
129            title('"%s" (%s)' % (feature.id, feature.description.CONTENT))
130            grid(True)
131
132            # Save the figure to a temporary file
133            tempFile = NamedTemporaryFile(suffix='.png')
134            savefig(tempFile.name)
135           
136            return tempFile
137
138        def _set_response(tempFile):
139            '''
140            Set the WSGI response to an image, containing image read from a temporary location.
141            '''
142            img = Image.open(tempFile.name)
143            buf = StringIO()
144            img.save(buf, 'PNG')
145            response.content_type = 'image/png'
146            response.content = buf.getvalue()
147           
148        #----------------------
149
150        # Get parameters from the request object
151        try:
152            feature_id = str(request.params['feature_id'])     # convert back from Unicode
153            station_name = str(request.params['station_name']) # convert back from Unicode
154        except KeyError:
155            raise HTTPBadRequest('Parameters "feature_id" and "station_name" must be supplied.')
156
157        # Retrieve the csmlPointSeriesFeature object
158        csmlPointSeriesFeature = self._retrieveCsmlPointSeriesFeature(station_name, feature_id)
159
160        # Try to plot the feature into a temporary file, and put the contents of that file into the response
161        try:
162            tempFile = _plot_feature(csmlPointSeriesFeature)    # plot the feature into a temporary file
163            _set_response(tempFile)                             # set the response as an image containing the plot
164        finally:
165            try:
166                tempFile.close()
167            except NameError:
168                pass  # tempFile undefined -- no need to close
169
170    def list(self):
171        '''Handler for generating a list of CSML features per station, with dynamic links to the plot service.'''
172
173        # Get parameters from the request object
174        try:
175            station_name = str(request.params['station_name']) # convert back from Unicode
176        except KeyError:
177            raise HTTPBadRequest('Parameter "station_name" must be supplied.')
178
179        # Prepare the request for the GeoServer -- to return a collection of np:Station, containing a single np:Station,
180        # which contains a list of CSML features.
181        geoServerRequestUrl = urllib.quote('http://130.246.76.98:8084/geoserver/wfs?request=getFeature&service=wfs&version=1.1.0&typename=np:Station&filter=<ogc:Filter xmlns:ogc="http://www.opengis.net/ogc" xmlns:gml="http://www.opengis.net/gml"><ogc:PropertyIsEqualTo><ogc:PropertyName>stationName</ogc:PropertyName><ogc:Literal>' + station_name + '</ogc:Literal></ogc:PropertyIsEqualTo></ogc:Filter>', '/._:?&=')
182
183        # Acquire the GeoServer response and parse it into a WFSStationCollection object
184        geoServerResponse = wget(geoServerRequestUrl)
185        if not geoServerResponse:
186            raise LookupError('Cannot acquire response from server (wrong URL or server down)')
187        wfsStationCollection = csml2kml.Station.WFSStationCollection()
188        wfsStationCollection.parseString(geoServerResponse)
189        if len(wfsStationCollection.stations) != 1:
190            raise ValueError('Multiple stations match OGC selection filter (only one must match)')
191        wfsStation = wfsStationCollection.stations[0]
192
193        # Generate HTML with a list of CSML features contained in the station.
194        htmlElement = Element('html')
195        SubElement(htmlElement, 'title').text = 'List of CSML features for station ' + station_name
196        bodyElement = SubElement(htmlElement, 'body')
197        SubElement(htmlElement, 'h2').text = 'List of CSML features for station ' + station_name
198        tableElement = SubElement(htmlElement, 'table')
199        tableElement.set('border', '1')
200        for csmlFeatureId in wfsStation.csmlFeatureIds:
201            try:
202                # Test whether the feature exists. Remove this if such test is no more necessary.
203                self._retrieveCsmlPointSeriesFeature(station_name, csmlFeatureId)
204
205                # If no LookupError exception has been caught, proceed with inluding the feature in the list.
206                rowElement = SubElement(tableElement, 'tr')
207                dataElement = SubElement(rowElement, 'td')
208                anchorElement = SubElement(dataElement, 'a')
209                linkToGrapher = 'http://bond.badc.rl.ac.uk:8089/csmlGrapher/plot?station_name=' + station_name + '&feature_id=' + csmlFeatureId
210                anchorElement.set('href', linkToGrapher)
211                anchorElement.text = csmlFeatureId
212            except LookupError:
213                pass
214
215        htmlStringIO = StringIO()
216        ElementTree(htmlElement).write(htmlStringIO)
217
218        response.content_type = 'text/html'
219        response.content = htmlStringIO.getvalue()
Note: See TracBrowser for help on using the repository browser.