source: trunk/src/java/uk/ac/rdg/resc/ncwms/controller/AbstractMetadataController.java @ 939

Revision 939, 17.2 KB checked in by guygriffiths, 9 months ago (diff)

Fixed bug where getMinMax was using the wrong coordinates as x and y

Line 
1/*
2 * Copyright (c) 2007 The University of Reading
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 *    notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 *    notice, this list of conditions and the following disclaimer in the
12 *    documentation and/or other materials provided with the distribution.
13 * 3. Neither the name of the University of Reading, nor the names of the
14 *    authors or contributors may be used to endorse or promote products
15 *    derived from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29package uk.ac.rdg.resc.ncwms.controller;
30
31import java.util.ArrayList;
32import java.util.HashMap;
33import java.util.LinkedHashMap;
34import java.util.List;
35import java.util.Map;
36import javax.servlet.http.HttpServletRequest;
37import javax.servlet.http.HttpServletResponse;
38import org.joda.time.DateTime;
39import org.joda.time.DateTimeZone;
40import org.joda.time.Duration;
41import org.joda.time.Period;
42import org.slf4j.Logger;
43import org.slf4j.LoggerFactory;
44import org.springframework.web.servlet.ModelAndView;
45import uk.ac.rdg.resc.edal.coverage.grid.RegularGrid;
46import uk.ac.rdg.resc.edal.util.Range;
47import uk.ac.rdg.resc.edal.util.Ranges;
48import uk.ac.rdg.resc.ncwms.controller.AbstractWmsController.LayerFactory;
49import uk.ac.rdg.resc.ncwms.exceptions.LayerNotDefinedException;
50import uk.ac.rdg.resc.ncwms.exceptions.MetadataException;
51import uk.ac.rdg.resc.ncwms.graphics.ColorPalette;
52import uk.ac.rdg.resc.ncwms.usagelog.UsageLogEntry;
53import uk.ac.rdg.resc.ncwms.util.WmsUtils;
54import uk.ac.rdg.resc.ncwms.wms.Layer;
55import uk.ac.rdg.resc.ncwms.wms.ScalarLayer;
56import uk.ac.rdg.resc.ncwms.wms.VectorLayer;
57
58/**
59 * Controller that handles all requests for non-standard metadata by the
60 * Godiva2 site.  Eventually Godiva2 will be changed to accept standard
61 * metadata (i.e. fragments of GetCapabilities)... maybe.
62 *
63 * @author Jon Blower
64 */
65public abstract class AbstractMetadataController
66{
67    private static final Logger log = LoggerFactory.getLogger(AbstractMetadataController.class);
68
69    private final LayerFactory layerFactory;
70
71    protected AbstractMetadataController(LayerFactory layerFactory)
72    {
73        this.layerFactory = layerFactory;
74    }
75   
76    public ModelAndView handleRequest(HttpServletRequest request,
77        HttpServletResponse response, UsageLogEntry usageLogEntry)
78        throws MetadataException
79    {
80        try
81        {
82            String item = request.getParameter("item");
83            usageLogEntry.setWmsOperation("GetMetadata:" + item);
84            if (item == null)
85            {
86                throw new Exception("Must provide an ITEM parameter");
87            }
88            else if (item.equals("menu"))
89            {
90                return this.showMenu(request, usageLogEntry);
91            }
92            else if (item.equals("layerDetails"))
93            {
94                return this.showLayerDetails(request, usageLogEntry);
95            }
96            else if (item.equals("timesteps"))
97            {
98                return this.showTimesteps(request);
99            }
100            else if (item.equals("minmax"))
101            {
102                return this.showMinMax(request, usageLogEntry);
103            }
104            else if (item.equals("animationTimesteps"))
105            {
106                return this.showAnimationTimesteps(request);
107            }
108            throw new Exception("Invalid value for ITEM parameter");
109        }
110        catch(Exception e)
111        {
112            // Wrap all exceptions in a MetadataException.  These will be automatically
113            // displayed via displayMetadataException.jsp, in JSON format
114            throw new MetadataException(e);
115        }
116    }
117   
118    /**
119     * Shows the hierarchy of layers available from this server, or a pre-set
120     * hierarchy.  May differ between implementations
121     */
122    protected abstract ModelAndView showMenu(HttpServletRequest request, UsageLogEntry usageLogEntry) throws Exception;
123   
124    /**
125     * Shows an JSON document containing the details of the given variable (units,
126     * zvalues, tvalues etc).  See showLayerDetails.jsp.
127     */
128    private ModelAndView showLayerDetails(HttpServletRequest request,
129        UsageLogEntry usageLogEntry) throws Exception
130    {
131        Layer layer = this.getLayer(request);
132        usageLogEntry.setLayer(layer);
133       
134        // Find the time the user has requested (this is the time that is
135        // currently displayed on the Godiva2 site).  If no time has been
136        // specified we use the current time
137        DateTime targetDateTime = new DateTime(layer.getChronology());
138        String targetDateIso = request.getParameter("time");
139        if (targetDateIso != null && !targetDateIso.trim().equals(""))
140        {
141            try
142            {
143                targetDateTime = WmsUtils.iso8601ToDateTime(targetDateIso, layer.getChronology());
144            }
145            catch(IllegalArgumentException iae)
146            {
147                // targetDateIso was not valid for the layer's chronology
148                // We swallow this exception: targetDateTime will remain
149                // unchanged.
150            }
151        }
152       
153        Map<Integer, Map<Integer, List<Integer>>> datesWithData =
154            new LinkedHashMap<Integer, Map<Integer, List<Integer>>>();
155        List<DateTime> timeValues = layer.getTimeValues();
156        DateTime nearestDateTime = timeValues.isEmpty() ? new DateTime(0) : timeValues.get(0);
157       
158        // Takes an array of time values for a layer and turns it into a Map of
159        // year numbers to month numbers to day numbers, for use in
160        // showVariableDetails.jsp.  This is used to provide a list of days for
161        // which we have data.  Also calculates the nearest value on the time axis
162        // to the time we're currently displaying on the web interface.
163        for (DateTime dateTime : layer.getTimeValues())
164        {
165            // We must make sure that dateTime() is in UTC or getDayOfMonth() etc
166            // might return unexpected results
167            dateTime = dateTime.withZone(DateTimeZone.UTC);
168            // See whether this dateTime is closer to the target dateTime than
169            // the current closest value
170            long d1 = new Duration(dateTime, targetDateTime).getMillis();
171            long d2 = new Duration(nearestDateTime, targetDateTime).getMillis();
172            if (Math.abs(d1) < Math.abs(d2)) nearestDateTime = dateTime;
173
174            int year = dateTime.getYear();
175            Map<Integer, List<Integer>> months = datesWithData.get(year);
176            if (months == null)
177            {
178                months = new LinkedHashMap<Integer, List<Integer>>();
179                datesWithData.put(year, months);
180            }
181            // We need to subtract 1 from the month number as Javascript months
182            // are 0-based (Joda-time months are 1-based).  This retains
183            // compatibility with previous behaviour.
184            int month = dateTime.getMonthOfYear() - 1;
185            List<Integer> days = months.get(month);
186            if (days == null)
187            {
188                days = new ArrayList<Integer>();
189                months.put(month, days);
190            }
191            int day = dateTime.getDayOfMonth();
192            if (!days.contains(day)) days.add(day);
193        }
194       
195        Map<String, Object> models = new HashMap<String, Object>();
196        models.put("layer", layer);
197        models.put("datesWithData", datesWithData);
198        models.put("nearestTimeIso", WmsUtils.dateTimeToISO8601(nearestDateTime));
199        // The names of the palettes supported by this layer.  Actually this
200        // will be the same for all layers, but we can't put this in the menu
201        // because there might be several menu JSPs.
202        models.put("paletteNames", ColorPalette.getAvailablePaletteNames());
203        return new ModelAndView("showLayerDetails", models);
204    }
205   
206    /**
207     * @return the Layer that the user is requesting, throwing an
208     * Exception if it doesn't exist or if there was a problem reading from the
209     * data store.
210     */
211    private Layer getLayer(HttpServletRequest request) throws LayerNotDefinedException
212    {
213        String layerName = request.getParameter("layerName");
214        if (layerName == null)
215        {
216            throw new LayerNotDefinedException("null");
217        }
218        return this.layerFactory.getLayer(layerName);
219    }
220   
221    /**
222     * Finds all the timesteps that occur on the given date, which will be provided
223     * in the form "2007-10-18".
224     */
225    private ModelAndView showTimesteps(HttpServletRequest request)
226        throws Exception
227    {
228        Layer layer = getLayer(request);
229        if (layer.getTimeValues().isEmpty()) return null; // return no data if no time axis present
230       
231        String dayStr = request.getParameter("day");
232        if (dayStr == null)
233        {
234            throw new Exception("Must provide a value for the day parameter");
235        }
236        DateTime date = WmsUtils.iso8601ToDateTime(dayStr, layer.getChronology());
237       
238        // List of date-times that fall on this day
239        List<DateTime> timesteps = new ArrayList<DateTime>();
240        // Search exhaustively through the layer's valid time values
241        // TODO: inefficient: should stop once last day has been found.
242        for (DateTime tVal : layer.getTimeValues())
243        {
244            if (onSameDay(tVal, date))
245            {
246                timesteps.add(tVal);
247            }
248        }
249        log.debug("Found {} timesteps on {}", timesteps.size(), dayStr);
250       
251        return new ModelAndView("showTimesteps", "timesteps", timesteps);
252    }
253   
254    /**
255     * @return true if the two given DateTimes fall on the same day.
256     */
257    private static boolean onSameDay(DateTime dt1, DateTime dt2)
258    {
259        // We must make sure that the DateTimes are both in UTC or the field
260        // comparisons will not do what we expect
261        dt1 = dt1.withZone(DateTimeZone.UTC);
262        dt2 = dt2.withZone(DateTimeZone.UTC);
263        boolean onSameDay = dt1.getYear() == dt2.getYear()
264                         && dt1.getMonthOfYear() == dt2.getMonthOfYear()
265                         && dt1.getDayOfMonth() == dt2.getDayOfMonth();
266        log.debug("onSameDay({}, {}) = {}", new Object[]{dt1, dt2, onSameDay});
267        return onSameDay;
268    }
269   
270    /**
271     * Shows an XML document containing the minimum and maximum values for the
272     * tile given in the parameters.
273     */
274    private ModelAndView showMinMax(HttpServletRequest request,
275        UsageLogEntry usageLogEntry) throws Exception
276    {
277        RequestParams params = new RequestParams(request.getParameterMap());
278        // We only need the bit of the GetMap request that pertains to data extraction
279        // TODO: the hard-coded "1.1.1" is ugly: it basically means that the
280        // GetMapDataRequest object will look for "SRS" instead of "CRS"
281        GetMapDataRequest dr = new GetMapDataRequest(params, "1.1.1");
282       
283        // Get the variable we're interested in
284        Layer layer = this.layerFactory.getLayer(dr.getLayers()[0]);
285        usageLogEntry.setLayer(layer);
286       
287        // Get the grid onto which the data is being projected
288        RegularGrid grid = WmsUtils.getImageGrid(dr);
289       
290        // Get the value on the z axis
291        double zValue = AbstractWmsController.getElevationValue(dr.getElevationString(), layer);
292       
293        // Get the requested timestep (taking the first only if an animation is requested)
294        List<DateTime> timeValues = AbstractWmsController.getTimeValues(dr.getTimeString(), layer);
295        DateTime tValue = timeValues.isEmpty() ? null : timeValues.get(0);
296       
297        // Now read the data and calculate the minimum and maximum values
298        List<Float> magnitudes;
299        if (layer instanceof ScalarLayer)
300        {
301            magnitudes = ((ScalarLayer)layer).readHorizontalPoints(tValue, zValue, grid);
302        }
303        else if (layer instanceof VectorLayer)
304        {
305            VectorLayer vecLayer = (VectorLayer)layer;
306            List<Float> east = vecLayer.getEastwardComponent().readHorizontalPoints(tValue, zValue, grid);
307            List<Float> north = vecLayer.getNorthwardComponent().readHorizontalPoints(tValue, zValue, grid);
308            magnitudes = WmsUtils.getMagnitudes(east, north);
309        }
310        else
311        {
312            throw new IllegalStateException("Invalid Layer type");
313        }
314
315        Range<Float> valueRange = Ranges.findMinMax(magnitudes);
316        return new ModelAndView("showMinMax", "valueRange", valueRange);
317    }
318
319    /**
320     * Calculates the TIME strings necessary to generate animations for the
321     * given layer at hourly, daily, weekly, monthly and yearly resolution.
322     * @param request
323     * @return
324     * @throws java.lang.Exception
325     */
326    private ModelAndView showAnimationTimesteps(HttpServletRequest request)
327        throws Exception
328    {
329        Layer layer = this.getLayer(request);
330        String startStr = request.getParameter("start");
331        String endStr = request.getParameter("end");
332        if (startStr == null || endStr == null)
333        {
334            throw new Exception("Must provide values for start and end");
335        }
336
337        // Find the start and end indices along the time axis
338        int startIndex = AbstractWmsController.findTIndex(startStr, layer);
339        int endIndex = AbstractWmsController.findTIndex(endStr, layer);
340        List<DateTime> tValues = layer.getTimeValues();
341
342        // E.g.: {
343        //  "Full" : "start/end",
344        //  "Hourly" : "t1,t2,t3,t4",
345        //  "Daily" : "ta,tb,tc,td"
346        //  etc
347        //  }
348        Map<String, String> timeStrings = new LinkedHashMap<String, String>();
349
350        timeStrings.put("Full (" + (endIndex - startIndex + 1) + " frames)", startStr + "/" + endStr);
351        addTimeString("Daily", timeStrings, tValues, startIndex, endIndex, new Period().withDays(1));
352        addTimeString("Weekly", timeStrings, tValues, startIndex, endIndex, new Period().withWeeks(1));
353        addTimeString("Monthly", timeStrings, tValues, startIndex, endIndex, new Period().withMonths(1));
354        addTimeString("Bi-monthly", timeStrings, tValues, startIndex, endIndex, new Period().withMonths(2));
355        addTimeString("Twice-yearly", timeStrings, tValues, startIndex, endIndex, new Period().withMonths(6));
356        addTimeString("Yearly", timeStrings, tValues, startIndex, endIndex, new Period().withYears(1));
357
358        return new ModelAndView("showAnimationTimesteps", "timeStrings", timeStrings);
359    }
360
361    private static void addTimeString(String label, Map<String, String> timeStrings,
362        List<DateTime> tValues, int startIndex, int endIndex, Period resolution)
363    {
364        List<DateTime> timesteps = getAnimationTimesteps(tValues, startIndex, endIndex, resolution);
365        // We filter out all the animations with less than one timestep
366        if (timesteps.size() > 1)
367        {
368            String timeString = getTimeString(timesteps);
369            timeStrings.put(label + " (" + timesteps.size() + " frames)", timeString);
370        }
371    }
372
373    private static List<DateTime> getAnimationTimesteps(List<DateTime> tValues, int startIndex,
374        int endIndex, Period resolution)
375    {
376        List<DateTime> times = new ArrayList<DateTime>();
377        times.add(tValues.get(startIndex));
378        for (int i = startIndex + 1; i <= endIndex; i++)
379        {
380            DateTime lastdt = times.get(times.size() - 1);
381            DateTime thisdt = tValues.get(i);
382            if (!thisdt.isBefore(lastdt.plus(resolution)))
383            {
384                times.add(thisdt);
385            }
386        }
387        return times;
388    }
389
390    private static String getTimeString(List<DateTime> timesteps)
391    {
392        if (timesteps.size() == 0) return "";
393        StringBuilder builder = new StringBuilder(WmsUtils.dateTimeToISO8601(timesteps.get(0)));
394        for (int i = 1; i < timesteps.size(); i++)
395        {
396            builder.append("," + WmsUtils.dateTimeToISO8601(timesteps.get(i)));
397        }
398        return builder.toString();
399    }
400   
401}
Note: See TracBrowser for help on using the repository browser.