From 56d375da862902b484f547f465521386ea923635 Mon Sep 17 00:00:00 2001 From: Ben Steffensmeier Date: Mon, 19 Nov 2012 11:25:05 -0600 Subject: [PATCH] Issue #189 grid retrieval api. Change-Id: Ice9432ab703420c8d441860de085a8d34a2610c8 Former-commit-id: e0b91523c54227ef15588f5dd672b2450a9dd8ac --- .../META-INF/MANIFEST.MF | 20 +- .../grid/datastorage/GridDataRetriever.java | 358 ++++++++++++++++++ .../data/AbstractDataWrapper.java | 88 +---- .../util/GridGeometryWrapChecker.java | 158 ++++++++ 4 files changed, 531 insertions(+), 93 deletions(-) create mode 100644 edexOsgi/com.raytheon.uf.common.dataplugin.grid/src/com/raytheon/uf/common/dataplugin/grid/datastorage/GridDataRetriever.java create mode 100644 edexOsgi/com.raytheon.uf.common.geospatial/src/com/raytheon/uf/common/geospatial/util/GridGeometryWrapChecker.java diff --git a/edexOsgi/com.raytheon.uf.common.dataplugin.grid/META-INF/MANIFEST.MF b/edexOsgi/com.raytheon.uf.common.dataplugin.grid/META-INF/MANIFEST.MF index 987d36ec4c..d562304799 100644 --- a/edexOsgi/com.raytheon.uf.common.dataplugin.grid/META-INF/MANIFEST.MF +++ b/edexOsgi/com.raytheon.uf.common.dataplugin.grid/META-INF/MANIFEST.MF @@ -10,29 +10,33 @@ Eclipse-BuddyPolicy: registered, ext, global Import-Package: com.raytheon.uf.common.dataplugin, com.raytheon.uf.common.dataplugin.annotations, com.raytheon.uf.common.dataplugin.persist, - com.raytheon.uf.common.datastorage.records, com.raytheon.uf.common.geospatial, - com.raytheon.uf.common.gridcoverage, com.raytheon.uf.common.localization, - com.raytheon.uf.common.parameter, com.raytheon.uf.common.serialization, com.raytheon.uf.common.serialization.annotations, com.raytheon.uf.common.serialization.comm, com.raytheon.uf.common.status, com.raytheon.uf.common.time, com.vividsolutions.jts.geom, - javax.measure.unit, javax.persistence, org.hibernate.annotations, org.opengis.metadata.spatial Export-Package: com.raytheon.uf.common.dataplugin.grid, + com.raytheon.uf.common.dataplugin.grid.dataquery, com.raytheon.uf.common.dataplugin.grid.dataset, + com.raytheon.uf.common.dataplugin.grid.datastorage, com.raytheon.uf.common.dataplugin.grid.mapping, com.raytheon.uf.common.dataplugin.grid.request, com.raytheon.uf.common.dataplugin.grid.units, com.raytheon.uf.common.dataplugin.grid.util -Require-Bundle: com.raytheon.uf.common.comm;bundle-version="1.12.1174", - com.raytheon.uf.common.dataplugin.level;bundle-version="1.12.1174", +Require-Bundle: javax.measure, + com.raytheon.uf.common.geospatial, + org.geotools, + com.raytheon.uf.common.localization, + com.raytheon.uf.common.datastorage, + com.raytheon.uf.common.gridcoverage;bundle-version="1.0.0", com.raytheon.uf.common.parameter;bundle-version="1.0.0", - com.raytheon.uf.common.util;bundle-version="1.12.1174", - com.raytheon.uf.common.dataquery;bundle-version="1.0.0" + com.raytheon.uf.common.comm;bundle-version="1.12.1174", + com.raytheon.uf.common.dataquery;bundle-version="1.0.0", + com.raytheon.uf.common.dataplugin.level;bundle-version="1.12.1174", + com.raytheon.uf.common.util;bundle-version="1.12.1174" diff --git a/edexOsgi/com.raytheon.uf.common.dataplugin.grid/src/com/raytheon/uf/common/dataplugin/grid/datastorage/GridDataRetriever.java b/edexOsgi/com.raytheon.uf.common.dataplugin.grid/src/com/raytheon/uf/common/dataplugin/grid/datastorage/GridDataRetriever.java new file mode 100644 index 0000000000..cd99edc7ef --- /dev/null +++ b/edexOsgi/com.raytheon.uf.common.dataplugin.grid/src/com/raytheon/uf/common/dataplugin/grid/datastorage/GridDataRetriever.java @@ -0,0 +1,358 @@ +/** + * This software was developed and / or modified by Raytheon Company, + * pursuant to Contract DG133W-05-CQ-1067 with the US Government. + * + * U.S. EXPORT CONTROLLED TECHNICAL DATA + * This software product contains export-restricted data whose + * export/transfer/disclosure is restricted by U.S. law. Dissemination + * to non-U.S. persons whether in the United States or abroad requires + * an export license or other authorization. + * + * Contractor Name: Raytheon Company + * Contractor Address: 6825 Pine Street, Suite 340 + * Mail Stop B8 + * Omaha, NE 68106 + * 402.291.0100 + * + * See the AWIPS II Master Rights File ("Master Rights File.pdf") for + * further licensing information. + **/ +package com.raytheon.uf.common.dataplugin.grid.datastorage; + +import java.io.File; +import java.io.FileNotFoundException; + +import javax.measure.converter.ConversionException; +import javax.measure.converter.UnitConverter; +import javax.measure.unit.Unit; + +import org.geotools.coverage.grid.GridGeometry2D; +import org.opengis.metadata.spatial.PixelOrientation; + +import com.raytheon.uf.common.dataplugin.annotations.DataURI; +import com.raytheon.uf.common.dataplugin.grid.GridPathProvider; +import com.raytheon.uf.common.dataplugin.grid.GridRecord; +import com.raytheon.uf.common.dataplugin.persist.IHDFFilePathProvider; +import com.raytheon.uf.common.datastorage.DataStoreFactory; +import com.raytheon.uf.common.datastorage.IDataStore; +import com.raytheon.uf.common.datastorage.Request; +import com.raytheon.uf.common.datastorage.StorageException; +import com.raytheon.uf.common.datastorage.records.FloatDataRecord; +import com.raytheon.uf.common.datastorage.records.IDataRecord; +import com.raytheon.uf.common.geospatial.MapUtil; +import com.raytheon.uf.common.geospatial.util.GridGeometryWrapChecker; +import com.raytheon.uf.common.gridcoverage.GridCoverage; +import com.raytheon.uf.common.gridcoverage.LambertConformalGridCoverage; +import com.raytheon.uf.common.gridcoverage.LatLonGridCoverage; +import com.raytheon.uf.common.gridcoverage.MercatorGridCoverage; +import com.raytheon.uf.common.gridcoverage.PolarStereoGridCoverage; +import com.raytheon.uf.common.gridcoverage.exception.GridCoverageException; +import com.raytheon.uf.common.gridcoverage.subgrid.SubGrid; +import com.raytheon.uf.common.localization.IPathManager; +import com.raytheon.uf.common.localization.msgs.GetServersRequest; +import com.raytheon.uf.common.localization.msgs.GetServersResponse; +import com.raytheon.uf.common.parameter.lookup.ParameterLookup; +import com.raytheon.uf.common.serialization.comm.RequestRouter; +import com.vividsolutions.jts.geom.Coordinate; + +/** + * Convenience class for requesting grid data. This class provides automatic + * unit conversion and also the ability to allow worldwide data to overlap. + * + *
+ * 
+ * SOFTWARE HISTORY
+ * 
+ * Date         Ticket#    Engineer    Description
+ * ------------ ---------- ----------- --------------------------
+ * Nov 14, 2012            bsteffen     Initial creation
+ * 
+ * 
+ * + * @author bsteffen + * @version 1.0 + */ + +public class GridDataRetriever { + + protected static String serverDataDir; + + protected GridRecord record; + + protected GridCoverage requestCoverage; + + protected Request request = Request.ALL; + + protected int worldWrapColumns = -1; + + protected Unit unit; + + protected UnitConverter converter; + + /** + * Construct a GridDataRetriever that will retrieve data for the provided + * GridRecord. + * + * @param record + * GridRecord for which data is retrieved. + */ + public GridDataRetriever(GridRecord record) { + this.record = record; + } + + /** + * Construct a GridDataRetriever that will retrieve data for the provided + * dataURI. The dataURI must be a valid dataURI for a grid record. + * + * @param dataUri + * dataURI for grid data to retrieve. + */ + public GridDataRetriever(String dataUri) { + this.record = new GridRecord(dataUri); + this.record.setParameter(ParameterLookup.getInstance().getParameter( + record.getParameter().getAbbreviation())); + } + + /** + * For data that wraps around the world, specify the number of redundant + * columns to add. For data that does not wrap around the world this method + * call has no affect and the full data set will be retrieved. + * + * @param worldWrapColumns + * The number of redundant columns to include in the x direction + * for wrapping grids. + * @return a boolean true if the data is world wrapping, false if not(in + * which case this method call has no affect). + * @throws GridCoverageException + * thrown when there are errors constructing a wrapping + * GridCoverage + */ + public boolean setWorldWrapColumns(int worldWrapColumns) + throws GridCoverageException { + GridCoverage dataLoc = record.getLocation(); + GridGeometry2D dataGeom = dataLoc.getGridGeometry(); + int wrapX = GridGeometryWrapChecker.checkForWrapping(dataGeom); + if (wrapX != -1) { + int newX = wrapX + worldWrapColumns; + if (newX == dataLoc.getNx()) { + this.request = Request.ALL; + } else if (newX < dataLoc.getNx()) { + Coordinate upperRight = new Coordinate(newX - 1, 0); + upperRight = MapUtil.gridCoordinateToLatLon(upperRight, + PixelOrientation.CENTER, dataLoc); + setRequestArea(dataLoc.getLowerLeftLon(), + dataLoc.getLowerLeftLat(), upperRight.x, upperRight.y); + } else { + this.request = Request.ALL; + if (dataLoc instanceof LatLonGridCoverage) { + LatLonGridCoverage newLoc = new LatLonGridCoverage( + (LatLonGridCoverage) dataLoc); + newLoc.setLa2(0); + newLoc.setLo2(0); + requestCoverage = newLoc; + } else if (dataLoc instanceof MercatorGridCoverage) { + MercatorGridCoverage newLoc = new MercatorGridCoverage( + (MercatorGridCoverage) dataLoc); + newLoc.setLa2(null); + newLoc.setLo2(null); + requestCoverage = newLoc; + } else if (dataLoc instanceof LambertConformalGridCoverage) { + requestCoverage = new LambertConformalGridCoverage( + (LambertConformalGridCoverage) dataLoc); + } else if (dataLoc instanceof PolarStereoGridCoverage) { + // I really doubt it is possible to world wrap a + // PolarStereoCoverage, but just in case... + requestCoverage = new PolarStereoGridCoverage( + (PolarStereoGridCoverage) dataLoc); + } else { + throw new GridCoverageException( + "Cannot wrap data for projection of type " + + dataLoc.getClass().getName()); + } + requestCoverage.setNx(newX); + requestCoverage.setGridGeometry(null); + requestCoverage.initialize(); + this.worldWrapColumns = newX - dataLoc.getNx(); + } + return true; + } else { + return false; + } + } + + /** + * Set the requested area to be a subgrid of the total area. This uses the + * trim functionality of GridCoverage to generate an area which has corners + * at the provided latitude and longitude. + * + * @param lon1 + * @param lat1 + * @param lon2 + * @param lat2 + * @throws GridDataRetrievalException + * @throws GridCoverageException + */ + protected void setRequestArea(double lon1, double lat1, double lon2, + double lat2) throws GridCoverageException { + SubGrid subGrid = new SubGrid(); + subGrid.setLowerLeftLat(Math.min(lat1, lat2)); + subGrid.setLowerLeftLon(Math.min(lat1, lat2)); + subGrid.setUpperRightLat(Math.min(lat1, lat2)); + subGrid.setUpperRightLon(Math.min(lat1, lat2)); + requestCoverage = record.getLocation().trim(subGrid); + int[] minIndex = { subGrid.getUpperLeftX(), subGrid.getUpperLeftY() }; + int[] maxIndex = { subGrid.getUpperLeftX() + subGrid.getNX(), + subGrid.getUpperLeftY() + subGrid.getNY() }; + request = Request.buildSlab(minIndex, maxIndex); + requestCoverage.initialize(); + } + + /** + * Set the desired unit for the data being retrieved. If this method is + * successful the data will automatically be converted when it is retrieved. + * + * @param unit + * the desired unit for the data. + * @throws ConversionException + * occurs when the provided unit is incompatible with the + * storage unit of the data. + */ + public void setUnit(Unit unit) throws ConversionException { + this.converter = record.getParameter().getUnit().getConverterTo(unit); + this.unit = unit; + } + + /** + * retrieve the grid data. This method will automatically perform any + * requested unit conversion or world wrap adjustments. + * + * @return FloatDataRecord containing the raw values for this grid. + * @throws StorageException + * if anything goes wrong while retrieving the data, for example + * if the data cannot be found in the datastore or if there are + * problems communicating with the servers. + */ + public FloatDataRecord getDataRecord() throws StorageException { + IDataStore ds = DataStoreFactory.getDataStore(findStorageLocation()); + IDataRecord dataRecord; + try { + dataRecord = ds.retrieve(getGroup(), "Data", request); + } catch (FileNotFoundException e) { + throw new StorageException(e.getLocalizedMessage(), null, e); + } + FloatDataRecord floatRecord = (FloatDataRecord) dataRecord; + boolean cloned = false; + if (converter != null) { + if (!cloned) { + floatRecord = (FloatDataRecord) floatRecord.clone(); + cloned = true; + } + float[] data = floatRecord.getFloatData(); + for (int i = 0; i < data.length; i += 1) { + if (data[i] <= -9999) { + // -9999 and -999999 are both commonly used no data values, + // and rarely are such extremes valid. No Data Value. + data[i] = data[i]; + } else { + data[i] = (float) converter.convert(data[i]); + } + } + } + if (worldWrapColumns > 0) { + if (!cloned) { + floatRecord = (FloatDataRecord) floatRecord.clone(); + cloned = true; + } + int nx = (int) floatRecord.getSizes()[0]; + int newNx = nx + worldWrapColumns; + int ny = (int) floatRecord.getSizes()[1]; + float[] oldData = floatRecord.getFloatData(); + float[] newData = new float[newNx * ny]; + for (int y = 0; y < ny; y += 1) { + // this is in a loop so it works correctly when extraColumns > + // nx + for (int numCopied = 0; numCopied < newNx; numCopied += nx) { + System.arraycopy(oldData, y * nx, newData, y * newNx + + numCopied, Math.min(nx, newNx - numCopied)); + } + } + floatRecord.setSizes(new long[] { newNx, ny }); + floatRecord.setFloatData(newData); + + } + return floatRecord; + } + + /** + * Get a GridCoverage describing the spatial area covered by the retrieved + * data. If setWorldWrapColumns or setRequestArea was used then this will + * return a coverage describing the modified area, otherwise this is the + * same as the coverage in the GridRecord. + * + * @return grid coverage of the data record. + */ + public GridCoverage getCoverage() { + if (requestCoverage == null) { + return record.getLocation(); + } else { + return requestCoverage; + } + } + + /** + * Get a unit object describing the retrieved data. If setUnit was + * successfully used then this will return the same unit, otherwise it + * returns the storage unit of the data. + * + * @return unit of the data record + */ + public Unit getUnit() { + if (unit == null) { + return record.getParameter().getUnit(); + } + return unit; + } + + private String getGroup() { + if (GridPathProvider.STATIC_PARAMETERS.contains(record.getParameter() + .getAbbreviation())) { + return DataURI.SEPARATOR + record.getLocation().getId() + + DataURI.SEPARATOR + + record.getParameter().getAbbreviation(); + } else { + return record.getDataURI(); + } + } + + private File findStorageLocation() throws StorageException { + IHDFFilePathProvider pathProvider = record.getHDFPathProvider(); + + String path = pathProvider.getHDFPath(record.getPluginName(), record); + String fileName = pathProvider.getHDFFileName(record.getPluginName(), + record); + + return new File(getServerDataDir() + IPathManager.SEPARATOR + + record.getPluginName() + IPathManager.SEPARATOR + path + + IPathManager.SEPARATOR + fileName); + } + + private static synchronized String getServerDataDir() + throws StorageException { + if (serverDataDir == null) { + // TODO cave already knows the server data dir in VizApp, and edex + // has it in system properties but we can't access either because + // this is common code, architecturally we need some way around + // this. For now this will send it's own request which is slightly + // wasteful but not terribly harmful. + try { + GetServersResponse response = (GetServersResponse) RequestRouter + .route(new GetServersRequest()); + serverDataDir = response.getServerDataDir(); + } catch (Exception e) { + throw new StorageException("Error communicating with server.", + null, e); + } + } + return serverDataDir; + } +} diff --git a/edexOsgi/com.raytheon.uf.common.geospatial/src/com/raytheon/uf/common/geospatial/interpolation/data/AbstractDataWrapper.java b/edexOsgi/com.raytheon.uf.common.geospatial/src/com/raytheon/uf/common/geospatial/interpolation/data/AbstractDataWrapper.java index 0718924553..5517e44dda 100644 --- a/edexOsgi/com.raytheon.uf.common.geospatial/src/com/raytheon/uf/common/geospatial/interpolation/data/AbstractDataWrapper.java +++ b/edexOsgi/com.raytheon.uf.common.geospatial/src/com/raytheon/uf/common/geospatial/interpolation/data/AbstractDataWrapper.java @@ -20,12 +20,8 @@ package com.raytheon.uf.common.geospatial.interpolation.data; import org.geotools.coverage.grid.GeneralGridGeometry; -import org.geotools.geometry.DirectPosition2D; -import org.geotools.referencing.CRS; -import org.geotools.referencing.crs.DefaultGeographicCRS; -import org.opengis.referencing.crs.CoordinateReferenceSystem; -import org.opengis.referencing.datum.PixelInCell; -import org.opengis.referencing.operation.MathTransform; + +import com.raytheon.uf.common.geospatial.util.GridGeometryWrapChecker; /** * @@ -63,7 +59,7 @@ public abstract class AbstractDataWrapper implements DataSource, public AbstractDataWrapper(GeneralGridGeometry geometry) { this.nx = geometry.getGridRange().getSpan(0); this.ny = geometry.getGridRange().getSpan(1); - checkForWrapping(geometry); + this.wrapX = GridGeometryWrapChecker.checkForWrapping(geometry); } public AbstractDataWrapper(int nx, int ny) { @@ -117,82 +113,4 @@ public abstract class AbstractDataWrapper implements DataSource, protected abstract void setDataValueInternal(double dataValue, int x, int y); - // Attempt to detect the case where a geographic coordinate reference - // system wraps around the world so that values out of range on the - // X-axis can be retrieved from the other side of the grid. If this is - // the case the sourceWrapX value will be set to the number of grid - // cells that are needed to wrap all the way around the world. - protected void checkForWrapping(GeneralGridGeometry geometry) { - try { - CoordinateReferenceSystem sourceCRS = geometry - .getCoordinateReferenceSystem(); - MathTransform grid2crs = geometry - .getGridToCRS(PixelInCell.CELL_CENTER); - MathTransform crs2LatLon = CRS.findMathTransform(sourceCRS, - DefaultGeographicCRS.WGS84); - // Although theoretically any two points would yield the same - // result, nx is chosen as the x coordinate because it overcomes - // some of the normalization performed in geotools math transforms. - // For most math transforms, geotools will normalize the LatLon - // values into into the range centralMeridian +/- 180. In these - // cases the 360 degree shift performed later is completely - // worthless since geotools will normalize the points into the - // same range regardless of how it is shifted. For a worldwide grid - // the nx coordinate is guaranteed to be just slightly over the - // normalized range, so that when the transform normalizes it will - // use a point that is -360 degrees away from the original point. - // This means that even though the -360 degree shift we apply is - // worthless, the normalization process actually applies an - // equivelant shift that yields the correct result. The end result - // is that whether the transform normalizes or not there is always a - // shift of -360 degrees for all worldwide grids. - - // Start with two points in grid space, one on each corner side of - // the y direction. - DirectPosition2D corner1 = new DirectPosition2D(nx, 0); - DirectPosition2D corner2 = new DirectPosition2D(nx, ny - 1); - // transform the points to crs space. - grid2crs.transform(corner1, corner1); - grid2crs.transform(corner2, corner2); - // and then to latLon space - crs2LatLon.transform(corner1, corner1); - crs2LatLon.transform(corner2, corner2); - // shift the points by 360 degrees because the goal is to know how - // many grid cells exist in one 360 roll of longitude. - corner1.x = corner1.x - 360; - corner2.x = corner2.x - 360; - // transform back to crs. - crs2LatLon.inverse().transform(corner1, corner1); - crs2LatLon.inverse().transform(corner2, corner2); - // and back to grid space - grid2crs.inverse().transform(corner1, corner1); - grid2crs.inverse().transform(corner2, corner2); - // the difference between the starting x value and the current x - // value is the number - int sourceWrapX = (int) (nx - corner1.x); - // In order to wrap then the transformed point x value should be - // on the other side of the grid and the y value should not have - // changed significantly. Additionally the wrapped x value - // should fall exactly on a grid cell. - if (corner1.x > nx - 1) { - return; - } else if (Math.abs(corner1.y - 0) > 0.0001) { - return; - } else if (Math.abs(corner2.y - ny + 1) > 0.0001) { - return; - } else if (Math.abs(corner1.x + sourceWrapX - nx) > 0.0001) { - return; - } else if (Math.abs(corner2.x + sourceWrapX - nx) > 0.0001) { - return; - } else { - this.wrapX = sourceWrapX; - } - - } catch (Exception e) { - // if anything goes wrong in this process just assume we don't - // wrap the x axis, thats not a big deal and it is normal for - // non geographic coordinate systems. - ; - } - } } \ No newline at end of file diff --git a/edexOsgi/com.raytheon.uf.common.geospatial/src/com/raytheon/uf/common/geospatial/util/GridGeometryWrapChecker.java b/edexOsgi/com.raytheon.uf.common.geospatial/src/com/raytheon/uf/common/geospatial/util/GridGeometryWrapChecker.java new file mode 100644 index 0000000000..86aa0c9235 --- /dev/null +++ b/edexOsgi/com.raytheon.uf.common.geospatial/src/com/raytheon/uf/common/geospatial/util/GridGeometryWrapChecker.java @@ -0,0 +1,158 @@ +/** + * This software was developed and / or modified by Raytheon Company, + * pursuant to Contract DG133W-05-CQ-1067 with the US Government. + * + * U.S. EXPORT CONTROLLED TECHNICAL DATA + * This software product contains export-restricted data whose + * export/transfer/disclosure is restricted by U.S. law. Dissemination + * to non-U.S. persons whether in the United States or abroad requires + * an export license or other authorization. + * + * Contractor Name: Raytheon Company + * Contractor Address: 6825 Pine Street, Suite 340 + * Mail Stop B8 + * Omaha, NE 68106 + * 402.291.0100 + * + * See the AWIPS II Master Rights File ("Master Rights File.pdf") for + * further licensing information. + **/ +package com.raytheon.uf.common.geospatial.util; + +import org.geotools.coverage.grid.GeneralGridGeometry; +import org.geotools.geometry.DirectPosition2D; +import org.geotools.referencing.CRS; +import org.geotools.referencing.crs.DefaultGeographicCRS; +import org.opengis.referencing.FactoryException; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.datum.PixelInCell; +import org.opengis.referencing.operation.MathTransform; +import org.opengis.referencing.operation.TransformException; + +/** + * Provide utility methods for determining if a grid geometry is world wide and + * wrapping so that out of range on the X-axis can be retrieved from the other + * side of the grid. + * + *
+ * 
+ * SOFTWARE HISTORY
+ * 
+ * Date         Ticket#    Engineer    Description
+ * ------------ ---------- ----------- --------------------------
+ * Oct 5, 2012            bsteffen     Initial creation
+ * 
+ * 
+ * + * @author bsteffen + * @version 1.0 + */ + +public class GridGeometryWrapChecker { + + private static double TOLERANCE = 0.00001; + + /** + * + * Attempt to detect the case where a geographic coordinate reference system + * wraps around the world so that values out of range on the X-axis can be + * retrieved from the other side of the grid. + * + * If the provided grid geometry wraps than the result will be a positive + * value representing the number of grid cells used to wrap around the + * world. The return value can be used to extend the range of the grid + * infinitely in either direction by reusing column values at an interval of + * the return value. For example if a grid normally has an x range from 0 to + * 359 and the result of this method is 360 then the range can be extended + * to 360 by reusing the values from the 0 column, in fact any integer + * column can be referenced by simply taking columnNumber % 360 to get a + * value within the original valid range. + * + * If the grid does not wrap around then the result is -1; + * + * @param geometry + * @return + */ + public static int checkForWrapping(GeneralGridGeometry geometry) { + try { + int nx = geometry.getGridRange().getSpan(0); + int ny = geometry.getGridRange().getSpan(1); + CoordinateReferenceSystem sourceCRS = geometry + .getCoordinateReferenceSystem(); + MathTransform grid2crs = geometry + .getGridToCRS(PixelInCell.CELL_CENTER); + MathTransform crs2LatLon = CRS.findMathTransform(sourceCRS, + DefaultGeographicCRS.WGS84); + MathTransform latLon2crs = crs2LatLon.inverse(); + MathTransform crs2grid = grid2crs.inverse(); + // Although theoretically any two points would yield the same + // result, nx is chosen as the x coordinate because it overcomes + // some of the normalization performed in geotools math transforms. + // For most math transforms, geotools will normalize the LatLon + // values into into the range centralMeridian +/- 180. In these + // cases the 360 degree shift performed later is completely + // worthless since geotools will normalize the points into the + // same range regardless of how it is shifted. For a worldwide grid + // the nx coordinate is guaranteed to be just slightly over the + // normalized range, so that when the transform normalizes it will + // use a point that is -360 degrees away from the original point. + // This means that even though the -360 degree shift we apply is + // worthless, the normalization process actually applies an + // equivelant shift that yields the correct result. The end result + // is that whether the transform normalizes or not there is always a + // shift of -360 degrees for all worldwide grids. + + // Start with two points in grid space, one on each corner side of + // the y direction. + DirectPosition2D corner1 = new DirectPosition2D(nx, 0); + DirectPosition2D corner2 = new DirectPosition2D(nx, ny - 1); + // transform the points to crs space. + grid2crs.transform(corner1, corner1); + grid2crs.transform(corner2, corner2); + // and then to latLon space + crs2LatLon.transform(corner1, corner1); + crs2LatLon.transform(corner2, corner2); + // shift the points by 360 degrees because the goal is to know how + // many grid cells exist in one 360 roll of longitude. + corner1.x = corner1.x - 360; + corner2.x = corner2.x - 360; + // transform back to crs. + latLon2crs.transform(corner1, corner1); + latLon2crs.transform(corner2, corner2); + // and back to grid space + crs2grid.transform(corner1, corner1); + crs2grid.transform(corner2, corner2); + // the difference between the starting x value and the current x + // value is the number + int sourceWrapX = (int) (nx - corner1.x); + // In order to wrap then the transformed point x value should be + // on the other side of the grid and the y value should not have + // changed significantly. Additionally the wrapped x value + // should fall exactly on a grid cell. + if (corner1.x > nx - 1) { + return -1; + } else if (Math.abs(corner1.y - 0) > TOLERANCE) { + return -1; + } else if (Math.abs(corner2.y - ny + 1) > TOLERANCE) { + return -1; + } else if (Math.abs(corner1.x + sourceWrapX - nx) > TOLERANCE) { + return -1; + } else if (Math.abs(corner2.x + sourceWrapX - nx) > TOLERANCE) { + return -1; + } else { + return sourceWrapX; + } + } catch (FactoryException e) { + // Every known crs which can world wrap does not throw factory + // exceptions when getting transformation so if an exception is + // thrown assume it is because the grid does not world wrap. + return -1; + } catch (TransformException e) { + // this exception is expected for some grids that do not world + // wrap, often because while checking transformation is performed + // for invalid coordinates. + return -1; + } + } + +}