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

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

Tested and setup functioning of csml2kmlpylon using relocated config file.

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