/* (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.wcs2_0.util; import java.awt.geom.AffineTransform; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import javax.media.jai.Interpolation; import org.geoserver.catalog.CoverageInfo; import org.geoserver.catalog.CoverageStoreInfo; import org.geoserver.data.util.CoverageUtils; import org.geoserver.platform.OWS20Exception; import org.geoserver.platform.ServiceException; import org.geoserver.wcs2_0.WCS20Const; import org.geoserver.wcs2_0.exception.WCS20Exception; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.io.GridCoverage2DReader; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.coverage.grid.io.GridCoverage2DReader; import org.geotools.coverage.grid.io.OverviewPolicy; import org.geotools.factory.GeoTools; import org.geotools.factory.Hints; import org.geotools.gce.imagemosaic.ImageMosaicFormat; import org.geotools.geometry.GeneralEnvelope; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.parameter.Parameter; import org.geotools.referencing.CRS; import org.geotools.util.DefaultProgressListener; import org.geotools.util.Version; import org.geotools.util.logging.Logging; import org.opengis.coverage.grid.Format; import org.opengis.coverage.grid.GridCoverageReader; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.geometry.BoundingBox; import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.MathTransform; /** * Utility class performing operations related to http requests. * * * TODO: these methods should be put back into org.geoserver.ows.util.RequestUtils */ public class RequestUtils { /** {@link Logger} for this class.*/ private final static Logger LOGGER= Logging.getLogger(RequestUtils.class); /** * Given a list of provided versions, and a list of accepted versions, this method will * return the negotiated version to be used for response according to the OWS 2.0 specification. * * The difference from the 11 version is that here versions can have format "x.y". * * @param providedList a non null, non empty list of provided versions (in "x.y.z" or "x.y" format) * @param acceptedList a list of accepted versions, eventually null or empty (in "x.y.z" or "x.y" format) * @return the negotiated version to be used for response * * @see org.geoserver.ows.util.RequestUtils#getVersionOws11(java.util.List, java.util.List) */ public static String getVersionOws20(List<String> providedList, List<String> acceptedList) { //first figure out which versions are provided TreeSet<Version> provided = new TreeSet<Version>(); for (String v : providedList) { provided.add(new Version(v)); } // if no accept list provided, we return the biggest supported version if(acceptedList == null || acceptedList.isEmpty()) return provided.last().toString(); // next figure out what the client accepts (and check they are good version numbers) List<Version> accepted = new ArrayList<Version>(); for (String v : acceptedList) { checkVersionNumber20(v, "AcceptVersions"); accepted.add(new Version(v)); } // from the specification "The server, upon receiving a GetCapabilities request, shall scan // through this list and find the first version number that it supports" Version negotiated = null; for (Version version : accepted) { if (provided.contains(version)) { negotiated = version; break; } } // from the spec: "If the list does not contain any version numbers that the server // supports, the server shall return an Exception with // exceptionCode="VersionNegotiationFailed" if(negotiated == null) throw new OWS20Exception("Could not find any matching version", OWS20Exception.OWSExceptionCode.VersionNegotiationFailed); return negotiated.toString(); } /** * Checks the validity of a version number (the specification version numbers, two or three dot * separated integers between 0 and 99). Throws a ServiceException if the version number * is not valid. * @param v the version number (in string format) * @param the locator for the service exception (may be null) */ public static void checkVersionNumber20(String v, String locator) throws ServiceException { if (!v.matches("[0-9]{1,2}\\.[0-9]{1,2}(\\.[0-9]{1,2})?")) { String msg = v + " is an invalid version number"; throw new OWS20Exception("Could not find any matching version,"+msg, OWS20Exception.OWSExceptionCode.VersionNegotiationFailed, locator); } } /** * Reads the best matching grid out of a grid coverage applying sub-sampling and using overviews * as necessary * * @param mapContent * @param reader * @param params * @param readGG * @param interpolation * @param hints * * @throws IOException */ public static GridCoverage2D readBestCoverage( final GridCoverage2DReader reader, final Object params, final GridGeometry2D readGG, final Interpolation interpolation, final OverviewPolicy overviewPolicy, Hints hints) throws IOException { //// // // Intersect the present envelope with the request envelope, also in WGS 84 to make sure // there is an actual intersection // //// try { final ReferencedEnvelope coverageEnvelope=new ReferencedEnvelope(reader.getOriginalEnvelope()); if (!coverageEnvelope.intersects((BoundingBox) ReferencedEnvelope.reference(readGG.getEnvelope()))) { return null; } } catch (Exception e) { LOGGER.log( Level.WARNING, "Failed to compare data and request envelopes, proceeding with rendering anyways", e); } // // // It is an GridCoverage2DReader, let's use parameters // if we have any supplied by a user. // // // first I created the correct ReadGeometry final Parameter<GridGeometry2D> readGGParam = (Parameter<GridGeometry2D>) AbstractGridFormat.READ_GRIDGEOMETRY2D.createValue(); readGGParam.setValue(readGG); final Parameter<Interpolation> readInterpolation = (Parameter<Interpolation>) ImageMosaicFormat.INTERPOLATION.createValue(); readInterpolation.setValue(interpolation); final Parameter<OverviewPolicy> readOverview = (Parameter<OverviewPolicy>) AbstractGridFormat.OVERVIEW_POLICY.createValue(); readOverview.setValue(overviewPolicy); // then I try to get read parameters associated with this // coverage if there are any. GridCoverage2D coverage = null; GeneralParameterValue[] readParams = (GeneralParameterValue[]) params; final int length = readParams == null ? 0 :readParams.length; if (length > 0) { // // // // Getting parameters to control how to read this coverage. // Remember to check to actually have them before forwarding // them to the reader. // // // // we have a valid number of parameters, let's check if // also have a READ_GRIDGEOMETRY2D. In such case we just // override it with the one we just build for this // request. final String readGGName = AbstractGridFormat.READ_GRIDGEOMETRY2D.getName().toString(); final String readInterpolationName = ImageMosaicFormat.INTERPOLATION.getName().toString(); final String overviewPolicyName = AbstractGridFormat.OVERVIEW_POLICY.getName().toString(); int i = 0; boolean foundInterpolation = false; boolean foundGG = false; boolean foundOverviewPolicy = false; for (; i < length; i++) { final String paramName = readParams[i].getDescriptor().getName().toString(); if (paramName.equalsIgnoreCase(readGGName)){ ((Parameter) readParams[i]).setValue(readGG); foundGG = true; } else if(paramName.equalsIgnoreCase(readInterpolationName)){ ((Parameter) readParams[i]).setValue(interpolation); foundInterpolation = true; } else if (paramName.equalsIgnoreCase(overviewPolicyName)){ // Override the default overview value using the one found on read params ((Parameter) readParams[i]).setValue(overviewPolicy); foundOverviewPolicy = true; } } // did we find anything? if (!foundGG || !foundInterpolation || !foundOverviewPolicy){// || !(foundBgColor && bgColor != null)) { // add the correct read geometry to the supplied // params since we did not find anything List<GeneralParameterValue> paramList = new ArrayList<GeneralParameterValue>(); paramList.addAll(Arrays.asList(readParams)); if(!foundGG) { paramList.add(readGGParam); } if(!foundInterpolation) { paramList.add(readInterpolation); } if(!foundOverviewPolicy && readOverview != null && readOverview.getValue() != null) { paramList.add(readOverview); } readParams = (GeneralParameterValue[]) paramList.toArray(new GeneralParameterValue[paramList.size()]); } coverage = (GridCoverage2D) reader.read(readParams); } else { coverage = (GridCoverage2D) reader.read(new GeneralParameterValue[] {readGGParam ,readInterpolation}); } return coverage; } /** * Returns the "Sample to geophysics" transform as an affine transform, or {@code null} * if none. Note that the returned instance may be an immutable one, not necessarly the * default Java2D implementation. * * @param gridToCRS The "grid to CRS" {@link MathTransform} transform. * @return The "grid to CRS" affine transform of the given coverage, or {@code null} * if none or if the transform is not affine. */ public static AffineTransform getAffineTransform(final MathTransform gridToCRS) { if(gridToCRS==null){ return null; } if (gridToCRS instanceof AffineTransform) { return (AffineTransform) gridToCRS; } return null; } /** * This utility method can be used to read a small sample {@link GridCoverage2D} for inspection * from the specified {@link CoverageInfo}. * * @param reader the {@link GridCoverage2DReader} that we'll read the coverage from * */ static public GridCoverage2D readSampleGridCoverage(GridCoverage2DReader reader)throws Exception { // // Now reading a fake small GridCoverage just to retrieve meta // information about bands: // // - calculating a new envelope which is just 5x5 pixels // - if it's a mosaic, limit the number of tiles we're going to read to one // (with time and elevation there might be hundreds of superimposed tiles) // - reading the GridCoverage subset // final GeneralEnvelope originalEnvelope = reader.getOriginalEnvelope(); final GridEnvelope originalRange = reader.getOriginalGridRange(); final Format coverageFormat = reader.getFormat(); final ParameterValueGroup readParams = coverageFormat.getReadParameters(); final Map parameters = CoverageUtils.getParametersKVP(readParams); final int minX = originalRange.getLow(0); final int minY = originalRange.getLow(1); final int width = originalRange.getSpan(0); final int height = originalRange.getSpan(1); final int maxX = minX + (width <= 5 ? width : 5); final int maxY = minY + (height <= 5 ? height : 5); // we have to be sure that we are working against a valid grid range. final GridEnvelope2D testRange = new GridEnvelope2D(minX, minY, maxX, maxY); // build the corresponding envelope final MathTransform gridToWorldCorner = reader.getOriginalGridToWorld(PixelInCell.CELL_CORNER); final GeneralEnvelope testEnvelope = CRS.transform(gridToWorldCorner, new GeneralEnvelope(testRange.getBounds())); testEnvelope.setCoordinateReferenceSystem(originalEnvelope.getCoordinateReferenceSystem()); // make sure mosaics with many superimposed tiles won't blow up with // a "too many open files" exception String maxAllowedTiles = ImageMosaicFormat.MAX_ALLOWED_TILES.getName().toString(); if(parameters.keySet().contains(maxAllowedTiles)) { parameters.put(maxAllowedTiles, 1); } parameters.put(AbstractGridFormat.READ_GRIDGEOMETRY2D.getName().toString(), new GridGeometry2D(testRange, testEnvelope)); // try to read this coverage return (GridCoverage2D) reader.read(CoverageUtils.getParameters(readParams, parameters, true)); } /** * This utility method can be used to read a small sample {@link GridCoverage2D} for inspection * from the specified {@link CoverageInfo}. * * @param ci the {@link CoverageInfo} that contains the description of the GeoServer coverage to read from. * */ static public GridCoverage2D readSampleGridCoverage(CoverageInfo ci)throws Exception { final GridCoverage2DReader reader = getCoverageReader(ci); return readSampleGridCoverage(reader); } /** * Grabs the reader from the specified coverage * @param ci * * @throws IOException */ public static GridCoverage2DReader getCoverageReader(CoverageInfo ci) throws IOException, Exception { // get a reader for this coverage final CoverageStoreInfo store = (CoverageStoreInfo) ci.getStore(); final GridCoverageReader reader_ = ci.getGridCoverageReader(new DefaultProgressListener(), GeoTools.getDefaultHints()); if (reader_ == null) { throw new Exception("Unable to acquire a reader for this coverage with format: " + store.getFormat().getName()); } final GridCoverage2DReader reader = (GridCoverage2DReader) reader_; return reader; } /** * Makes sure the version is present and supported * @param version */ public static void checkVersion(String version) { if(version == null) { throw new WCS20Exception("Missing version", OWS20Exception.OWSExceptionCode.MissingParameterValue, version); } if ( ! WCS20Const.V201.equals(version) && ! WCS20Const.V20.equals(version)) { throw new WCS20Exception("Could not understand version:" + version); } } /** * Makes sure the service is present and supported * @param serviceName */ public static void checkService(String serviceName) { if( serviceName == null ) { throw new WCS20Exception("Missing service name", OWS20Exception.OWSExceptionCode.MissingParameterValue, "service"); } if( ! "WCS".equals(serviceName)) { throw new WCS20Exception("Error in service name, expected value: WCS", OWS20Exception.OWSExceptionCode.InvalidParameterValue, serviceName); } } }