source: DPPP/kml/csml2kml/python/csml2kmlpylon/csml2kmlpylon/controllers/csmlGrapher.py @ 3633

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

Finished listing of features according to collection times (only those features are listed which are available during the whole interval. Also, add more detailed error messages.

Line 
1# Pylons-specific imports
2import logging
3from csml2kmlpylon.lib.base import *
4from paste.httpexceptions import HTTPBadRequest, HTTPNotFound
5
6# Other imports
7import Image
8import pylab
9from pylab import *
10from datetime import datetime, timedelta
11import re
12import urllib
13from cStringIO import StringIO
14from tempfile import NamedTemporaryFile
15from cElementTree import Element, SubElement, ElementTree, XML
16from urllib import quote
17
18import csml
19import csml2kml.Station
20from csml2kml.utils import wget
21
22log = logging.getLogger(__name__)
23
24class CsmlgrapherController(BaseController):
25
26    def __call__(self, environ, start_response):
27        '''
28        Initialise the web service by overriding a method that always get called upon controller construction.
29        More specifically, read in the config file.
30        '''
31
32        # The name of the config file is set in the server's "development.ini" file.
33        configFileName = config['app_conf']['csmlGrapher.configfile']
34       
35        # Load the configuration XML element (but only the part pertaining to the web service)
36        grapherConfig = (ElementTree().parse(configFileName)).find('GrapherWebService')
37
38        # Set the configurable variables
39        self.displayIntervalStart = dates.dateutil.parser.parse(grapherConfig.find('DisplayIntervalStart').text)
40        self.displayIntervalEnd = dates.dateutil.parser.parse(grapherConfig.find('DisplayIntervalEnd').text)
41        self.geoServerUrl = grapherConfig.find('GeoServerURL').text
42
43        # Call the __call__ method of the parent class
44        return BaseController.__call__(self, environ, start_response)
45
46    def _datetimeToGeoServerDate(self, datetime):
47        monthCodes = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']
48        return repr(datetime.day) + '-' + monthCodes[datetime.month-1] + '-' + repr(datetime.year)
49
50    def _retrieveCsmlPointSeriesFeature(self, station_name, feature_id):
51        '''
52        @return: A C{PointSeriesFeature} object representing the single CSML feature if the feature was found;
53        C{None} if a feature collection has been returned, but contains no CSML features (this happens when
54        there are no measured time points in the used time interval).
55        @throws: A C{LookupError} exception with a message if there is no response from the server.
56        '''
57       
58        # Prepare the request for the GeoServer.
59        geoServerRequestUrl = urllib.quote(self.geoServerUrl + '?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>'+ self._datetimeToGeoServerDate(self.displayIntervalStart) +'</ogc:Literal></ogc:LowerBoundary><ogc:UpperBoundary><ogc:Literal>'+ self._datetimeToGeoServerDate(self.displayIntervalEnd) +'</ogc:Literal></ogc:UpperBoundary></ogc:PropertyIsBetween></ogc:And></ogc:Filter>', '/._:?&=')
60
61        # Acquire the GeoServer response and parse it into a CSMLFeatureCollection object
62        geoServerResponse = wget(geoServerRequestUrl)
63        if not geoServerResponse:
64            raise LookupError('Cannot acquire response from server (wrong URL or server down)')
65        csmlFeatureCollection = csml.parser.CSMLFeatureCollection()
66        csmlFeatureCollection.fromXML( XML(geoServerResponse) )
67
68        # If csmlFeatureCollection contains a single CSML feature, the isolate it. If it contains no feature,
69        # probably no data points are available for that interval.
70        try:
71            csmlFeature = csmlFeatureCollection.featureMembers
72        except AttributeError:
73            raise LookupError(
74                'No data points available for the given time interval. ' + 
75                'Perhaps the interval is shorter than the frequency of data points, ' +
76                'or collection of data for this feature was interrupted at the time.'
77                )
78
79        # Make sure that the feature is a PointSeriesFeature
80        if not isinstance(csmlFeature, csml.parser.PointSeriesFeature):
81            raise LookupError('CSML feature not a PointSeriesFeature')
82        csmlPointSeriesFeature = csmlFeature
83
84        return csmlPointSeriesFeature
85
86    def plot(self):
87        '''
88        Handler for plotting a specific PointSeries feature given by URL parameter "feature_id".
89        Retrieves the data from a web service, which should preferably run on the same machine.
90
91        Request parameters:
92                               feature_id      Unique identifier of the requested CSML PointSeries feature.
93                               station_name    Name of the requested station.
94                               
95        Response:              An image/png of the time series for the CSML PointSeries feature.
96        '''
97        def _plot_feature(feature):
98            '''
99            Plot a PointSeries feature using matplotlib, into a temporary file
100            '''
101           
102            # Make sure that time is considered to be UTC time. This is because all CSML data is supposed to be in UTC,
103            # but actually pylab is "time zone aware" and will interpret the time as local time zone time.
104            # Not quite sure if this would be a problem, but rather enforce it is UTC.
105            def _enforce_UTC_timezone(datestr):
106                if datestr[-1] == 'Z':
107                    return datestr
108                else:
109                    return datestr + 'Z'
110           
111            # Isolate time points   
112            times=feature.value.pointSeriesDomain.timePositionList.CONTENT.split()
113            times = map(_enforce_UTC_timezone, times)
114           
115            # Note the start and end date (they are actually date+time), and time span between them
116            start_date = dates.dateutil.parser.parse(times[0])
117            end_date = dates.dateutil.parser.parse(times[-1])
118            span = datestr2num(times[-1]) - datestr2num(times[0])
119
120            # Make a list of times as float numbers being days passed since start of epoch (here 01-01-0001)
121            elapsed_times = map(datestr2num, times)
122
123            # Generate tick locator and formatter; these determine the time axis: locator determines tick positions
124            # and formatter format of time
125            (tickLocator, tickFormatter) = dates.date_ticker_factory(span, numticks=8)
126
127            # We may need to do this for bodc data (i.e. not inline data)
128            # vals=feature.value.rangeSet.valueArray.valueComponent.insertedExtract.components.getDataFromChunks(0,19)
129
130            # Get values of the measured quantity
131            ql = feature.value.rangeSet.quantityList
132            vals = map(float, ql.CONTENT.split())
133
134            # Determine the units of measurement
135            uom=ql.uom.title()
136            if ql.uom.islower():
137                uom = uom.lower()
138            if ql.uom.isupper():
139                uom = uom.upper()
140
141            # Make sure the times are sorted in ascending order (and rearrange the values accordingly)
142            sortTimesExplicitly = True
143            if sortTimesExplicitly:
144                valuesAtTimes = {}
145                for i in range(len(elapsed_times)):
146                    valuesAtTimes[elapsed_times[i]] = vals[i]
147                    elapsed_times.sort()
148                for i in range(len(elapsed_times)):
149                    vals[i] = valuesAtTimes[elapsed_times[i]]
150
151            # Prepare the plot the figure (actual plotting actions are postponed until save)
152            fig = figure()
153            plot_date(elapsed_times, vals, 'b-', xdate=True, lw=1)
154            plot_date(elapsed_times, vals, 'go', markeredgecolor = 'g', xdate=True, lw=2)
155            ax = gca()
156            ax.xaxis.set_major_locator(tickLocator)
157            ax.xaxis.set_major_formatter(tickFormatter)
158            fig.autofmt_xdate()  # show times without overlaps
159            time_format = '%d-%b-%Y %H:%M:%S UTC'
160            xlabel('Times between %s and %s ' % (start_date.strftime(time_format), end_date.strftime(time_format)))
161            ylabel('Values [%s]' % uom)
162            title('"%s" (%s)' % (feature.id, feature.description.CONTENT))
163            grid(True)
164
165            # Save the figure to a temporary file
166            tempFile = NamedTemporaryFile(suffix='.png')
167            savefig(tempFile.name)
168           
169            return tempFile
170
171        def _set_response(tempFile):
172            '''
173            Set the WSGI response to an image, containing image read from a temporary location.
174            '''
175            img = Image.open(tempFile.name)
176            buf = StringIO()
177            img.save(buf, 'PNG')
178            response.content_type = 'image/png'
179            response.content = buf.getvalue()
180           
181        #----------------------
182
183        # Get parameters from the request object
184        try:
185            feature_id = str(request.params['feature_id'])     # convert back from Unicode
186            station_name = str(request.params['station_name']) # convert back from Unicode
187        except KeyError:
188            raise HTTPBadRequest('Parameters "feature_id" and "station_name" must be supplied.')
189
190        # Try to retrieve the csmlPointSeriesFeature object
191        try:
192            csmlPointSeriesFeature = self._retrieveCsmlPointSeriesFeature(station_name, feature_id)
193        except LookupError, e:
194            errorMessage = e.args[0]
195            httpNotFound = HTTPNotFound()
196            httpNotFound.explanation = ''
197            httpNotFound.detail = errorMessage
198            raise httpNotFound
199
200        # Try to plot the feature into a temporary file, and put the contents of that file into the response
201        try:
202            tempFile = _plot_feature(csmlPointSeriesFeature)    # plot the feature into a temporary file
203            _set_response(tempFile)                             # set the response as an image containing the plot
204        finally:
205            try:
206                tempFile.close()
207            except NameError:
208                pass  # tempFile undefined -- no need to close
209
210    def list(self):
211        '''
212        Handler for generating a list of CSML features per station, with dynamic links to the plot service.
213        Request parameters:
214                               station_name    Name of the requested station.
215                               
216        Response:              A text/html with the page containing links to the plot service on the same server.
217        '''
218
219        # Get parameters from the request object
220        try:
221            station_name = str(request.params['station_name']) # convert back from Unicode
222        except KeyError:
223            raise HTTPBadRequest('Parameter "station_name" must be supplied.')
224
225        # Prepare the request for the GeoServer -- to return a collection of np:Station, containing a single np:Station,
226        # which contains a list of CSML features.
227        geoServerRequestUrl = urllib.quote(self.geoServerUrl + '?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>', '/._:?&=')
228
229        # Acquire the GeoServer response and parse it into a WFSStationCollection object
230        geoServerResponse = wget(geoServerRequestUrl)
231        if not geoServerResponse:
232            raise LookupError('Cannot acquire response from server (wrong URL or server down)')
233        wfsStationCollection = csml2kml.Station.WFSStationCollection()
234        wfsStationCollection.parseString(geoServerResponse)
235        if len(wfsStationCollection.stations) != 1:
236            raise ValueError('Multiple stations match OGC selection filter (only one must match)')
237        wfsStation = wfsStationCollection.stations[0]
238
239        # Generate HTML with a list of CSML features contained in the station.
240        # Currently, this is done simply by directly generating HTML (i.e. no template was used).
241        htmlElement = Element('html')
242        SubElement(htmlElement, 'title').text = 'List of CSML features for station ' + station_name
243        bodyElement = SubElement(htmlElement, 'body')
244        SubElement(bodyElement, 'h2').text = 'List of CSML features for station ' + station_name
245        tableElement = SubElement(bodyElement, 'table')
246        tableElement.set('border', '1')
247        headingRowElement = SubElement(tableElement, 'tr')
248        SubElement(headingRowElement, 'th').text = 'Feature name'
249        SubElement(headingRowElement, 'th').text = 'Data collected since*'
250        SubElement(headingRowElement, 'th').text = 'Data collected until* '
251        for stationFeature in wfsStation.stationFeatures:
252            if self.displayIntervalStart >= stationFeature.collectBeginDate and self.displayIntervalEnd <= stationFeature.collectEndDate:
253                rowElement = SubElement(tableElement, 'tr')
254                featureNameElement = SubElement(rowElement, 'td')
255                anchorElement = SubElement(featureNameElement, 'a')
256                linkToGrapher = 'http://bond.badc.rl.ac.uk:8089/csmlGrapher/plot?station_name=' + station_name + '&feature_id=' + stationFeature.featureId
257                anchorElement.set('href', linkToGrapher)
258                anchorElement.text = stationFeature.featureId
259                SubElement(rowElement, 'td').text = self._datetimeToGeoServerDate(stationFeature.collectBeginDate)
260                SubElement(rowElement, 'td').text = self._datetimeToGeoServerDate(stationFeature.collectEndDate)
261
262        htmlStringIO = StringIO()
263        ElementTree(htmlElement).write(htmlStringIO)
264
265        response.content_type = 'text/html'
266        response.content = htmlStringIO.getvalue()
Note: See TracBrowser for help on using the repository browser.