/* * 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.io; import java.awt.Dimension; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import org.geotools.arcsde.ArcSdeException; import org.geotools.arcsde.raster.info.RasterCellType; import org.geotools.arcsde.raster.info.RasterDatasetInfo; import org.geotools.arcsde.session.Command; import org.geotools.arcsde.session.ISession; import org.geotools.arcsde.session.ISessionPool; import org.geotools.arcsde.session.UnavailableConnectionException; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.util.logging.Logging; import org.opengis.coverage.grid.GridEnvelope; import com.esri.sde.sdk.client.SeConnection; import com.esri.sde.sdk.client.SeException; import com.esri.sde.sdk.client.SeQuery; import com.esri.sde.sdk.client.SeRaster; import com.esri.sde.sdk.client.SeRasterConstraint; import com.esri.sde.sdk.client.SeRasterTile; import com.esri.sde.sdk.client.SeRow; import com.esri.sde.sdk.client.SeSqlConstruct; /** * Offers an iterator like interface to fetch ArcSDE raster tiles. * * @author Gabriel Roldan (OpenGeo) * @since 2.5.4 * @version $Id$ * @source $URL: * http://svn.osgeo.org/geotools/trunk/modules/plugin/arcsde/datastore/src/main/java/org * /geotools/arcsde/gce/NativeTileReader.java $ */ @SuppressWarnings({ "nls" }) final class NativeTileReader implements TileReader { private static final Logger LOGGER = Logging.getLogger("org.geotools.arcsde.gce"); private final RasterDatasetInfo rasterInfo; private final long rasterId; private final int pyramidLevel; private final GridEnvelope requestedTiles; private final ISessionPool sessionPool; private ISession session; private boolean started; private final int pixelsPerTile; private final int tileDataLength; private int bitsPerSample; private final RasterCellType nativeCellType; private QueryObjects queryObjects; private final TileDataFetcher dataFetcher; /** * @see DefaultTiledRasterReader#nextRaster() */ private static class QueryRasterCommand extends Command<Void> { private SeQuery preparedQuery; private SeRow row; private final SeRasterConstraint rasterConstraint; private final String rasterColumn; private final String rasterTable; private final long rasterId; /** * * @param rConstraint * indicates which bands, pyramid level and grid envelope to query * @param rasterTable * indicates which raster table to query * @param rasterColumn * indicates what raster column in the raster table to query * @param rasterId * indicates which raster in the raster catalog to query */ public QueryRasterCommand(final SeRasterConstraint rConstraint, final String rasterTable, final String rasterColumn, final long rasterId) { this.rasterConstraint = rConstraint; this.rasterTable = rasterTable; this.rasterColumn = rasterColumn; this.rasterId = rasterId; } @Override public Void execute(ISession session, SeConnection connection) throws SeException, IOException { final SeSqlConstruct sqlConstruct = new SeSqlConstruct(rasterTable); /* * Filter by the given raster id */ final String rasterIdFilter = rasterColumn + " = " + rasterId; sqlConstruct.setWhere(rasterIdFilter); final String[] rasterColumns = { rasterColumn }; preparedQuery = new SeQuery(connection, rasterColumns, sqlConstruct); preparedQuery.prepareQuery(); preparedQuery.execute(); this.row = preparedQuery.fetch(); if (row == null) { return null; } preparedQuery.queryRasterTile(rasterConstraint); return null; } public SeQuery getPreparedQuery() { return preparedQuery; } public SeRow getSeRow() { return row; } } /** * Creates a {@link TileReader} that reads tiles out of ArcSDE for the given * {@code preparedQuery} and {@code SeRow} using the given {@code session}, in the native raster * format. * <p> * As for any object that receives a {@link ISession session}, the same rule applies: this class * is not responsible of {@link ISession#dispose() disposing} the session, but the calling code * is. * </p> * * @param preparedQuery * the query stream to close when done * @param row * @param imageDimensions * the image size, x and y are the offsets, width and height the actual width and * height, used to ignore incomming pixel data as appropriate to fit the image * dimensions * @param bitsPerSample * @param numberOfBands2 * @param tileRange */ NativeTileReader(final ISessionPool sessionPool, final RasterDatasetInfo rasterInfo, final long rasterId, final int pyramidLevel, final GridEnvelope tileRange) { this.sessionPool = sessionPool; this.rasterInfo = rasterInfo; this.rasterId = rasterId; this.pyramidLevel = pyramidLevel; this.requestedTiles = tileRange; final Dimension tileSize = rasterInfo.getTileDimension(rasterId); this.pixelsPerTile = tileSize.width * tileSize.height; this.nativeCellType = rasterInfo.getNativeCellType(); this.bitsPerSample = nativeCellType.getBitsPerSample(); this.tileDataLength = (int) Math .ceil(((double) pixelsPerTile * (double) bitsPerSample) / 8D); final RasterCellType targetCellType = rasterInfo.getTargetCellType(rasterId); this.dataFetcher = TileDataFetcher.getTileDataFetcher(this.nativeCellType, targetCellType); int rasterIndex = rasterInfo.getRasterIndex(rasterId); int maxTileX = rasterInfo.getNumTilesWide(rasterIndex, pyramidLevel) - 1; int maxTileY = rasterInfo.getNumTilesHigh(rasterIndex, pyramidLevel) - 1; if (tileRange.getLow(0) < 0 || tileRange.getLow(1) < 0 || tileRange.getHigh(0) > maxTileX || tileRange.getHigh(1) > maxTileY) { throw new IllegalArgumentException("Invalid tile range for raster #" + rasterId + "/" + pyramidLevel + ": " + tileRange + ". Valid range is 0.." + maxTileX + " 0.." + maxTileY); } } /** * @see org.geotools.arcsde.raster.io.TileReader#getBitsPerSample() */ public int getBitsPerSample() { return bitsPerSample; } /** * @see org.geotools.arcsde.raster.io.TileReader#getPixelsPerTile() */ public int getPixelsPerTile() { return pixelsPerTile; } /** * @see org.geotools.arcsde.raster.io.TileReader#getNumberOfBands() */ public int getNumberOfBands() { return rasterInfo.getNumBands(); } /** * @see org.geotools.arcsde.raster.io.TileReader#getTileWidth() */ public int getTileWidth() { return rasterInfo.getTileWidth(rasterId); } /** * @see org.geotools.arcsde.raster.io.TileReader#getTileHeight() */ public int getTileHeight() { return rasterInfo.getTileHeight(rasterId); } /** * @see org.geotools.arcsde.raster.io.TileReader#getTilesWide() */ public int getTilesWide() { return requestedTiles.getSpan(0); } /** * @see org.geotools.arcsde.raster.io.TileReader#getTilesHigh() */ public int getTilesHigh() { return requestedTiles.getSpan(1); } /** * @see org.geotools.arcsde.raster.io.TileReader#getBytesPerTile() */ public int getBytesPerTile() { return tileDataLength; } /** * Creates and executes the {@link SeQuery} that's used to fetch the required tiles from the * specified raster, and stores (as member variables) the {@link SeRow} to fetch the tiles from * and the {@link SeQuery} to be closed at the TileReader's disposal * * @throws IOException */ private void execute() throws IOException { final GridEnvelope requestedTiles = this.requestedTiles; this.queryObjects = execute(requestedTiles); this.started = true; this.lastTileX = this.lastTileY = -1; } private QueryObjects execute(final GridEnvelope requestedTiles) throws IOException { final int rasterIndex = rasterInfo.getRasterIndex(rasterId); /* * Create the raster constraint to query the needed tiles out of the specified raster at the * given pyramid level */ final SeRasterConstraint rConstraint; try { final int numberOfBands; numberOfBands = rasterInfo.getNumBands(); int[] bandsToQuery = new int[numberOfBands]; for (int bandN = 1; bandN <= numberOfBands; bandN++) { bandsToQuery[bandN - 1] = bandN; } int minTileX = requestedTiles.getLow(0); int minTileY = requestedTiles.getLow(1); int maxTileX = requestedTiles.getHigh(0); int maxTileY = requestedTiles.getHigh(1); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Requesting tiles [x=" + minTileX + "-" + maxTileX + ", y=" + minTileY + "-" + maxTileY + "] from tile range [x=0-" + (rasterInfo.getNumTilesWide(rasterIndex, pyramidLevel) - 1) + ", y=0-" + (rasterInfo.getNumTilesHigh(rasterIndex, pyramidLevel) - 1) + "]"); } // SDEPoint tileOrigin = rAttr.getTileOrigin(); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Tiled image size: " + requestedTiles); } final int interleaveType = SeRaster.SE_RASTER_INTERLEAVE_BIP; rConstraint = new SeRasterConstraint(); rConstraint.setBands(bandsToQuery); rConstraint.setLevel(pyramidLevel); rConstraint.setEnvelope(minTileX, minTileY, maxTileX, maxTileY); rConstraint.setInterleave(interleaveType); } catch (SeException se) { throw new ArcSdeException(se); } /* * Obtain the ISession this tile reader will work with until exhausted */ try { if (this.session == null) { // lets share connections as we're going to do read only operations final boolean transactional = false; this.session = sessionPool.getSession(transactional); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("Using " + session + " to read raster #" + rasterId + " on Thread " + Thread.currentThread().getName() + ". Tile set: " + requestedTiles); } } } catch (UnavailableConnectionException e) { // really bad luck.. throw new RuntimeException(e); } final String rasterTable = rasterInfo.getRasterTable(); final String rasterColumn = rasterInfo.getRasterColumns()[0]; final QueryRasterCommand queryCommand = new QueryRasterCommand(rConstraint, rasterTable, rasterColumn, rasterId); session.issue(queryCommand); final SeRow row = queryCommand.getSeRow(); final SeQuery preparedQuery = queryCommand.getPreparedQuery(); return new QueryObjects(preparedQuery, row); } private static class QueryObjects { SeQuery preparedQuery; SeRow row; public QueryObjects(SeQuery preparedQuery, SeRow row) { this.preparedQuery = preparedQuery; this.row = row; } } /** * @see org.geotools.arcsde.raster.io.TileReader#getTile(int, int, byte[][]) */ public void getTile(final int tileX, final int tileY, byte[][] data) throws IOException { assert data == null || data.length == getNumberOfBands(); final int numberOfBands = getNumberOfBands(); TileInfo[] tileInfo = getTileInfo(); TileInfo t; for (int b = 0; b < numberOfBands; b++) { t = tileInfo[b];// new TileInfo(getPixelsPerTile()); t.setTileData(data[b]); tileInfo[b] = t; } getTile(tileX, tileY, tileInfo); } TileInfo[] tileInfo; /** * @see org.geotools.arcsde.raster.io.TileReader#getTile(int, int, short[][]) */ public void getTile(int tileX, int tileY, short[][] data) throws IOException { assert data == null || data.length == getNumberOfBands(); final int numberOfBands = getNumberOfBands(); TileInfo[] tileInfo = getTileInfo(); TileInfo t; for (int b = 0; b < numberOfBands; b++) { t = tileInfo[b];// new TileInfo(getPixelsPerTile()); t.setTileData(data[b]); tileInfo[b] = t; } getTile(tileX, tileY, tileInfo); } private TileInfo[] getTileInfo() { if (tileInfo == null) { final int numberOfBands = getNumberOfBands(); tileInfo = new TileInfo[numberOfBands]; for (int b = 0; b < numberOfBands; b++) { TileInfo t = new TileInfo(getPixelsPerTile()); tileInfo[b] = t; } } return tileInfo; } /** * @see org.geotools.arcsde.raster.io.TileReader#getTile(int, int, int[][]) */ public void getTile(int tileX, int tileY, int[][] data) throws IOException { assert data == null || data.length == getNumberOfBands(); final int numberOfBands = getNumberOfBands(); TileInfo[] tileInfo = getTileInfo(); TileInfo t; for (int b = 0; b < numberOfBands; b++) { t = tileInfo[b];// new TileInfo(getPixelsPerTile()); t.setTileData(data[b]); tileInfo[b] = t; } getTile(tileX, tileY, tileInfo); } /** * @see org.geotools.arcsde.raster.io.TileReader#getTile(int, int, float[][]) */ public void getTile(int tileX, int tileY, float[][] data) throws IOException { assert data == null || data.length == getNumberOfBands(); final int numberOfBands = getNumberOfBands(); TileInfo[] tileInfo = getTileInfo(); TileInfo t; for (int b = 0; b < numberOfBands; b++) { t = tileInfo[b];// new TileInfo(getPixelsPerTile()); t.setTileData(data[b]); tileInfo[b] = t; } getTile(tileX, tileY, tileInfo); } /** * @see org.geotools.arcsde.raster.io.TileReader#getTile(int, int, double[][]) */ public void getTile(int tileX, int tileY, double[][] data) throws IOException { assert data == null || data.length == getNumberOfBands(); final int numberOfBands = getNumberOfBands(); TileInfo[] tileInfo = getTileInfo(); TileInfo t; for (int b = 0; b < numberOfBands; b++) { t = tileInfo[b];// new TileInfo(getPixelsPerTile()); t.setTileData(data[b]); tileInfo[b] = t; } getTile(tileX, tileY, tileInfo); } private void getTile(final int tileX, final int tileY, TileInfo[] target) throws IOException { // System.out.printf("fetchTile %d, %d\n", tileX, tileY); try { fetchTile(tileX, tileY, target); } catch (IOException e) { dispose(); throw e; } catch (RuntimeException e) { dispose(); throw (RuntimeException) new RuntimeException("Error geting tile " + tileX + "," + tileY + " on raster " + rasterInfo.getRasterTable() + "#" + this.rasterId) .initCause(e); } } private int lastTileX = -1; private int lastTileY = -1; /** * How many random tiles requested (ie, not consecutive) before re executing the original * request as a rewind */ private static final int RANDOM_THRESHOLD = Integer.MAX_VALUE; private int nonConsecutiveCallCount; private void fetchTile(final int tileX, final int tileY, TileInfo[] target) throws IOException { SeRasterTile[] seTile = null; if (isConsecutive(tileX, tileY)) { while (lastTileX != tileX || lastTileY != tileY) { seTile = nextTile(); } } else { if (nonConsecutiveCallCount == RANDOM_THRESHOLD) { nonConsecutiveCallCount = 0; if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.info("Number of random (non consecutive) tile request exceeded" + " predefined threshold. Rewind by executing original request again"); } dispose(); fetchTile(tileX, tileY, target); return; } else { nonConsecutiveCallCount++; seTile = fetchSingleTile(tileX, tileY); } } if (lastTileX == getMaxTileX() && lastTileY == getMaxTileY()) { dispose(); } extractTile(seTile, target); } /** * @see org.geotools.arcsde.raster.io.TileReader#getMaxTileX() */ public int getMaxTileX() { return requestedTiles.getHigh(0); } /** * @see org.geotools.arcsde.raster.io.TileReader#getMaxTileY() */ public int getMaxTileY() { return requestedTiles.getHigh(1); } /** * Executes a separate request to fetch this single tile * * @throws IOException */ private SeRasterTile[] fetchSingleTile(final int tileX, final int tileY) throws IOException { if (LOGGER.isLoggable(Level.INFO)) { LOGGER.info("fetchSingleTile raster #" + this.rasterId + ", plevel " + pyramidLevel + ", tile " + tileX + ", " + tileY); } final int width = 1; final int height = 1; final GridEnvelope requestTiles = new GridEnvelope2D(tileX, tileY, width, height); final QueryObjects singleTileQueryObjects = execute(requestTiles); final SeQuery query = singleTileQueryObjects.preparedQuery; final SeRow row = singleTileQueryObjects.row; final TileFetchCommand command = new TileFetchCommand(row, nativeCellType); SeRasterTile[] tileData; try { tileData = readTile(command); } finally { session.close(query); } return tileData; } /** * Determines whether the tile defined by {@code tileX, tileY} is consecutive to the original * request, whether it is exactly the next in the stream or any other one that follows the last * tile fetched from the original request. */ private boolean isConsecutive(final int tileX, final int tileY) { if (tileX > lastTileX && tileY >= lastTileY) { return true; } if (tileX <= lastTileX && tileY > lastTileY) { return true; } return false; } private TileFetchCommand sequentialFetchCommand; private SeRasterTile[] nextTile() throws IOException { if (!started) { execute(); SeRow row = queryObjects.row; RasterCellType nativeType = nativeCellType; sequentialFetchCommand = new TileFetchCommand(row, nativeType); } SeRasterTile[] tileData = readTile(sequentialFetchCommand); if (lastTileX == -1 && lastTileY == -1) { lastTileX = getMinTileX(); lastTileY = getMinTileY(); } else { lastTileX++; if (lastTileX > getMaxTileX()) { lastTileX = getMinTileX(); lastTileY++; } } return tileData; } private SeRasterTile[] readTile(final TileFetchCommand fetchCommand) throws IOException { final int numBands = getNumberOfBands(); SeRasterTile[] tile = new SeRasterTile[numBands]; SeRasterTile bandTile; for (int i = 0; i < numBands; i++) { bandTile = session.issue(fetchCommand); if (bandTile == null) { throw new IllegalStateException("There are no more tiles to fetch"); } tile[i] = bandTile; } return tile; } /** * @see org.geotools.arcsde.raster.io.TileReader#dispose() */ public void dispose() { if (session != null) { // System.err.println("TileReader disposing " + session + " on Thread " // + Thread.currentThread().getName()); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("TileReader disposing " + session + " on Thread " + Thread.currentThread().getName()); } if (queryObjects != null) { try { SeQuery preparedQuery = queryObjects.preparedQuery; session.close(preparedQuery); } catch (Exception e) { LOGGER.log(Level.WARNING, "Closing tile reader's prepared Query", e); } } if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("Disposing " + session + " on thread " + Thread.currentThread().getName()); } session.dispose(); session = null; // get ready for more invocations queryObjects = null; started = false; lastTileX = lastTileY = -1; } } /** * Disposes as to make sure the {@link ISession session} is returned to the pool even if a * failing or non careful client left this object hanging around * * @see #dispose() * @see java.lang.Object#finalize() */ @Override protected void finalize() { dispose(); } public int getMinTileX() { return requestedTiles.getLow(0); } public int getMinTileY() { return requestedTiles.getLow(1); } public String getServerName() { return sessionPool.getConfig().getServerName(); } public String getRasterTableName() { return rasterInfo.getRasterTable(); } public int getPyramidLevel() { return pyramidLevel; } public long getRasterId() { return rasterId; } private void extractTile(final SeRasterTile[] seTile, TileInfo[] target) { final int numberOfBands = getNumberOfBands(); assert numberOfBands == seTile.length; assert numberOfBands == target.length; SeRasterTile tile; TileInfo bandData; for (int bandN = 0; bandN < numberOfBands; bandN++) { tile = seTile[bandN]; final byte[] bitMaskData = tile.getBitMaskData(); final int numPixelsRead = tile.getNumPixels(); final long bandId = tile.getBandId().longValue(); final int colIndex = tile.getColumnIndex(); final int rowIndex = tile.getRowIndex(); final Number noData = rasterInfo.getNoDataValue(rasterId, bandN); bandData = target[bandN]; bandData.setBandId(bandId); bandData.setColumnIndex(colIndex); bandData.setRowIndex(rowIndex); bandData.setNumPixelsRead(numPixelsRead); bandData.setBitmaskData(bitMaskData); bandData.setNoDataValue(noData); dataFetcher.setTileData(tile, bandData); } } }