/*
* Copyright 1998-2014 University Corporation for Atmospheric Research/Unidata
*
* Portions of this software were developed by the Unidata Program at the
* University Corporation for Atmospheric Research.
*
* Access and use of this software shall impose the following obligations
* and understandings on the user. The user is granted the right, without
* any fee or cost, to use, copy, modify, alter, enhance and distribute
* this software, and any derivative works thereof, and its supporting
* documentation for any purpose whatsoever, provided that this entire
* notice appears in all copies of the software, derivative works and
* supporting documentation. Further, UCAR requests that the user credit
* UCAR/Unidata in any publications that result from the use of this
* software or in any product that includes this software. The names UCAR
* and/or Unidata, however, may not be used in any advertising or publicity
* to endorse or promote any products or commercial entity unless specific
* written permission is obtained from UCAR/Unidata. The user also
* understands that UCAR/Unidata is not obligated to provide the user with
* any support, consulting, training or assistance of any kind with regard
* to the use, operation and performance of this software nor to provide
* the user with any updates, revisions, new versions or "bug fixes."
*
* THIS SOFTWARE IS PROVIDED BY UCAR/UNIDATA "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL UCAR/UNIDATA BE LIABLE FOR ANY SPECIAL,
* INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
* FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
* NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
* WITH THE ACCESS, USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package thredds.server.ncss.controller;
import thredds.server.ncss.exception.*;
import thredds.server.ncss.exception.UnsupportedOperationException;
import thredds.server.ncss.params.NcssParamsBean;
import thredds.server.ncss.util.NcssRequestUtils;
import thredds.servlet.ThreddsConfig;
import ucar.ma2.InvalidRangeException;
import ucar.ma2.Range;
import ucar.nc2.NetcdfFileWriter;
import ucar.nc2.dataset.CoordinateAxis1D;
import ucar.nc2.dt.GridCoordSystem;
import ucar.nc2.dt.GridDatatype;
import ucar.nc2.dt.grid.CFGridWriter2;
import ucar.nc2.dt.grid.GridDataset;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.unidata.geoloc.LatLonPointImpl;
import ucar.unidata.geoloc.LatLonRect;
import ucar.unidata.geoloc.ProjectionRect;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.List;
import java.util.Random;
/**
* @author mhermida
*
*/
class GridResponder extends GridDatasetResponder {
static private final short ESTIMATED_C0MPRESION_RATE = 5; // Compression rate used to estimate the filesize of netcdf4 compressed files
public static GridResponder factory(GridDataset gds, String requestPathInfo) {
return new GridResponder(gds, requestPathInfo);
}
///////////////////////////////////////////////////////////////////////////////
private GridDataset gds;
private String requestPathInfo;
private GridResponder(GridDataset gds, String requestPathInfo) {
this.gds = gds;
this.requestPathInfo = requestPathInfo;
}
/**
*
* Returns the resulting file
*/
File getResponseFile(HttpServletResponse response, NcssParamsBean params,
NetcdfFileWriter.Version version)
throws NcssException, InvalidRangeException, ParseException, IOException {
if (!checkRequestedVars(gds, params) && params.getVertCoord() != null ) { // LOOK should catch validation error earlier
throw new UnsupportedOperationException("The variables requested: " + params.getVar() +
" have different vertical levels. Grid requests with vertCoord must have variables with same vertical levels.");
}
File netcdfResult;
if (isSpatialSubset(params)) {
netcdfResult = writeLatLonSubset(params, version);
} else {
netcdfResult = writeCoordinatesSubset(params, response, version);
}
return netcdfResult;
}
private boolean isSpatialSubset(NcssParamsBean params) throws InvalidBBOXException {
boolean spatialSubset = false;
int contValid = 0;
if (params.getNorth() != null)
contValid++;
if (params.getSouth() != null)
contValid++;
if (params.getEast() != null)
contValid++;
if (params.getWest() != null)
contValid++;
if (contValid == 4) {
if (params.getNorth() < params.getSouth()) {
throw new InvalidBBOXException("Invalid bbox. Bounding Box must have north > south");
}
if (params.getEast() < params.getWest()) {
throw new InvalidBBOXException("Invalid bbox. Bounding Box must have east > west; if crossing 180 meridian, use east boundary > 180");
}
spatialSubset = true;
} else {
if (contValid > 0)
throw new InvalidBBOXException("Invalid bbox. All params north, south, east and west must be provided");
else { // no bbox provided --> is spatialSubsetting
if (params.getMaxx() == null && params.getMinx() == null
&& params.getMaxy() == null && params.getMiny() == null)
spatialSubset = true;
}
}
return spatialSubset;
}
private File writeLatLonSubset(NcssParamsBean params, NetcdfFileWriter.Version version) throws RequestTooLargeException,
OutOfBoundariesException, InvalidRangeException, ParseException,
IOException, VariableNotContainedInDatasetException,
InvalidBBOXException, TimeOutOfWindowException {
LatLonRect maxBB = gds.getBoundingBox();
LatLonRect requestedBB = setBBForRequest(params, gds);
boolean hasBB = !ucar.nc2.util.Misc.closeEnough(requestedBB.getUpperRightPoint().getLatitude(), maxBB.getUpperRightPoint().getLatitude())
|| !ucar.nc2.util.Misc.closeEnough(requestedBB.getLowerLeftPoint().getLatitude(), maxBB.getLowerLeftPoint().getLatitude())
|| !ucar.nc2.util.Misc.closeEnough(requestedBB.getUpperRightPoint().getLongitude(), maxBB.getUpperRightPoint().getLongitude())
|| !ucar.nc2.util.Misc.closeEnough(requestedBB.getLowerLeftPoint().getLongitude(), maxBB.getLowerLeftPoint().getLongitude());
// Don't check this...
// if (checkBB(maxBB, requestedBB)) {
Range zRange = null;
// Request with zRange --> adds a limitation: only variables with the
// same vertical level???
if (params.getVertCoord() != null || params.getVertStride() > 1)
zRange = getZRange(gds, params.getVertCoord(), params.getVertStride(), params.getVar());
List<CalendarDate> wantedDates = getRequestedDates(gds, params);
CalendarDateRange wantedDateRange = null;
if (!wantedDates.isEmpty())
wantedDateRange = CalendarDateRange.of(wantedDates.get(0), wantedDates.get(wantedDates.size() - 1));
return writeGridFile(gds, params.getVar(), hasBB ? requestedBB : null, null, params.getHorizStride(), zRange, wantedDateRange,
params.getTimeStride(), params.isAddLatLon(), version);
}
private File writeCoordinatesSubset(NcssParamsBean params, HttpServletResponse response, NetcdfFileWriter.Version version)
throws OutOfBoundariesException, ParseException,
InvalidRangeException, RequestTooLargeException, IOException,
InvalidBBOXException, TimeOutOfWindowException {
// Check coordinate params: maxx, maxy, minx, miny
Double minx = params.getMinx();
Double maxx = params.getMaxx();
Double miny = params.getMiny();
Double maxy = params.getMaxy();
int contValid = 0;
if (minx != null)
contValid++;
if (maxx != null)
contValid++;
if (miny != null)
contValid++;
if (maxy != null)
contValid++;
if (contValid == 4) {
if (minx > maxx) {
throw new InvalidBBOXException("Invalid bbox. Bounding Box must have minx < maxx");
}
if (miny > maxy) {
throw new InvalidBBOXException("Invalid bbox. Bounding Box must have miny < maxy");
}
} else {
throw new InvalidBBOXException("Invalid bbox. All params minx, maxx. miny, maxy must be provided");
}
ProjectionRect rect = new ProjectionRect(minx, miny, maxx, maxy);
Range zRange = null;
// Request with zRange --> adds a limitation: only variables with the
// same vertical level???
if (params.getVertCoord() != null || params.getVertStride() > 1)
zRange = getZRange(gds, params.getVertCoord(), params.getVertStride(), params.getVar());
List<CalendarDate> wantedDates = getRequestedDates(gds, params);
CalendarDateRange wantedDateRange = CalendarDateRange.of(
wantedDates.get(0), wantedDates.get(wantedDates.size() - 1));
return writeGridFile(gds, params.getVar(), null, rect, params.getHorizStride(), zRange, wantedDateRange, 1, params.isAddLatLon(), version);
}
private File writeGridFile(GridDataset gds, List<String> vars, LatLonRect bbox, ProjectionRect projRect, Integer horizStride,
Range zRange, CalendarDateRange dateRange, Integer timeStride, boolean addLatLon, NetcdfFileWriter.Version version)
throws RequestTooLargeException, InvalidRangeException, IOException {
long maxFileDownloadSize = ThreddsConfig.getBytes("NetcdfSubsetService.maxFileDownloadSize", -1L);
if (maxFileDownloadSize > 0) {
long estimatedSize = CFGridWriter2.makeSizeEstimate(gds, vars, bbox, projRect, horizStride, zRange, dateRange, timeStride, addLatLon);
if (version == NetcdfFileWriter.Version.netcdf4)
estimatedSize /= ESTIMATED_C0MPRESION_RATE;
if (estimatedSize > maxFileDownloadSize)
throw new RequestTooLargeException("NCSS response too large = " + estimatedSize + " max = " + maxFileDownloadSize);
}
Random random = new Random(System.currentTimeMillis());
int randomInt = random.nextInt();
String filename = NcssRequestUtils.getFileNameForResponse(requestPathInfo, version);
String pathname = Integer.toString(randomInt) + "/" + filename;
File ncFile = NcssDiskCache.getInstance().getDiskCache().getCacheFile(pathname);
if(ncFile == null)
throw new IllegalStateException("NCSS misconfigured cache = ");
String cacheFilename = ncFile.getPath();
//String url = buildCacheUrl(pathname);
//httpHeaders.set("Content-Location", url);
//httpHeaders.set("Content-Disposition", "attachment; filename=\""
// + filename + "\"");
NetcdfFileWriter writer = NetcdfFileWriter.createNew(version, cacheFilename, null); // default chunking - let user control at some point
CFGridWriter2.writeFile(gds, vars, bbox, projRect, horizStride, zRange, dateRange, timeStride, addLatLon, writer);
return new File(cacheFilename);
}
private LatLonRect setBBForRequest(NcssParamsBean params, GridDataset gds) throws InvalidBBOXException {
if (params.getNorth() == null && params.getSouth() == null
&& params.getWest() == null && params.getEast() == null)
return gds.getBoundingBox();
return new LatLonRect(new LatLonPointImpl(params.getSouth(),
params.getWest()), params.getNorth() - params.getSouth(),
params.getEast() - params.getWest());
}
private Range getZRange(GridDataset gds, Double verticalCoord,
Integer vertStride, List<String> vars)
throws OutOfBoundariesException {
boolean hasVerticalCoord = false;
Range zRange = null;
if (verticalCoord != null) {
hasVerticalCoord = !Double.isNaN(verticalCoord);
// allow a vert level to be specified - but only one, and look in
// first 3D var with nlevels > 1
if (hasVerticalCoord) {
try {
for (String varName : vars) {
GridDatatype grid = gds.findGridDatatype(varName);
GridCoordSystem gcs = grid.getCoordinateSystem();
CoordinateAxis1D vaxis = gcs.getVerticalAxis();
if (vaxis != null && vaxis.getSize() > 1) {
int bestIndex = -1;
double bestDiff = Double.MAX_VALUE;
for (int i = 0; i < vaxis.getSize(); i++) {
double diff = Math.abs(vaxis.getCoordValue(i)
- verticalCoord);
if (diff < bestDiff) {
bestIndex = i;
bestDiff = diff;
}
}
if (bestIndex >= 0)
zRange = new Range(bestIndex, bestIndex);
}
}
} catch (InvalidRangeException ire) {
throw new OutOfBoundariesException(
"Invalid vertical level: " + ire.getMessage());
}
// there's also a vertStride, but not needed since only 1D slice
// is allowed
}
} else {// No vertical range was provided, we get the zRange with the
// zStride (1 by default)
if (vertStride > 1) {
try {
zRange = new Range(0, 0, vertStride);
for (String varName : vars) {
GridDatatype grid = gds.findGridDatatype(varName);
GridCoordSystem gcs = grid.getCoordinateSystem();
CoordinateAxis1D vaxis = gcs.getVerticalAxis();
if (vaxis != null) {
// Range vRange = new Range(0,
// (int)vaxis.getSize()-1, 1);
zRange = new Range(
zRange.first(),
zRange.last() > vaxis.getSize() ? zRange
.last() : (int) vaxis.getSize() - 1,
vertStride);
}
}
} catch (InvalidRangeException ire) {
throw new OutOfBoundariesException(
"Invalid vertical stride: " + ire.getMessage());
}
}
}
return zRange;
}
}