| 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 |
|
|---|
| 29 | package uk.ac.rdg.resc.ncwms.controller;
|
|---|
| 30 |
|
|---|
| 31 | import java.util.ArrayList;
|
|---|
| 32 | import java.util.HashMap;
|
|---|
| 33 | import java.util.LinkedHashMap;
|
|---|
| 34 | import java.util.List;
|
|---|
| 35 | import java.util.Map;
|
|---|
| 36 | import javax.servlet.http.HttpServletRequest;
|
|---|
| 37 | import javax.servlet.http.HttpServletResponse;
|
|---|
| 38 | import org.joda.time.DateTime;
|
|---|
| 39 | import org.joda.time.DateTimeZone;
|
|---|
| 40 | import org.joda.time.Duration;
|
|---|
| 41 | import org.joda.time.Period;
|
|---|
| 42 | import org.slf4j.Logger;
|
|---|
| 43 | import org.slf4j.LoggerFactory;
|
|---|
| 44 | import org.springframework.web.servlet.ModelAndView;
|
|---|
| 45 | import uk.ac.rdg.resc.edal.coverage.grid.RegularGrid;
|
|---|
| 46 | import uk.ac.rdg.resc.edal.util.Range;
|
|---|
| 47 | import uk.ac.rdg.resc.edal.util.Ranges;
|
|---|
| 48 | import uk.ac.rdg.resc.ncwms.controller.AbstractWmsController.LayerFactory;
|
|---|
| 49 | import uk.ac.rdg.resc.ncwms.exceptions.LayerNotDefinedException;
|
|---|
| 50 | import uk.ac.rdg.resc.ncwms.exceptions.MetadataException;
|
|---|
| 51 | import uk.ac.rdg.resc.ncwms.graphics.ColorPalette;
|
|---|
| 52 | import uk.ac.rdg.resc.ncwms.usagelog.UsageLogEntry;
|
|---|
| 53 | import uk.ac.rdg.resc.ncwms.util.WmsUtils;
|
|---|
| 54 | import uk.ac.rdg.resc.ncwms.wms.Layer;
|
|---|
| 55 | import uk.ac.rdg.resc.ncwms.wms.ScalarLayer;
|
|---|
| 56 | import 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 | */
|
|---|
| 65 | public 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 | }
|
|---|