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

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

Made csmlGrapher configurable using the same config file as station2kml.

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