/* (c) 2014 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.wcs; import static org.vfny.geoserver.wcs.WcsException.WcsExceptionCode.InvalidParameterValue; import java.awt.geom.AffineTransform; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; import javax.media.jai.Interpolation; import net.opengis.ows11.BoundingBoxType; import net.opengis.ows11.CodeType; import net.opengis.wcs11.AxisSubsetType; import net.opengis.wcs11.DescribeCoverageType; import net.opengis.wcs11.DomainSubsetType; import net.opengis.wcs11.FieldSubsetType; import net.opengis.wcs11.GetCapabilitiesType; import net.opengis.wcs11.GetCoverageType; import net.opengis.wcs11.GridCrsType; import net.opengis.wcs11.OutputType; import net.opengis.wcs11.RangeSubsetType; import net.opengis.wcs11.TimePeriodType; import net.opengis.wcs11.TimeSequenceType; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CoverageDimensionInfo; import org.geoserver.catalog.CoverageInfo; import org.geoserver.config.GeoServer; import org.geoserver.data.util.CoverageUtils; import org.geoserver.ows.util.RequestUtils; import org.geoserver.wcs.kvp.GridCS; import org.geoserver.wcs.kvp.GridType; import org.geoserver.wcs.response.DescribeCoverageTransformer; import org.geoserver.wcs.response.WCSCapsTransformer; import org.geoserver.wcs.responses.CoverageResponseDelegate; import org.geoserver.wcs.responses.CoverageResponseDelegateFinder; import org.geotools.coverage.grid.GeneralGridGeometry; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.coverage.grid.io.GridCoverage2DReader; import org.geotools.geometry.GeneralEnvelope; import org.geotools.gml2.bindings.GML2EncodingUtils; import org.geotools.parameter.DefaultParameterDescriptor; import org.geotools.referencing.CRS; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.referencing.operation.transform.AffineTransform2D; import org.geotools.referencing.operation.transform.IdentityTransform; import org.geotools.util.logging.Logging; import org.opengis.coverage.grid.GridCoverage; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.filter.Filter; import org.opengis.geometry.Envelope; import org.opengis.parameter.GeneralParameterDescriptor; import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.ParameterDescriptor; import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.cs.AxisDirection; import org.opengis.referencing.cs.CoordinateSystemAxis; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.referencing.operation.CoordinateOperationFactory; import org.opengis.referencing.operation.MathTransform; import org.vfny.geoserver.util.WCSUtils; import org.vfny.geoserver.wcs.WcsException; import org.vfny.geoserver.wcs.WcsException.WcsExceptionCode; public class DefaultWebCoverageService111 implements WebCoverageService111 { Logger LOGGER = Logging.getLogger(DefaultWebCoverageService111.class); private Catalog catalog; private GeoServer geoServer; private CoverageResponseDelegateFinder responseFactory; public DefaultWebCoverageService111(GeoServer geoServer, CoverageResponseDelegateFinder responseFactory) { this.geoServer = geoServer; this.catalog = geoServer.getCatalog(); this.responseFactory = responseFactory; } public WCSInfo getServiceInfo() { return geoServer.getService(WCSInfo.class); } public WCSCapsTransformer getCapabilities(GetCapabilitiesType request) { // do the version negotiation dance List<String> provided = new ArrayList<String>(); // provided.add("1.0.0"); provided.add("1.1.0"); provided.add("1.1.1"); List<String> accepted = null; if (request.getAcceptVersions() != null) accepted = request.getAcceptVersions().getVersion(); String version = RequestUtils.getVersionOws11(provided, accepted); // TODO: add support for 1.0.0 in here if ("1.1.0".equals(version) || "1.1.1".equals(version)) { WCSCapsTransformer capsTransformer = new WCSCapsTransformer(geoServer); capsTransformer.setEncoding(Charset.forName((getServiceInfo().getGeoServer() .getSettings().getCharset()))); return capsTransformer; } throw new WcsException("Could not understand version:" + version); } public DescribeCoverageTransformer describeCoverage(DescribeCoverageType request) { final String version = request.getVersion(); if ("1.1.0".equals(version) || "1.1.1".equals(version)) { WCSInfo wcs = getServiceInfo(); DescribeCoverageTransformer describeTransformer = new DescribeCoverageTransformer(wcs, catalog, responseFactory); describeTransformer.setEncoding(Charset.forName(wcs.getGeoServer().getSettings() .getCharset())); return describeTransformer; } throw new WcsException("Could not understand version:" + version); } @SuppressWarnings({ "deprecation", "unchecked" }) public GridCoverage[] getCoverage(GetCoverageType request) { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest(new StringBuffer("execute CoverageRequest response. Called request is: ") .append(request).toString()); } WCSInfo wcs = getServiceInfo(); CoverageInfo meta = null; GridCoverage2D coverage = null; try { CodeType identifier = request.getIdentifier(); if (identifier == null) throw new WcsException("Internal error, the coverage identifier must not be null", InvalidParameterValue, "identifier"); meta = catalog.getCoverageByName(identifier.getValue()); if (meta == null) { throw new WcsException("No such coverage: " + request.getIdentifier().getValue()); } // first let's run some sanity checks on the inputs checkDomainSubset(meta, request.getDomainSubset(), wcs); checkRangeSubset(meta, request.getRangeSubset()); checkOutput(meta, request.getOutput()); // grab the format, the reader using the default params final GridCoverage2DReader reader = (GridCoverage2DReader) meta.getGridCoverageReader( null, WCSUtils.getReaderHints(wcs)); // handle spatial domain subset, if needed final GeneralEnvelope originalEnvelope = reader.getOriginalEnvelope(); final BoundingBoxType bbox = request.getDomainSubset().getBoundingBox(); final CoordinateReferenceSystem nativeCRS = originalEnvelope .getCoordinateReferenceSystem(); final GeneralEnvelope requestedEnvelopeInNativeCRS; final GeneralEnvelope requestedEnvelope; if (bbox != null) { // first off, parse the envelope corners double[] lowerCorner = new double[bbox.getLowerCorner().size()]; double[] upperCorner = new double[bbox.getUpperCorner().size()]; for (int i = 0; i < lowerCorner.length; i++) { lowerCorner[i] = (Double) bbox.getLowerCorner().get(i); upperCorner[i] = (Double) bbox.getUpperCorner().get(i); } requestedEnvelope = new GeneralEnvelope(lowerCorner, upperCorner); // grab the native crs // if no crs has beens specified, the native one is assumed if (bbox.getCrs() == null) { requestedEnvelope.setCoordinateReferenceSystem(nativeCRS); requestedEnvelopeInNativeCRS = requestedEnvelope; } else { // otherwise we need to transform final CoordinateReferenceSystem bboxCRS = CRS.decode(bbox.getCrs()); requestedEnvelope.setCoordinateReferenceSystem(bboxCRS); if (!CRS.equalsIgnoreMetadata(bboxCRS, nativeCRS)) { CoordinateOperationFactory of = CRS.getCoordinateOperationFactory(true); CoordinateOperation co = of.createOperation(bboxCRS, nativeCRS); requestedEnvelopeInNativeCRS = CRS.transform(co, requestedEnvelope); } else { requestedEnvelopeInNativeCRS = new GeneralEnvelope(requestedEnvelope); } } } else { requestedEnvelopeInNativeCRS = reader.getOriginalEnvelope(); requestedEnvelope = requestedEnvelopeInNativeCRS; } final GridCrsType gridCRS = request.getOutput().getGridCRS(); // Compute the crs that the final coverage will be served into final CoordinateReferenceSystem targetCRS; if (gridCRS == null) { targetCRS = reader.getOriginalEnvelope().getCoordinateReferenceSystem(); } else { targetCRS = CRS.decode(gridCRS.getGridBaseCRS()); } // // Raster destination size // int elevationLevels = 0; double[] elevations = null; // grab the grid to world transformation MathTransform gridToCRS = reader.getOriginalGridToWorld(PixelInCell.CELL_CENTER); // // TIME Values // final List<Date> timeValues = new LinkedList<Date>(); TimeSequenceType temporalSubset = request.getDomainSubset().getTemporalSubset(); if (temporalSubset != null && temporalSubset.getTimePosition() != null && temporalSubset.getTimePosition().size() > 0) { for (Iterator it = temporalSubset.getTimePosition().iterator(); it.hasNext();) { Date tp = (Date) it.next(); timeValues.add(tp); } } else if (temporalSubset != null && temporalSubset.getTimePeriod() != null && temporalSubset.getTimePeriod().size() > 0) { for (Iterator it = temporalSubset.getTimePeriod().iterator(); it.hasNext();) { TimePeriodType tp = (TimePeriodType) it.next(); Date beginning = (Date) tp.getBeginPosition(); Date ending = (Date) tp.getEndPosition(); timeValues.add(beginning); timeValues.add(ending); } } // now we have enough info to read the coverage, grab the parameters // and add the grid geometry info final GeneralEnvelope intersectionEnvelopeInSourceCRS = new GeneralEnvelope( requestedEnvelopeInNativeCRS); intersectionEnvelopeInSourceCRS.intersect(originalEnvelope); final GridGeometry2D requestedGridGeometry = new GridGeometry2D( PixelInCell.CELL_CENTER, gridToCRS, intersectionEnvelopeInSourceCRS, null); final ParameterValueGroup readParametersDescriptor = reader.getFormat() .getReadParameters(); GeneralParameterValue[] readParameters = CoverageUtils.getParameters( readParametersDescriptor, meta.getParameters()); readParameters = (readParameters != null ? readParameters : new GeneralParameterValue[0]); // // Setting coverage reading params. // final ParameterValue requestedGridGeometryParam = new DefaultParameterDescriptor( AbstractGridFormat.READ_GRIDGEOMETRY2D.getName().toString(), GeneralGridGeometry.class, null, requestedGridGeometry).createValue(); /* * Test if the parameter "TIME" is present in the WMS request, and by the way in the reading parameters. If it is the case, one can adds * it to the request. If an exception is thrown, we have nothing to do. */ final List<GeneralParameterDescriptor> parameterDescriptors = readParametersDescriptor .getDescriptor().descriptors(); ParameterValue time = null; boolean hasTime = timeValues.size() > 0; ParameterValue elevation = null; boolean hasElevation = elevations != null && !Double.isNaN(elevations[0]); if (hasElevation || hasTime) { for (GeneralParameterDescriptor pd : parameterDescriptors) { final String code = pd.getName().getCode(); // // TIME // if (code.equalsIgnoreCase("TIME")) { time = (ParameterValue) pd.createValue(); time.setValue(timeValues); } // // ELEVATION // if (code.equalsIgnoreCase("ELEVATION")) { elevation = (ParameterValue) pd.createValue(); elevation.setValue(elevations[0]); } // leave? if ((hasElevation && elevation != null && hasTime && time != null) || !hasElevation && hasTime && time != null || hasElevation && elevation != null && !hasTime) break; } } // // add read parameters // int addedParams = 1 + (hasTime ? 1 : 0) + (hasElevation ? 1 : 0); // add to the list GeneralParameterValue[] readParametersClone = new GeneralParameterValue[readParameters.length + addedParams--]; System.arraycopy(readParameters, 0, readParametersClone, 0, readParameters.length); readParametersClone[readParameters.length + addedParams--] = requestedGridGeometryParam; if (hasTime) readParametersClone[readParameters.length + addedParams--] = time; if (hasElevation) readParametersClone[readParameters.length + addedParams--] = elevation; readParameters = readParametersClone; // Check we're not being requested to read too much data from input (first check, // guesses the grid size using the information contained in CoverageInfo) WCSUtils.checkInputLimits(wcs, meta, reader, requestedGridGeometry); // // Check if we have a filter among the params // Filter filter = WCSUtils.getRequestFilter(); if (filter != null) { readParameters = CoverageUtils.mergeParameter(parameterDescriptors, readParameters, filter, "FILTER", "Filter"); } // // make sure we work in streaming mode // // work in streaming fashion when JAI is involved readParameters = WCSUtils.replaceParameter(readParameters, Boolean.TRUE, AbstractGridFormat.USE_JAI_IMAGEREAD); // // perform Read ... // coverage = (GridCoverage2D) reader.read(readParameters); if ((coverage == null) || !(coverage instanceof GridCoverage2D)) { throw new IOException("The requested coverage could not be found."); } // now that we have read the coverage double check the input size WCSUtils.checkInputLimits(wcs, coverage); // some raster sources do not really read less data (arcgrid for example), we may need to crop if (!intersectionEnvelopeInSourceCRS.contains(coverage.getEnvelope2D(), true)) { coverage = WCSUtils.crop(coverage, intersectionEnvelopeInSourceCRS); } /** * Band Select (works on just one field) */ GridCoverage2D bandSelectedCoverage = coverage; String interpolationType = null; if (request.getRangeSubset() != null) { if (request.getRangeSubset().getFieldSubset().size() > 1) { throw new WcsException("Multi field coverages are not supported yet"); } FieldSubsetType field = (FieldSubsetType) request.getRangeSubset().getFieldSubset() .get(0); interpolationType = field.getInterpolationType(); // handle axis subset if (field.getAxisSubset().size() > 1) { throw new WcsException("Multi axis coverages are not supported yet"); } if (field.getAxisSubset().size() == 1) { // prepare a support structure to quickly get the band index // of a // key List<CoverageDimensionInfo> dimensions = meta.getDimensions(); Map<String, Integer> dimensionMap = new HashMap<String, Integer>(); for (int i = 0; i < dimensions.size(); i++) { String keyName = dimensions.get(i).getName().replace(' ', '_'); dimensionMap.put(keyName, i); } // extract the band indexes AxisSubsetType axisSubset = (AxisSubsetType) field.getAxisSubset().get(0); List keys = axisSubset.getKey(); int[] bands = new int[keys.size()]; for (int j = 0; j < bands.length; j++) { final String key = (String) keys.get(j); Integer index = dimensionMap.get(key); if (index == null) throw new WcsException("Unknown field/axis/key combination " + field.getIdentifier().getValue() + "/" + axisSubset.getIdentifier() + "/" + key); bands[j] = index; } // finally execute the band select try { bandSelectedCoverage = (GridCoverage2D) WCSUtils .bandSelect(coverage, bands); } catch (WcsException e) { throw new WcsException(e.getLocalizedMessage()); } } } /** * Checking for supported Interpolation Methods */ Interpolation interpolation = Interpolation.getInstance(Interpolation.INTERP_NEAREST); if (interpolationType != null) { if (interpolationType.equalsIgnoreCase("linear") || interpolationType.equalsIgnoreCase("bilinear")) { interpolation = Interpolation.getInstance(Interpolation.INTERP_BILINEAR); } else if (interpolationType.equalsIgnoreCase("cubic") || interpolationType.equalsIgnoreCase("bicubic")) { interpolation = Interpolation.getInstance(Interpolation.INTERP_BICUBIC); } else if (interpolationType.equalsIgnoreCase("nearest")) { interpolation = Interpolation.getInstance(Interpolation.INTERP_NEAREST); } } // adjust the grid geometry to use the final bbox and crs final GeneralEnvelope intersectionEnvelope; boolean reprojectionNeeded = !CRS.equalsIgnoreMetadata(nativeCRS, targetCRS); if (reprojectionNeeded) { CoordinateOperationFactory of = CRS.getCoordinateOperationFactory(true); CoordinateOperation co = of.createOperation(nativeCRS, targetCRS); intersectionEnvelope = CRS.transform(co, intersectionEnvelopeInSourceCRS); } else { intersectionEnvelope = intersectionEnvelopeInSourceCRS; } // compute the output resolution double pixelSizeX; double pixelSizeY; if (gridCRS != null) { Double[] origin = (Double[]) gridCRS.getGridOrigin(); Double[] offsets = (Double[]) gridCRS.getGridOffsets(); // from the specification if grid origin is omitted and the crs // is 2d the default it's 0,0 if (origin == null) { origin = new Double[] { 0.0, 0.0 }; } // if no offsets has been specified we try to default on the // native ones if (offsets == null) { offsets = estimateOffsets(reader, gridCRS, gridToCRS, intersectionEnvelope, reprojectionNeeded); } // building the actual transform for the resulting grid geometry AffineTransform tx; if (gridCRS.getGridType().equals(GridType.GT2dSimpleGrid.getXmlConstant())) { tx = new AffineTransform(offsets[0], 0, 0, offsets[1], origin[0], origin[1]); } else if (gridCRS.getGridType().equals(GridType.GT2dGridIn2dCrs.getXmlConstant())) { tx = new AffineTransform(offsets[0], offsets[1], offsets[2], offsets[3], origin[0], origin[1]); } else { tx = new AffineTransform(offsets[0], offsets[4], offsets[1], offsets[3], origin[0], origin[1]); if (origin.length != 3 || offsets.length != 6) throw new WcsException("", InvalidParameterValue, "GridCRS"); // // ELEVATIONS // // TODO: draft code ... it needs more study! elevationLevels = (int) Math.round(requestedEnvelope.getUpperCorner() .getOrdinate(2) - requestedEnvelope.getLowerCorner().getOrdinate(2)); // compute the elevation levels, we have elevationLevels values if (elevationLevels > 0) { elevations = new double[elevationLevels]; elevations[0] = requestedEnvelope.getLowerCorner().getOrdinate(2); // TODO put the extrema elevations[elevationLevels - 1] = requestedEnvelope.getUpperCorner() .getOrdinate(2); if (elevationLevels > 2) { final int adjustedLevelsNum = elevationLevels - 1; double step = (elevations[elevationLevels - 1] - elevations[0]) / adjustedLevelsNum; for (int i = 1; i < adjustedLevelsNum; i++) elevations[i] = elevations[i - 1] + step; } } } pixelSizeX = Math.abs(tx.getScaleX()); pixelSizeY = Math.abs(tx.getScaleY()); gridToCRS = new AffineTransform2D(tx); } else { Double[] offsets = estimateOffsets(reader, gridCRS, gridToCRS, intersectionEnvelope, reprojectionNeeded); if (offsets.length == 2) { pixelSizeX = Math.abs(offsets[0]); pixelSizeY = Math.abs(offsets[1]); AffineTransform tx = new AffineTransform(offsets[0], 0, 0, offsets[1], 0, 0); gridToCRS = new AffineTransform2D(tx); } else { AffineTransform tx = new AffineTransform(offsets[0], offsets[1], offsets[3], offsets[4], 0, 0); pixelSizeX = Math.abs(XAffineTransform.getScaleX0(tx)); pixelSizeY = Math.abs(XAffineTransform.getScaleY0(tx)); gridToCRS = new AffineTransform2D(tx); } } /** * Reproject */ // adjust to have at least one pixel in the output if (intersectionEnvelope.getSpan(0) < Math.abs(pixelSizeX)) { double minX = intersectionEnvelope.getMinimum(0); intersectionEnvelope.setRange(0, minX, minX + pixelSizeX); } if (intersectionEnvelope.getSpan(1) < Math.abs(pixelSizeY)) { double minY = intersectionEnvelope.getMinimum(1); intersectionEnvelope.setRange(1, minY, minY + pixelSizeY); } final GridGeometry2D destinationGridGeometry = new GridGeometry2D( PixelInCell.CELL_CENTER, gridToCRS, intersectionEnvelope, null); // before extracting the output make sure it's not too big WCSUtils.checkOutputLimits(wcs, destinationGridGeometry.getGridRange2D(), bandSelectedCoverage.getRenderedImage().getSampleModel()); // reproject if necessary boolean sameGridGeometry = bandSelectedCoverage.getGridGeometry().equals( destinationGridGeometry); if (reprojectionNeeded || !sameGridGeometry) { final GridCoverage2D reprojectedCoverage = WCSUtils.resample(bandSelectedCoverage, nativeCRS, targetCRS, destinationGridGeometry, interpolation); return new GridCoverage[] { reprojectedCoverage }; } else { return new GridCoverage[] { bandSelectedCoverage }; } } catch (Throwable e) { if (coverage != null) { CoverageCleanerCallback.addCoverages(coverage); } if (e instanceof WcsException) { throw (WcsException) e; } else { throw new WcsException(e); } } } private Double[] estimateOffsets(final GridCoverage2DReader reader, final GridCrsType gridCRS, MathTransform gridToCRS, final GeneralEnvelope intersectionEnvelope, boolean reprojectionNeeded) { Double[] offsets; if (!(gridToCRS instanceof AffineTransform2D) && !(gridToCRS instanceof IdentityTransform)) throw new WcsException( "Internal error, the coverage we're playing with does not have an affine transform..."); if (!reprojectionNeeded) { if (gridCRS != null) { if (gridToCRS instanceof IdentityTransform) { if (gridCRS.getGridType().equals(GridType.GT2dSimpleGrid.getXmlConstant()) || gridCRS.getGridType().equals( GridType.GT2dGridIn2dCrs.getXmlConstant())) offsets = new Double[] { 1.0, -1.0 }; else offsets = new Double[] { 1.0, 0.0, 0.0, 0.0, -1.0, 0.0 }; } else { AffineTransform2D affine = (AffineTransform2D) gridToCRS; if (gridCRS.getGridType().equals(GridType.GT2dSimpleGrid.getXmlConstant()) || gridCRS.getGridType().equals( GridType.GT2dGridIn2dCrs.getXmlConstant())) offsets = new Double[] { affine.getScaleX(), affine.getScaleY() }; else offsets = new Double[] { affine.getScaleX(), affine.getShearX(), affine.getShearY(), affine.getScaleY() }; } } else { AffineTransform2D at = (AffineTransform2D) gridToCRS; offsets = new Double[] { at.getScaleX(), at.getShearX(), 0d, at.getShearY(), at.getScaleY(), 0d }; } } else { // the input resolution is going to be completed unrelated to the output one // make an estimate assuming we want to keep the output raster with roughly // the same size as the input one double teWidth = intersectionEnvelope.getSpan(0); double teHeight = intersectionEnvelope.getSpan(1); double targetRatio = teWidth / teHeight; GridEnvelope gr = reader.getOriginalGridRange(); int targetRasterWidth = gr.getSpan(0); int targetRasterHeight = (int) Math.ceil(targetRasterWidth / targetRatio); double scaleX = teWidth / targetRasterWidth; double scaleY = -teHeight / targetRasterHeight; if (gridCRS == null || gridCRS.getGridType().equals(GridType.GT2dSimpleGrid.getXmlConstant()) || gridCRS.getGridType().equals(GridType.GT2dGridIn2dCrs.getXmlConstant())) { offsets = new Double[] { scaleX, scaleY }; } else { offsets = new Double[] { scaleX, 0.0, 0.0, 0.0, scaleY, 0.0 }; } } return offsets; } private void checkDomainSubset(CoverageInfo meta, DomainSubsetType domainSubset, WCSInfo wcs) throws Exception { BoundingBoxType bbox = domainSubset.getBoundingBox(); // domain subset should actually be always specified, but we try to be more lenient // (we should probably have a "strict cite" behavior if (bbox == null) { return; } // workaround for https://osgeo-org.atlassian.net/projects/GEOT/issues/GEOT-1710 if ("urn:ogc:def:crs:OGC:1.3:CRS84".equals(bbox.getCrs())) { bbox.setCrs("EPSG:4326"); } CoordinateReferenceSystem bboxCRs = CRS.decode(bbox.getCrs()); GridCoverage2DReader reader = (GridCoverage2DReader) meta.getGridCoverageReader(null, WCSUtils.getReaderHints(wcs)); Envelope gridEnvelope = reader.getOriginalEnvelope(); GeneralEnvelope gridEnvelopeBboxCRS = null; if (bboxCRs instanceof GeographicCRS) { try { CoordinateOperationFactory cof = CRS.getCoordinateOperationFactory(true); final CoordinateOperation operation = cof.createOperation( gridEnvelope.getCoordinateReferenceSystem(), bboxCRs); gridEnvelopeBboxCRS = CRS.transform(operation, gridEnvelope); } catch (Exception e) { // this may happen, there is nothing we can do about it, we just // use the back transformed envelope to be more lenient about // which coordinate coorections to make on the longitude axis // should the antimeridian style be used } } // check the coordinates, but make sure the case 175,-175 is handled // as valid for the longitude axis in a geographic coordinate system // see section 7.6.2 of the WCS 1.1.1 spec) List<Double> lower = bbox.getLowerCorner(); List<Double> upper = bbox.getUpperCorner(); for (int i = 0; i < lower.size(); i++) { if (lower.get(i) > upper.get(i)) { final CoordinateSystemAxis axis = bboxCRs.getCoordinateSystem().getAxis(i); // see if the coordinates can be fixed if (bboxCRs instanceof GeographicCRS && axis.getDirection() == AxisDirection.EAST) { if (gridEnvelopeBboxCRS != null) { // try to guess which one needs to be fixed final double envMax = gridEnvelopeBboxCRS.getMaximum(i); if (envMax >= lower.get(i)) upper.set(i, upper.get(i) + (axis.getMaximumValue() - axis.getMinimumValue())); else lower.set(i, lower.get(i) - (axis.getMaximumValue() - axis.getMinimumValue())); } else { // just fix the upper and hope... upper.set(i, upper.get(i) + (axis.getMaximumValue() - axis.getMinimumValue())); } } // if even after the fix we're in the wrong situation, complain if (lower.get(i) > upper.get(i)) { throw new WcsException("illegal bbox, min of dimension " + (i + 1) + ": " + lower.get(i) + " is " + "greater than max of same dimension: " + upper.get(i), WcsExceptionCode.InvalidParameterValue, "BoundingBox"); } } } } /** * Checks that the elements of the Output part of the request do make sense by comparing them to the coverage metadata * * @param info * @param rangeSubset */ private void checkOutput(CoverageInfo meta, OutputType output) { if (output == null) return; String format = output.getFormat(); String declaredFormat = getDeclaredFormat(meta.getSupportedFormats(), format); if (declaredFormat == null) throw new WcsException("format " + format + " is not supported for this coverage", InvalidParameterValue, "format"); final GridCrsType gridCRS = output.getGridCRS(); if (gridCRS != null) { // check grid base crs is valid, and eventually default it out String gridBaseCrs = gridCRS.getGridBaseCRS(); if (gridBaseCrs != null) { // make sure the requested is among the supported ones, by // making a // code level // comparison (to avoid assuming epsg:xxxx and // http://www.opengis.net/gml/srs/epsg.xml#xxx are different // ones. // We'll also consider the urn one comparable, allowing eventual // axis flip on the // geographic crs String actualCRS = null; final String gridBaseCrsCode = extractCode(gridBaseCrs); for (Iterator it = meta.getResponseSRS().iterator(); it.hasNext();) { final String responseCRS = (String) it.next(); final String code = extractCode(responseCRS); if (code.equalsIgnoreCase(gridBaseCrsCode)) { actualCRS = responseCRS; } } if (actualCRS == null) throw new WcsException("CRS " + gridBaseCrs + " is not among the supported ones for coverage " + meta.getName(), WcsExceptionCode.InvalidParameterValue, "GridBaseCrs"); gridCRS.setGridBaseCRS(gridBaseCrs); } else { String code = GML2EncodingUtils.epsgCode(meta.getCRS()); gridCRS.setGridBaseCRS("urn:x-ogc:def:crs:EPSG:" + code); } // check grid type makes sense and apply default otherwise String gridTypeValue = gridCRS.getGridType(); GridType type = GridType.GT2dGridIn2dCrs; if (gridTypeValue != null) { type = null; for (GridType gt : GridType.values()) { if (gt.getXmlConstant().equalsIgnoreCase(gridTypeValue)) type = gt; } if (type == null) throw new WcsException("Unknown grid type " + gridTypeValue, InvalidParameterValue, "GridType"); else if (type == GridType.GT2dGridIn3dCrs) throw new WcsException("Unsupported grid type " + gridTypeValue, InvalidParameterValue, "GridType"); } gridCRS.setGridType(type.getXmlConstant()); // check gridcs and apply only value we know about String gridCS = gridCRS.getGridCS(); if (gridCS != null) { if (!gridCS.equalsIgnoreCase(GridCS.GCSGrid2dSquare.getXmlConstant())) throw new WcsException("Unsupported grid cs " + gridCS, InvalidParameterValue, "GridCS"); } gridCRS.setGridCS(GridCS.GCSGrid2dSquare.getXmlConstant()); // check the grid origin and set defaults CoordinateReferenceSystem crs = null; try { crs = CRS.decode(gridCRS.getGridBaseCRS()); } catch (Exception e) { throw new WcsException("Could not understand crs " + gridCRS.getGridBaseCRS(), WcsExceptionCode.InvalidParameterValue, "GridBaseCRS"); } if (!gridCRS.isSetGridOrigin() || gridCRS.getGridOrigin() == null) { // if not set, we have a default of "0 0" as a string, since I // cannot // find a way to make it default to new Double[] {0 0} I'll fix // it here Double[] origin = new Double[type.getOriginArrayLength()]; Arrays.fill(origin, 0.0); gridCRS.setGridOrigin(origin); } else { Double[] gridOrigin = (Double[]) gridCRS.getGridOrigin(); // make sure the origin dimension matches the output crs // dimension if (gridOrigin.length != type.getOriginArrayLength()) throw new WcsException("Grid origin size (" + gridOrigin.length + ") inconsistent with grid type " + type.getXmlConstant() + " that requires (" + type.getOriginArrayLength() + ")", WcsExceptionCode.InvalidParameterValue, "GridOrigin"); gridCRS.setGridOrigin(gridOrigin); } // perform same checks on the offsets Double[] gridOffsets = (Double[]) gridCRS.getGridOffsets(); if (gridOffsets != null) { // make sure the origin dimension matches the grid type if (type.getOffsetArrayLength() != gridOffsets.length) throw new WcsException("Invalid offsets lenght, grid type " + type.getXmlConstant() + " requires " + type.getOffsetArrayLength(), InvalidParameterValue, "GridOffsets"); } else { gridCRS.setGridOffsets(null); } } } /** * Extracts only the final part of an EPSG code allowing for a specification independent comparison (that is, it removes the EPSG:, urn:xxx:, * http://... prefixes) * * @param srsName * */ private String extractCode(String srsName) { if (srsName.startsWith("http://www.opengis.net/gml/srs/epsg.xml#")) return srsName.substring(40); else if (srsName.startsWith("urn:")) return srsName.substring(srsName.lastIndexOf(':') + 1); else if (srsName.startsWith("EPSG:")) return srsName.substring(5); else return srsName; } /** * Checks if the supported format string list contains the specified format, doing a case insensitive search. If found the declared output format * name is returned, otherwise null is returned. * * @param supportedFormats * @param format * */ private String getDeclaredFormat(List supportedFormats, String format) { // supported formats may be setup using old style formats, first scan // the // configured list for (Iterator it = supportedFormats.iterator(); it.hasNext();) { String sf = (String) it.next(); if (sf.equalsIgnoreCase(format)) { return sf; } else { CoverageResponseDelegate delegate = responseFactory.encoderFor(sf); if (delegate != null && delegate.canProduce(format)) return sf; } } return null; } /** * Checks that the elements of the RangeSubset part of the request do make sense by comparing them to the coverage metadata * * @param info * @param rangeSubset */ private void checkRangeSubset(CoverageInfo info, RangeSubsetType rangeSubset) { // quick escape if no range subset has been specified (it's legal) if (rangeSubset == null) return; if (rangeSubset.getFieldSubset().size() > 1) { throw new WcsException("Multi field coverages are not supported yet", InvalidParameterValue, "RangeSubset"); } // check field identifier FieldSubsetType field = (FieldSubsetType) rangeSubset.getFieldSubset().get(0); final String fieldId = field.getIdentifier().getValue(); if (!fieldId.equalsIgnoreCase("contents")) throw new WcsException("Unknown field " + fieldId, InvalidParameterValue, "RangeSubset"); // check interpolation String interpolation = field.getInterpolationType(); if (interpolation != null) { boolean interpolationSupported = false; if (interpolation.equalsIgnoreCase("nearest")) { interpolation = "nearest"; } else if (interpolation.equalsIgnoreCase("cubic") || interpolation.equalsIgnoreCase("bicubic")) { interpolation = "bicubic"; } else if (interpolation.equalsIgnoreCase("linear") || interpolation.equalsIgnoreCase("bilinear")) { interpolation = "bilinear"; } for (String method : info.getInterpolationMethods()) { if (method.toLowerCase().startsWith(interpolation)) { interpolationSupported = true; break; } } if (!interpolationSupported) throw new WcsException( "The requested Interpolation method is not supported by this Coverage.", InvalidParameterValue, "RangeSubset"); } // check axis if (field.getAxisSubset().size() > 1) { throw new WcsException("Multi axis coverages are not supported yet", InvalidParameterValue, "RangeSubset"); } else if (field.getAxisSubset().size() == 0) return; AxisSubsetType axisSubset = (AxisSubsetType) field.getAxisSubset().get(0); final String axisId = axisSubset.getIdentifier(); if (!axisId.equalsIgnoreCase("Bands")) throw new WcsException("Unknown axis " + axisId + " in field " + fieldId, InvalidParameterValue, "RangeSubset"); // prepare a support structure to quickly get the band index of a key // (and remember we replaced spaces with underscores in the keys to // avoid issues // with the kvp parsing of indentifiers that include spaces) List<CoverageDimensionInfo> dimensions = info.getDimensions(); Set<String> dimensionMap = new HashSet<String>(); for (int i = 0; i < dimensions.size(); i++) { String keyName = dimensions.get(i).getName().replace(' ', '_'); dimensionMap.add(keyName); } // check keys List keys = axisSubset.getKey(); int[] bands = new int[keys.size()]; for (int j = 0; j < bands.length; j++) { final String key = (String) keys.get(j); String parsedKey = null; for (String dimensionName : dimensionMap) { if (dimensionName.equalsIgnoreCase(key)) { parsedKey = dimensionName; break; } } if (parsedKey == null) throw new WcsException("Unknown field/axis/key combination " + fieldId + "/" + axisSubset.getIdentifier() + "/" + key, InvalidParameterValue, "RangeSubset"); else keys.set(j, parsedKey); } } /** * * @param date * */ private static Date cvtToGmt(Date date) { TimeZone tz = TimeZone.getDefault(); Date ret = new Date(date.getTime() - tz.getRawOffset()); // if we are now in DST, back off by the delta. Note that we are checking the GMT date, this is the KEY. if (tz.inDaylightTime(ret)) { Date dstDate = new Date(ret.getTime() - tz.getDSTSavings()); // check to make sure we have not crossed back into standard time // this happens when we are on the cusp of DST (7pm the day before the change for PDT) if (tz.inDaylightTime(dstDate)) { ret = dstDate; } } return ret; } }