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

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

Added the "csml2kmlpylon" Pylons server as a replacement of the improperly named "pylonsstack". Copied the csmlGrapher.py controller to the new Pylon.

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            # Make sure the times are sorted in ascending order (and rearrange the values accordingly)
132            sortTimesExplicitly = True
133            if sortTimesExplicitly:
134                valuesAtTimes = {}
135                for i in range(len(elapsed_times)):
136                    valuesAtTimes[elapsed_times[i]] = vals[i]
137                    elapsed_times.sort()
138                for i in range(len(elapsed_times)):
139                    vals[i] = valuesAtTimes[elapsed_times[i]]
140
141            # Prepare the plot the figure (actual plotting actions are postponed until save)
142            fig = figure()
143            plot_date(elapsed_times, vals, 'b-', xdate=True, lw=1)
144            plot_date(elapsed_times, vals, 'go', markeredgecolor = 'g', xdate=True, lw=2)
145            ax = gca()
146            ax.xaxis.set_major_locator(tickLocator)
147            ax.xaxis.set_major_formatter(tickFormatter)
148            fig.autofmt_xdate()  # show times without overlaps
149            time_format = '%d-%b-%Y %H:%M:%S UTC'
150            xlabel('Times between %s and %s ' % (start_date.strftime(time_format), end_date.strftime(time_format)))
151            ylabel('Values [%s]' % uom)
152            title('"%s" (%s)' % (feature.id, feature.description.CONTENT))
153            grid(True)
154
155            # Save the figure to a temporary file
156            tempFile = NamedTemporaryFile(suffix='.png')
157            savefig(tempFile.name)
158           
159            return tempFile
160
161        def _set_response(tempFile):
162            '''
163            Set the WSGI response to an image, containing image read from a temporary location.
164            '''
165            img = Image.open(tempFile.name)
166            buf = StringIO()
167            img.save(buf, 'PNG')
168            response.content_type = 'image/png'
169            response.content = buf.getvalue()
170           
171        #----------------------
172
173        # Get parameters from the request object
174        try:
175            feature_id = str(request.params['feature_id'])     # convert back from Unicode
176            station_name = str(request.params['station_name']) # convert back from Unicode
177        except KeyError:
178            raise HTTPBadRequest('Parameters "feature_id" and "station_name" must be supplied.')
179
180        # Retrieve the csmlPointSeriesFeature object
181        csmlPointSeriesFeature = self._retrieveCsmlPointSeriesFeature(station_name, feature_id)
182
183        # Try to plot the feature into a temporary file, and put the contents of that file into the response
184        try:
185            tempFile = _plot_feature(csmlPointSeriesFeature)    # plot the feature into a temporary file
186            _set_response(tempFile)                             # set the response as an image containing the plot
187        finally:
188            try:
189                tempFile.close()
190            except NameError:
191                pass  # tempFile undefined -- no need to close
192
193    def list(self):
194        '''
195        Handler for generating a list of CSML features per station, with dynamic links to the plot service.
196        Request parameters:
197                               station_name    Name of the requested station.
198                               
199        Response:              A text/html with the page containing links to the plot service on the same server.
200        '''
201
202        # Get parameters from the request object
203        try:
204            station_name = str(request.params['station_name']) # convert back from Unicode
205        except KeyError:
206            raise HTTPBadRequest('Parameter "station_name" must be supplied.')
207
208        # Prepare the request for the GeoServer -- to return a collection of np:Station, containing a single np:Station,
209        # which contains a list of CSML features.
210        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>', '/._:?&=')
211
212        # Acquire the GeoServer response and parse it into a WFSStationCollection object
213        geoServerResponse = wget(geoServerRequestUrl)
214        if not geoServerResponse:
215            raise LookupError('Cannot acquire response from server (wrong URL or server down)')
216        wfsStationCollection = csml2kml.Station.WFSStationCollection()
217        wfsStationCollection.parseString(geoServerResponse)
218        if len(wfsStationCollection.stations) != 1:
219            raise ValueError('Multiple stations match OGC selection filter (only one must match)')
220        wfsStation = wfsStationCollection.stations[0]
221
222        # Generate HTML with a list of CSML features contained in the station.
223        htmlElement = Element('html')
224        SubElement(htmlElement, 'title').text = 'List of CSML features for station ' + station_name
225        bodyElement = SubElement(htmlElement, 'body')
226        SubElement(htmlElement, 'h2').text = 'List of CSML features for station ' + station_name
227        tableElement = SubElement(htmlElement, 'table')
228        tableElement.set('border', '1')
229        for csmlFeatureId in wfsStation.csmlFeatureIds:
230            try:
231                # Test whether the feature exists. Remove this if such test is no more necessary.
232                self._retrieveCsmlPointSeriesFeature(station_name, csmlFeatureId)
233
234                # If no LookupError exception has been caught, proceed with inluding the feature in the list.
235                rowElement = SubElement(tableElement, 'tr')
236                dataElement = SubElement(rowElement, 'td')
237                anchorElement = SubElement(dataElement, 'a')
238                linkToGrapher = 'http://bond.badc.rl.ac.uk:8089/csmlGrapher/plot?station_name=' + station_name + '&feature_id=' + csmlFeatureId
239                anchorElement.set('href', linkToGrapher)
240                anchorElement.text = csmlFeatureId
241            except LookupError:
242                pass
243
244        htmlStringIO = StringIO()
245        ElementTree(htmlElement).write(htmlStringIO)
246
247        response.content_type = 'text/html'
248        response.content = htmlStringIO.getvalue()
Note: See TracBrowser for help on using the repository browser.