001: /*
002: * Geotools2 - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2002, Geotools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: *
016: */
017: package org.geotools.arcsde.gce.imageio;
018:
019: import java.awt.Color;
020: import java.awt.Dimension;
021: import java.awt.Font;
022: import java.awt.Graphics2D;
023: import java.awt.Point;
024: import java.awt.Rectangle;
025: import java.awt.image.BufferedImage;
026: import java.awt.image.WritableRaster;
027: import java.io.IOException;
028: import java.util.ArrayList;
029: import java.util.HashMap;
030: import java.util.Iterator;
031: import java.util.logging.Level;
032: import java.util.logging.Logger;
033:
034: import javax.imageio.ImageReadParam;
035: import javax.imageio.ImageReader;
036: import javax.imageio.ImageTypeSpecifier;
037: import javax.imageio.metadata.IIOMetadata;
038:
039: import org.geotools.arcsde.gce.ArcSDEPyramid;
040: import org.geotools.arcsde.gce.ArcSDEPyramidLevel;
041: import org.geotools.arcsde.gce.band.ArcSDERasterBandCopier;
042: import org.geotools.arcsde.pool.ArcSDEConnectionPool;
043: import org.geotools.arcsde.pool.ArcSDEPooledConnection;
044: import org.geotools.arcsde.pool.UnavailableArcSDEConnectionException;
045: import org.geotools.data.DataSourceException;
046:
047: import com.esri.sde.sdk.client.SeConnection;
048: import com.esri.sde.sdk.client.SeException;
049: import com.esri.sde.sdk.client.SeQuery;
050: import com.esri.sde.sdk.client.SeRaster;
051: import com.esri.sde.sdk.client.SeRasterAttr;
052: import com.esri.sde.sdk.client.SeRasterBand;
053: import com.esri.sde.sdk.client.SeRasterConstraint;
054: import com.esri.sde.sdk.client.SeRasterTile;
055: import com.esri.sde.sdk.client.SeRow;
056: import com.esri.sde.sdk.client.SeSqlConstruct;
057:
058: public class ArcSDERasterReader extends ImageReader {
059:
060: private static final boolean DEBUG = false;
061:
062: private static final ArrayList supportedImageTypes;
063: static {
064: supportedImageTypes = new ArrayList();
065: supportedImageTypes
066: .add(ImageTypeSpecifier
067: .createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB));
068: }
069:
070: private final Logger LOGGER = org.geotools.util.logging.Logging
071: .getLogger(this .getClass().toString());
072: private final ArcSDEPyramid _rasterPyramid;
073: private final Dimension _tileSize;
074: private final String _rasterTable, _rasterColumn;
075:
076: public ArcSDERasterReader(ArcSDERasterReaderSpi parent,
077: ArcSDEPyramid rasterPyramid, String rasterTable,
078: String rasterColumn) {
079: super (parent);
080: _tileSize = rasterPyramid.getTileDimension();
081: _rasterPyramid = rasterPyramid;
082: _rasterTable = rasterTable;
083: _rasterColumn = rasterColumn;
084: }
085:
086: public int getHeight(int imageIndex) throws IOException {
087: return _rasterPyramid.getPyramidLevel(imageIndex).size.height;
088: }
089:
090: public int getWidth(int imageIndex) throws IOException {
091: return _rasterPyramid.getPyramidLevel(imageIndex).size.width;
092: }
093:
094: public Iterator getImageTypes(int imageIndex) throws IOException {
095: return supportedImageTypes.iterator();
096: }
097:
098: public int getNumImages(boolean allowSearch) throws IOException {
099: return _rasterPyramid.getNumLevels();
100: }
101:
102: /**
103: * This method is THREADSAFE.
104: *
105: * Reads from its configured ArcSDE Raster "row" (one row in a raster table
106: * corresponds to one ArcSDERasterReader) in the manner described by the
107: * supplied ImageReadParam.
108: *
109: * @param param This must be an ArcSDERasterImageReadParam, containing a valid, live
110: * SeConnection through which this reader will "suck" its raster data from SDE.
111: * @param imageIndex This parameter specifies which image pyramid level (if there's more than one)
112: * to read from. If there's only one pyramid level, this value should be 0.
113: *
114: * @throws IOException
115: */
116: public BufferedImage read(int imageIndex, ImageReadParam param)
117: throws IOException {
118:
119: // we only read from ArcSDERasterImageReadParams.
120: if (!(param instanceof ArcSDERasterImageReadParam)) {
121: throw new IllegalArgumentException(
122: "read() must be called with an ArcSDERasterReadImageParam, not a "
123: + param.getClass());
124: }
125: final ArcSDERasterImageReadParam sdeirp = (ArcSDERasterImageReadParam) param;
126:
127: //double-check that all the required info is present.
128: if (sdeirp.getSourceBands() == null) {
129: throw new IllegalArgumentException(
130: "You must provide source bands to the ArcSDERasterReader via param.setSourceBands()");
131: }
132: if (sdeirp.getConnection() == null) {
133: throw new IllegalArgumentException(
134: "You must provide a connection to the ArcSDERasterReader via the param.setConnection() method.");
135: }
136: if (sdeirp.getBandMapper() == null) {
137: throw new IllegalArgumentException(
138: "You must provide a hashmap bandmapper to the ArcSDERasterReader via the param.setBandMapper() method");
139: }
140:
141: // start collecting our background information for doing the read.
142:
143: // the source region is the actual pixel extent of this particar pyramid layer
144: final Rectangle sourceRegion = param.getSourceRegion();
145:
146: final ArcSDEPyramidLevel curLevel = _rasterPyramid
147: .getPyramidLevel(imageIndex);
148:
149: // if we're reading "off the top" or "off the left" side of the image, then there's some "blank" part of
150: // the returned image that we need to leave blank. This offset tells us where to start writing into the
151: // destination image in that case. (Note, if we're reading "off the bottom" or "off the right" side of
152: // the image, this doesn't affect us as we just stop writing at the end of the tile and the rest is
153: // left blank automatically).
154: final Point destOffset = param.getDestinationOffset() == null ? new Point(
155: 0, 0)
156: : param.getDestinationOffset();
157: if (curLevel.getXOffset() != 0) {
158: sourceRegion.x += curLevel.getXOffset();
159: }
160: if (curLevel.getYOffset() != 0) {
161: if (LOGGER.isLoggable(Level.FINER))
162: LOGGER.finer("y-axis is offset by "
163: + curLevel.getYOffset()
164: + " at SDE pyramid level "
165: + curLevel.getLevel());
166: sourceRegion.y += curLevel.getYOffset();
167: }
168:
169: // the destination image for our eventual raster read. Can be provided (if the given one is non-null)
170: // or we can be expected to generate it (if the given one is null)
171: BufferedImage destination = param.getDestination();
172: if (destination == null
173: && (destOffset.x == 0 && destOffset.y == 0)) {
174: destination = new BufferedImage(sourceRegion.width,
175: sourceRegion.height, BufferedImage.TYPE_INT_ARGB);
176: } else if (destination == null) {
177: final int imageWidth = destOffset.x + sourceRegion.width;
178: final int imageHeight = destOffset.y + sourceRegion.height;
179: destination = new BufferedImage(imageWidth, imageHeight,
180: BufferedImage.TYPE_INT_ARGB);
181: } else if (destination != null
182: && !(destOffset.x == 0 && destOffset.y == 0)) {
183: int destWidth = sourceRegion.width, destHeight = sourceRegion.height;
184: if (destOffset.x + sourceRegion.width > destination
185: .getWidth())
186: destWidth = destination.getWidth() - destOffset.x;
187: if (destOffset.y + sourceRegion.height > destination
188: .getHeight())
189: destHeight = destination.getHeight() - destOffset.y;
190: destination = destination.getSubimage(destOffset.x,
191: destOffset.y, destWidth, destHeight);
192: } else {
193: //we've got a non-null destination image and there's no offset. Nothing to do!
194: }
195:
196: // figure out which tiles exactly, we'll be fetching
197: final int minTileX = sourceRegion.x / _tileSize.width;
198: final int minTileY = sourceRegion.y / _tileSize.height;
199: if (LOGGER.isLoggable(Level.FINER))
200: LOGGER.finer("figured minTiles: " + minTileX + ","
201: + minTileY + ". Image is "
202: + curLevel.getNumTilesWide() + "x"
203: + curLevel.getNumTilesHigh() + " tiles wxh.");
204: int maxTileX = (sourceRegion.x + sourceRegion.width)
205: / _tileSize.width;
206: int maxTileY = (sourceRegion.y + sourceRegion.height)
207: / _tileSize.height;
208: if (maxTileX > curLevel.getNumTilesWide())
209: maxTileX = curLevel.getNumTilesWide() - 1;
210: if (maxTileY > curLevel.getNumTilesHigh())
211: maxTileY = curLevel.getNumTilesHigh() - 1;
212:
213: //figure out what our offset into the tile grid is
214: final int tilegridOffsetX = sourceRegion.x % _tileSize.width;
215: final int tilegridOffsetY = sourceRegion.y % _tileSize.height;
216:
217: if (LOGGER.isLoggable(Level.INFO))
218: LOGGER.info("Reading " + param.getSourceRegion()
219: + " offset by " + sdeirp.getDestinationOffset()
220: + " (tiles " + minTileX + "," + minTileY + " to "
221: + maxTileX + "," + maxTileY + " in level "
222: + imageIndex + ")");
223:
224: // Now we do the actual reading from SDE.
225: ArcSDEPooledConnection scon = sdeirp.getConnection();
226: SeQuery query = null;
227:
228: try {
229: // This rather strange set of query operations is apparently the way
230: // one gets SDE Raster output. First, query the
231: // database for the single row in the raster business table.
232: //FIXME: Raster catalogs need to specify what their row number is.
233: query = new SeQuery(scon, new String[] { _rasterColumn },
234: new SeSqlConstruct(_rasterTable));
235: query.prepareQuery();
236: query.execute();
237: // Next, fetch the single row back.
238: final SeRow r = query.fetch();
239:
240: // Now build a SeRasterConstraint object which queries the db for
241: // the right tiles/bands/pyramid level
242: SeRasterConstraint rConstraint = new SeRasterConstraint();
243: rConstraint.setEnvelope(minTileX, minTileY, maxTileX,
244: maxTileY);
245: rConstraint.setLevel(imageIndex);
246: rConstraint.setBands(sdeirp.getSourceBands());
247:
248: // Finally, execute the raster query aganist the already-opened
249: // SeQuery object which already has an SeRow fetched against it.
250: query.queryRasterTile(rConstraint);
251:
252: // Now, magically, calls to r.getRasterTile() will fetch our list
253: // of tiles.
254: SeRasterTile curTile = r.getRasterTile();
255:
256: // the bit of our destination image that overlaps this tile
257: WritableRaster destinationSubTile;
258:
259: // the offsets into the CURRENT TILE of where we should start
260: // copying from the tile to the destination image. Will only be non-zero
261: // if we're at the "leading edge" or "top edge" of the grid of tiles.
262: int curTileOffsetX, curTileOffsetY;
263:
264: // When we copy from the current SeRasterTile into the destination image, we'll create
265: // a BufferedImage.getSubImage() of the destitation that perfectly overlaps the current
266: // SeRasterTile, and copy the data from the current SeRasterTile to that sub-image. These
267: // vars hold the proper offsets, width and height into the DESTINATION IMAGE.
268: int destImageOffsetX, destImageOffsetY;
269: int destImageTileWidth, destImageTileHeight;
270:
271: // Copying from an ArcSDE Raster to the specified image format, we can optionally have a
272: // band mapper which specifies which data band goes to which image band.
273: HashMap bandMapper = sdeirp.getBandMapper();
274:
275: // And we need to create a bandcopier for this raster type.
276: final ArcSDERasterBandCopier bandCopier = ArcSDERasterBandCopier
277: .getInstance(r.getRaster(0).getPixelType(),
278: _tileSize.width, _tileSize.height);
279:
280: while (curTile != null) {
281: // LOGGER.info("tile at " + curTile.getColumnIndex() + "," +
282: // curTile.getRowIndex() + " has " + curTile.getNumPixels() + "
283: // pixels");
284: if (curTile.getNumPixels() == 0 && false) {
285: curTile = r.getRasterTile();
286: continue;
287: }
288:
289: // Does our image start at the exact tile boundary? If we're in the middle of a range of tiles,
290: // it does, otherwise it's probably going to start slighty in from the edge of the tile.
291: if (curTile.getColumnIndex() == minTileX) {
292: curTileOffsetX = tilegridOffsetX;
293: } else {
294: curTileOffsetX = 0;
295: }
296:
297: if (curTile.getRowIndex() == minTileY) {
298: curTileOffsetY = tilegridOffsetY;
299: } else {
300: curTileOffsetY = 0;
301: }
302:
303: // Now we figure out how far into the destination image we go to create a mini sub-tile
304: // that overlaps this current tile. If we're at the first tile, we start at zero.
305: final int curTileX = curTile.getColumnIndex()
306: - minTileX;
307: if (curTileX == 0) {
308: destImageOffsetX = 0;
309: } else {
310: destImageOffsetX = (_tileSize.width - tilegridOffsetX)
311: + ((curTileX - 1) * _tileSize.width);
312: }
313:
314: final int curTileY = curTile.getRowIndex() - minTileY;
315: if (curTileY == 0) {
316: destImageOffsetY = 0;
317: } else {
318: destImageOffsetY = (_tileSize.height - tilegridOffsetY)
319: + ((curTileY - 1) * _tileSize.height);
320: }
321:
322: if (curTile.getColumnIndex() == maxTileX
323: && (destImageOffsetX + _tileSize.width > destination
324: .getWidth())) {
325: // if we're at the end of the row or column, we might try to grab too large of a sub-image. Make sure
326: // that we're only grabbing up to the actual size of the image.
327: destImageTileWidth = destination.getWidth()
328: - destImageOffsetX;
329: } else if (curTileX == 0) {
330: // if we're at the beginning of a row or column, we don't need to grab the entire
331: // sub-image. We need to grab just the bit that overlaps the current tile.
332: destImageTileWidth = (_tileSize.width - curTileOffsetX);
333: } else {
334: destImageTileWidth = _tileSize.width;
335: }
336:
337: if (curTile.getRowIndex() == maxTileY
338: && (destImageOffsetY + _tileSize.height > destination
339: .getHeight())) {
340: destImageTileHeight = destination.getHeight()
341: - destImageOffsetY;
342: } else if (curTileY == 0) {
343: destImageTileHeight = (_tileSize.height - curTileOffsetY);
344: } else {
345: destImageTileHeight = _tileSize.height;
346: }
347:
348: //LOGGER.info("creating subtile at " + new Rectangle(destImageOffsetX, destImageOffsetY, destImageTileWidth, destImageTileHeight));
349: if (destImageTileWidth == 0 || destImageTileHeight == 0) {
350: if (LOGGER.isLoggable(Level.FINER))
351: LOGGER.finer("Skipping tile " + curTileX + ","
352: + curTileY
353: + " because it has imagetile height "
354: + destImageTileHeight + " and width "
355: + destImageTileWidth);
356: curTile = r.getRasterTile();
357: continue;
358: }
359: BufferedImage subtile = destination.getSubimage(
360: destImageOffsetX, destImageOffsetY,
361: destImageTileWidth, destImageTileHeight);
362: destinationSubTile = subtile.getRaster();
363:
364: final Integer curBandId = new Integer((int) curTile
365: .getBandId().longValue());
366: final int targetBand = ((Integer) bandMapper
367: .get(curBandId)).intValue();
368: bandCopier.copyPixelData(curTile, destinationSubTile,
369: curTileOffsetX, curTileOffsetY, targetBand);
370:
371: if (DEBUG) {
372: int[] blackpixel = new int[] { 0x00, 0x00, 0x00,
373: 0xff };
374: for (int x = 0; x < destinationSubTile.getWidth(); x++) {
375: destinationSubTile.setPixel(x, 0, blackpixel);
376: destinationSubTile.setPixel(x,
377: destinationSubTile.getHeight() - 1,
378: blackpixel);
379: }
380: for (int y = 0; y < destinationSubTile.getHeight(); y++) {
381: destinationSubTile.setPixel(0, y, blackpixel);
382: destinationSubTile.setPixel(destinationSubTile
383: .getWidth() - 1, y, blackpixel);
384: }
385:
386: final Graphics2D graphics = subtile
387: .createGraphics();
388: graphics.setFont(new Font("Sans-serif", Font.BOLD,
389: 10));
390: graphics.setColor(Color.yellow);
391: graphics.drawString(curTile.getRowIndex() + ","
392: + curTile.getColumnIndex(), 10, 10);
393:
394: graphics.drawString(destImageOffsetX + ","
395: + destImageOffsetY + " -- "
396: + destImageTileWidth + "x"
397: + destImageTileHeight, 10, 25);
398:
399: graphics.dispose();
400: }
401:
402: // fetch the next tile
403: curTile = r.getRasterTile();
404: }
405: curTile = null;
406: destinationSubTile = null;
407:
408: //don't need to close connections, cause that's done in the 'finally' block
409: } catch (SeException se) {
410: LOGGER.log(Level.SEVERE, se.getSeError().getErrDesc(), se);
411: throw new DataSourceException(se);
412: } finally {
413: try {
414: if (query != null)
415: query.close();
416: /*if (scon != null && !scon.isClosed())
417: scon.close();*/
418: } catch (SeException se) {
419: LOGGER.log(Level.SEVERE, se.getSeError().getErrDesc(),
420: se);
421: throw new DataSourceException(
422: "Unable to clean up connections to database. May have left one hanging.",
423: se);
424: }
425: }
426:
427: return destination;
428: }
429:
430: /**
431: * Not implemented
432: */
433: public IIOMetadata getImageMetadata(int imageIndex)
434: throws IOException {
435: return null;
436: }
437:
438: /**
439: * Not implemented
440: */
441: public IIOMetadata getStreamMetadata() throws IOException {
442: return null;
443: }
444: }
|