/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2009, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
*/
package org.geotools.arcsde.raster.info;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.geotools.coverage.grid.io.OverviewPolicy;
import org.geotools.data.DataSourceException;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.esri.sde.sdk.client.SDEPoint;
import com.esri.sde.sdk.client.SeException;
import com.esri.sde.sdk.client.SeExtent;
import com.esri.sde.sdk.client.SeRasterAttr;
/**
* A RasterInfo gathers the metadata for a single raster in a raster dataset
* <p>
* Basically, it wraps the SeRasterAttr object and implements some convenience methods for doing
* calculations with it.
* </p>
*
* @author Saul Farber
* @author Gabriel Roldan
*/
public final class RasterInfo {
/**
* Orders pyramid levels by their level index
*/
private static final Comparator<PyramidLevelInfo> levelComparator = new Comparator<PyramidLevelInfo>() {
public int compare(PyramidLevelInfo p0, PyramidLevelInfo p1) {
return (p0.getLevel() - p1.getLevel());
}
};
ArrayList<PyramidLevelInfo> pyramidList;
private int tileWidth;
private int tileHeight;
private GeneralEnvelope originalEnvelope;
private ArrayList<RasterBandInfo> bands;
private CoordinateReferenceSystem crs;
private Long rasterId;
/**
* Creates an in-memory representation of an ArcSDE Raster Pyramid. Basically it wraps the
* supplide SeRasterAttr object and implements some convenience logic for extracting
* information/ doing calculations with it.
*
* @param rasterAttributes
* the SeRasterAttr object for the raster of interest.
* @param crs
* @throws DataSourceException
*/
RasterInfo(final SeRasterAttr rasterAttributes, final CoordinateReferenceSystem crs)
throws DataSourceException {
this.crs = crs;
try {
this.rasterId = Long.valueOf(rasterAttributes.getRasterId().longValue());
// levels goes from 0 to N, maxLevel is the zero-based max index of levels
final int numLevels = rasterAttributes.getMaxLevel() + 1;
pyramidList = new ArrayList<PyramidLevelInfo>(numLevels);
tileWidth = rasterAttributes.getTileWidth();
tileHeight = rasterAttributes.getTileHeight();
for (int level = 0; level < numLevels; level++) {
if (level == 1 && rasterAttributes.skipLevelOne()) {
continue;
}
/*
* this extent corresponds to the actual image size inside the tiled grid. That is,
* to getImageWidth/Height by level. The dimensions of this extent are correct, but
* it needs to be shifted by SeRasterAttr.getExtentOffsetByLevel(level) the same way
* the grid envelope does by SeRasterAttr.getImageOffsetByLevel(level)
*/
final SeExtent slExtent = rasterAttributes.getExtentByLevel(level);
final int levelWidth = rasterAttributes.getImageWidthByLevel(level);
final int levelHeight = rasterAttributes.getImageHeightByLevel(level);
Dimension levelImageSize = new Dimension(levelWidth, levelHeight);
Point imgOffset = new Point();
// extent offset equals imgOffset * pixel resolution (ie, if resx == 2 and offsetX =
// 10, extent offset x == 20)
Point2D extOffset = new Point2D.Double();
{
SDEPoint imageOffset = rasterAttributes.getImageOffsetByLevel(level);
int xOffset = (int) (imageOffset == null ? 0 : imageOffset.getX());
int yOffset = (int) (imageOffset == null ? 0 : imageOffset.getY());
imgOffset.setLocation(xOffset, yOffset);
SDEPoint extentOffset = rasterAttributes.getExtentOffsetByLevel(level);
double xOffsetExtent = extentOffset == null ? 0D : extentOffset.getX();
double yOffsetExtent = extentOffset == null ? 0D : extentOffset.getY();
extOffset.setLocation(xOffsetExtent, yOffsetExtent);
}
final int numTilesWide = rasterAttributes.getTilesPerRowByLevel(level);
final int numTileHigh = rasterAttributes.getTilesPerColByLevel(level);
ReferencedEnvelope levelExtent = new ReferencedEnvelope(slExtent.getMinX(),
slExtent.getMaxX(), slExtent.getMinY(), slExtent.getMaxY(), crs);
addPyramidLevel(level, levelExtent, imgOffset, extOffset, numTilesWide,
numTileHigh, levelImageSize);
}
} catch (SeException se) {
throw new DataSourceException(se);
}
}
public Long getRasterId() {
return rasterId;
}
public int getTileWidth() {
return tileWidth;
}
public int getTileHeight() {
return tileHeight;
}
/**
* Don't use this constructor. It only exists for unit testing purposes.
*
* @param tileWidth
* DON'T USE
* @param tileHeight
* DON'T USE
*/
public RasterInfo(Long rasterId, int tileWidth, int tileHeight) {
this.rasterId = rasterId;
this.tileWidth = tileWidth;
this.tileHeight = tileHeight;
pyramidList = new ArrayList<PyramidLevelInfo>(4);
}
public Dimension getTileDimension() {
return new Dimension(tileWidth, tileHeight);
}
public PyramidLevelInfo getPyramidLevel(int level) {
return pyramidList.get(level);
}
public int getNumLevels() {
return pyramidList.size();
}
/**
* @param pyramidLevel
* @return resx, resy, scalefactor
*/
double[] getResolution(final int pyramidLevel) {
final double highestRes = getPyramidLevel(0).getXRes();
PyramidLevelInfo level = getPyramidLevel(pyramidLevel);
double[] resolution = new double[3];
resolution[0] = level.getXRes();
resolution[1] = level.getYRes();
resolution[2] = level.getXRes() / highestRes;
return resolution;
}
/**
* <p>
* NOTE: logic stolen and adapted from {@code AbstractGridCoverage2DReader#getOverviewImage()}
* </p>
*
* @param policy
* @return
*/
public int getOptimalPyramidLevel(final OverviewPolicy policy, final double[] requestedRes) {
int pyramidLevelChoice = 0;
// sort resolutions from smallest pixels (higher res) to biggest pixels (higher res)
// keeping a reference to the original image choice
final double[] highestRes = getResolution(0);
// Now search for the best matching resolution.
// Check also for the "perfect match"... unlikely in practice unless someone
// tunes the clients to request exactly the resolution embedded in
// the overviews, something a perf sensitive person might do in fact
// the requested resolutions
final double reqx = requestedRes[0];
final double reqy = requestedRes[1];
// requested scale factor for least reduced axis
final double requestedScaleFactorX = reqx / highestRes[0];
final double requestedScaleFactorY = reqy / highestRes[1];
final int leastReduceAxis = requestedScaleFactorX <= requestedScaleFactorY ? 0 : 1;
final double requestedScaleFactor = leastReduceAxis == 0 ? requestedScaleFactorX
: requestedScaleFactorY;
final int numLevels = getNumLevels();
// no pyramiding or are we looking for a resolution even higher than the native one?
if (0 == numLevels || requestedScaleFactor <= 1) {
pyramidLevelChoice = 0;
} else {
// are we looking for a resolution even lower than the smallest overview?
final double[] min = getResolution(numLevels - 1);
if (requestedScaleFactor >= min[2]) {
pyramidLevelChoice = numLevels - 1;
} else {
// Ok, so we know the overview is between min and max, skip the first
// and search for an overview with a resolution lower than the one requested,
// that one and the one from the previous step will bound the searched resolution
double[] prev = highestRes;
for (int levelN = 1; levelN < numLevels; levelN++) {
final double[] curr = getResolution(levelN);
// perfect match check
if (curr[2] == requestedScaleFactor) {
pyramidLevelChoice = levelN;
} else {
/*
* middle check. The first part of the condition should be sufficient, but
* there are cases where the x resolution is satisfied by the lowest
* resolution, the y by the one before the lowest (so the aspect ratio of
* the request is different than the one of the overviews), and we would end
* up going out of the loop since not even the lowest can "top" the request
* for one axis
*/
if (curr[2] > requestedScaleFactor || levelN == numLevels - 1) {
if (policy == OverviewPolicy.QUALITY) {
pyramidLevelChoice = levelN - 1;
} else if (policy == OverviewPolicy.SPEED) {
return levelN;
} else if (requestedScaleFactor - prev[2] < curr[2]
- requestedScaleFactor) {
pyramidLevelChoice = levelN - 1;
} else {
pyramidLevelChoice = levelN;
}
break;
}
prev = curr;
}
}
}
}
// fallback
return pyramidLevelChoice;
}
/**
* Don't use this method. It's only public for unit testing purposes.
*
* @param level
* the zero-based level index for the new level
* @param extent
* the geographical extent the level covers, may need to be offsetted by {@code
* extOffset}
* @param imgOffset
* the offset on the X and Y axes of the actual image inside the tile space for this
* level
* @param extOffset
* the offset on the X and Y axes of the actual image inside the tile space for this
* level
* @param numTilesWide
* the number of tiles that make up the level on the X axis
* @param numTilesHigh
* the number of tiles that make up the level on the Y axis
* @param imageSize
* the size of the actual image in pixels
*/
public void addPyramidLevel(int level, ReferencedEnvelope extent, Point imgOffset,
Point2D extOffset, int numTilesWide, int numTilesHigh, Dimension imageSize) {
PyramidLevelInfo pyramidLevel;
pyramidLevel = new PyramidLevelInfo(level, extent, imgOffset, extOffset, numTilesWide,
numTilesHigh, imageSize);
pyramidList.add(pyramidLevel);
Collections.sort(pyramidList, levelComparator);
}
void setOriginalEnvelope(GeneralEnvelope originalEnvelope) {
this.originalEnvelope = originalEnvelope;
}
public GeneralEnvelope getOriginalEnvelope() {
return originalEnvelope;
}
public void setBands(List<RasterBandInfo> bands) {
this.bands = new ArrayList<RasterBandInfo>(bands);
}
public List<RasterBandInfo> getBands() {
return new ArrayList<RasterBandInfo>(bands);
}
public int getNumBands() {
return bands.size();
}
public RasterBandInfo getBand(final int index) {
return bands.get(index);
}
public CoordinateReferenceSystem getCoordinateReferenceSystem() {
return crs;
}
public RasterCellType getTargetCellType() {
// if (isColorMapped()) {
// // color map is already promoted if needed
// return getNativeCellType();
// }
List<Number> noDataValues = getNoDataValues();
RasterCellType nativeCellType = getNativeCellType();
RasterCellType targetCellType = RasterUtils.determineTargetCellType(nativeCellType,
noDataValues);
return targetCellType;
}
public boolean isColorMapped() {
return getBand(0).isColorMapped();
}
public RasterCellType getNativeCellType() {
return getBand(0).getCellType();
}
public List<Number> getNoDataValues() {
final List<Number> noDataValues = new ArrayList<Number>();
for (RasterBandInfo band : getBands()) {
Number noDataValue = band.getNoDataValue();
noDataValues.add(noDataValue);
}
return noDataValues;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(getClass().getSimpleName());
sb.append("[Id: ").append(getRasterId());
String srs = null;
try {
srs = CRS.lookupIdentifier(getCoordinateReferenceSystem(), false);
} catch (FactoryException e) {
e.printStackTrace();
}
sb.append(", bands: ").append(getNumBands());
sb.append(", levels: ").append(getNumLevels());
sb.append(", tile size: ").append(getTileWidth()).append("x").append(getTileHeight());
sb.append(", crs: ").append(srs == null ? getCoordinateReferenceSystem().toWKT() : srs);
GeneralEnvelope env = getOriginalEnvelope();
sb.append(", Envelope: ").append(env.getMinimum(0)).append(",").append(env.getMinimum(1))
.append(" ").append(env.getMaximum(0)).append(",").append(env.getMaximum(1));
sb.append("]\n Bands[");
for (RasterBandInfo band : getBands()) {
sb.append("\n\t");
sb.append(band.toString());
}
sb.append("\n ]");
sb.append("\n Pyramid[");
for (int l = 0; l < getNumLevels(); l++) {
sb.append("\n\t").append(getPyramidLevel(l).toString());
}
sb.append("\n ]");
return sb.toString();
}
}