0001: /*
0002: * Geotools 2 - OpenSource mapping toolkit
0003: * (C) 2006, Geotools Project Managment Committee (PMC)
0004: *
0005: * This library is free software; you can redistribute it and/or
0006: * modify it under the terms of the GNU Lesser General Public
0007: * License as published by the Free Software Foundation; either
0008: * version 2.1 of the License, or (at your option) any later version.
0009: *
0010: * This library is distributed in the hope that it will be useful,
0011: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0012: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0013: * Lesser General Public License for more details.
0014: *
0015: * You should have received a copy of the GNU Lesser General Public
0016: * License along with this library; if not, write to the Free Software
0017: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
0018: */
0019: package org.geotools.gce.imagemosaic;
0020:
0021: import java.awt.Color;
0022: import java.awt.Rectangle;
0023: import java.awt.RenderingHints;
0024: import java.awt.Transparency;
0025: import java.awt.geom.AffineTransform;
0026: import java.awt.geom.Area;
0027: import java.awt.geom.Point2D;
0028: import java.awt.image.BufferedImage;
0029: import java.awt.image.ColorModel;
0030: import java.awt.image.DataBuffer;
0031: import java.awt.image.IndexColorModel;
0032: import java.awt.image.renderable.ParameterBlock;
0033: import java.io.BufferedInputStream;
0034: import java.io.File;
0035: import java.io.FileInputStream;
0036: import java.io.FileNotFoundException;
0037: import java.io.IOException;
0038: import java.io.UnsupportedEncodingException;
0039: import java.lang.ref.SoftReference;
0040: import java.net.MalformedURLException;
0041: import java.net.URL;
0042: import java.net.URLDecoder;
0043: import java.util.Iterator;
0044: import java.util.List;
0045: import java.util.Properties;
0046: import java.util.logging.Level;
0047: import java.util.logging.Logger;
0048:
0049: import javax.imageio.ImageIO;
0050: import javax.imageio.ImageReadParam;
0051: import javax.media.jai.ImageLayout;
0052: import javax.media.jai.JAI;
0053: import javax.media.jai.ParameterBlockJAI;
0054: import javax.media.jai.PlanarImage;
0055: import javax.media.jai.ROI;
0056: import javax.media.jai.operator.MosaicDescriptor;
0057: import javax.media.jai.operator.PatternDescriptor;
0058:
0059: import org.geotools.coverage.grid.GeneralGridRange;
0060: import org.geotools.coverage.grid.GridCoverage2D;
0061: import org.geotools.coverage.grid.GridGeometry2D;
0062: import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
0063: import org.geotools.coverage.grid.io.AbstractGridFormat;
0064: import org.geotools.data.AbstractDataStore;
0065: import org.geotools.data.DataSourceException;
0066: import org.geotools.data.FeatureSource;
0067: import org.geotools.data.shapefile.ShapefileDataStore;
0068: import org.geotools.factory.FactoryRegistryException;
0069: import org.geotools.factory.Hints;
0070: import org.geotools.feature.Feature;
0071: import org.geotools.geometry.GeneralEnvelope;
0072: import org.geotools.geometry.jts.ReferencedEnvelope;
0073: import org.geotools.image.ImageWorker;
0074: import org.geotools.parameter.Parameter;
0075: import org.geotools.referencing.CRS;
0076: import org.geotools.referencing.operation.builder.GridToEnvelopeMapper;
0077: import org.geotools.resources.CRSUtilities;
0078: import org.geotools.resources.image.ImageUtilities;
0079: import org.opengis.coverage.grid.Format;
0080: import org.opengis.coverage.grid.GridCoverage;
0081: import org.opengis.coverage.grid.GridCoverageReader;
0082: import org.opengis.parameter.GeneralParameterValue;
0083: import org.opengis.referencing.FactoryException;
0084: import org.opengis.referencing.crs.CoordinateReferenceSystem;
0085: import org.opengis.referencing.datum.PixelInCell;
0086: import org.opengis.referencing.operation.MathTransform;
0087: import org.opengis.referencing.operation.NoninvertibleTransformException;
0088: import org.opengis.referencing.operation.TransformException;
0089: import org.opengis.geometry.MismatchedDimensionException;
0090:
0091: import com.vividsolutions.jts.geom.Envelope;
0092:
0093: /**
0094: * This reader is repsonsible for providing access to mosaic of georeferenced
0095: * images. Citing JAI documentation:
0096: *
0097: * The "Mosaic" operation creates a mosaic of two or more source images. This
0098: * operation could be used for example to assemble a set of overlapping
0099: * geospatially rectified images into a contiguous image. It could also be used
0100: * to create a montage of photographs such as a panorama.
0101: *
0102: * All source images are assumed to have been geometrically mapped into a common
0103: * coordinate space. The origin (minX, minY) of each image is therefore taken to
0104: * represent the location of the respective image in the common coordinate
0105: * system of the sour ce images. This coordinate space will also be that of the
0106: * destination image.
0107: *
0108: * All source images must have the same data type and sample size for all bands
0109: * and have the same number of bands as color components. The destination will
0110: * have the same data type, sample size, and number of bands and color
0111: * components as the sources.
0112: *
0113: *
0114: * @author Simone Giannecchini
0115: * @since 2.3
0116: *
0117: */
0118: public final class ImageMosaicReader extends
0119: AbstractGridCoverage2DReader implements GridCoverageReader {
0120:
0121: /** Logger. */
0122: private final static Logger LOGGER = org.geotools.util.logging.Logging
0123: .getLogger("org.geotools.gce.imagemosaic");
0124:
0125: /**
0126: * The source {@link URL} pointing to the index shapefile for this
0127: * {@link ImageMosaicReader}.
0128: */
0129: private final URL sourceURL;
0130:
0131: /** {@link AbstractDataStore} pointd to the index shapefile. */
0132: private final AbstractDataStore tileIndexStore;
0133:
0134: /** {@link SoftReference} to the index holding the tiles' envelopes. */
0135: private SoftReference index;
0136:
0137: /**
0138: * The typename of the chems inside the {@link ShapefileDataStore} that
0139: * contains the index for this {@link ImageMosaicReader}.
0140: */
0141: private final String typeName;
0142:
0143: /** {@link FeatureSource} for the shape index. */
0144: private final FeatureSource featureSource;
0145:
0146: /**
0147: * This {@link BufferedImage} is cached in memory since it is used whenever
0148: * I need to build up a fake mosaic.
0149: */
0150: private final BufferedImage unavailableImage;
0151:
0152: private boolean expandMe;
0153:
0154: /**
0155: * Max number of tiles that this plugin will load.
0156: *
0157: * If this number is exceeded, i.e. we request an area which is too large
0158: * instead of getting stuck ith opening thousands of files I give you back a
0159: * fake coverage.
0160: */
0161: public static final int MAX_TILES = 1000000;
0162:
0163: /**
0164: * COnstructor.
0165: *
0166: * @param source
0167: * The source object.
0168: * @throws IOException
0169: * @throws UnsupportedEncodingException
0170: *
0171: */
0172: public ImageMosaicReader(Object source, Hints uHints)
0173: throws IOException {
0174: // /////////////////////////////////////////////////////////////////////
0175: //
0176: // Forcing longitude first since the geotiff specification seems to
0177: // assume that we have first longitude the latitude.
0178: //
0179: // /////////////////////////////////////////////////////////////////////
0180: if (uHints != null) {
0181: // prevent the use from reordering axes
0182: this .hints.add(uHints);
0183: }
0184: if (source == null) {
0185:
0186: final IOException ex = new IOException(
0187: "ImageMosaicReader:No source set to read this coverage.");
0188: if (LOGGER.isLoggable(Level.WARNING))
0189: LOGGER.log(Level.WARNING, ex.getLocalizedMessage(), ex);
0190: throw new DataSourceException(ex);
0191: }
0192: this .source = source;
0193:
0194: // /////////////////////////////////////////////////////////////////////
0195: //
0196: // Check source
0197: //
0198: // /////////////////////////////////////////////////////////////////////
0199: if (source instanceof File)
0200: this .sourceURL = ((File) source).toURL();
0201: else if (source instanceof URL)
0202: this .sourceURL = (URL) source;
0203: else if (source instanceof String) {
0204: final File tempFile = new File((String) source);
0205: if (tempFile.exists()) {
0206: this .sourceURL = tempFile.toURL();
0207: } else
0208: try {
0209: this .sourceURL = new URL(URLDecoder.decode(
0210: (String) source, "UTF8"));
0211: if (this .sourceURL.getProtocol() != "file") {
0212: throw new IllegalArgumentException(
0213: "This plugin accepts only File, URL and String pointing to a file");
0214:
0215: }
0216: } catch (MalformedURLException e) {
0217: if (LOGGER.isLoggable(Level.WARNING))
0218: LOGGER.log(Level.WARNING, e
0219: .getLocalizedMessage(), e);
0220: throw new IllegalArgumentException(
0221: "This plugin accepts only File, URL and String pointing to a file");
0222:
0223: } catch (UnsupportedEncodingException e) {
0224: if (LOGGER.isLoggable(Level.WARNING))
0225: LOGGER.log(Level.WARNING, e
0226: .getLocalizedMessage(), e);
0227: throw new IllegalArgumentException(
0228: "This plugin accepts only File, URL and String pointing to a file");
0229:
0230: }
0231:
0232: } else
0233: throw new IllegalArgumentException(
0234: "This plugin accepts only File, URL and String pointing to a file");
0235:
0236: // /////////////////////////////////////////////////////////////////////
0237: //
0238: // Load tiles informations, especially the bounds, which will be
0239: // reused
0240: //
0241: // /////////////////////////////////////////////////////////////////////
0242: tileIndexStore = new ShapefileDataStore(this .sourceURL);
0243: if (LOGGER.isLoggable(Level.FINE))
0244: LOGGER.fine("Connected mosaic reader to its data store "
0245: + sourceURL.toString());
0246: final String[] typeNames = tileIndexStore.getTypeNames();
0247: if (typeNames.length <= 0)
0248: throw new IllegalArgumentException(
0249: "Problems when opening the index, no typenames for the schema are defined");
0250:
0251: typeName = typeNames[0];
0252: featureSource = tileIndexStore.getFeatureSource(typeName);
0253:
0254: // //
0255: //
0256: // Load all the features inside the index
0257: //
0258: // //
0259: if (LOGGER.isLoggable(Level.FINE))
0260: LOGGER.fine("About to create index");
0261: index = new SoftReference(new MemorySpatialIndex(featureSource
0262: .getFeatures()));
0263: if (LOGGER.isLoggable(Level.FINE))
0264: LOGGER.fine("Created index");
0265: // //
0266: //
0267: // get the crs if able to
0268: //
0269: // //
0270: final Object tempCRS = this .hints
0271: .get(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM);
0272: if (tempCRS != null) {
0273: this .crs = (CoordinateReferenceSystem) tempCRS;
0274: LOGGER.log(Level.WARNING, new StringBuffer(
0275: "Using forced coordinate reference system ")
0276: .append(crs.toWKT()).toString());
0277: } else {
0278: final CoordinateReferenceSystem tempcrs = featureSource
0279: .getSchema().getDefaultGeometry()
0280: .getCoordinateSystem();
0281: if (tempcrs == null) {
0282: // use the default crs
0283: crs = AbstractGridFormat.getDefaultCRS();
0284: LOGGER
0285: .log(
0286: Level.WARNING,
0287: new StringBuffer(
0288: "Unable to find a CRS for this coverage, using a default one: ")
0289: .append(crs.toWKT()).toString());
0290: } else
0291: crs = tempcrs;
0292: }
0293:
0294: // /////////////////////////////////////////////////////////////////////
0295: //
0296: // Load properties file with information about levels and envelope
0297: //
0298: // /////////////////////////////////////////////////////////////////////
0299: // property file
0300: loadProperties();
0301:
0302: // load the unavailaible pattern
0303: unavailableImage = ImageIO.read(this .getClass().getResource(
0304: "unav.png"));
0305: }
0306:
0307: /**
0308: * Loads the properties file that contains useful information about this
0309: * coverage.
0310: *
0311: * @throws UnsupportedEncodingException
0312: * @throws IOException
0313: * @throws FileNotFoundException
0314: */
0315: private void loadProperties() throws UnsupportedEncodingException,
0316: IOException, FileNotFoundException {
0317: String temp = URLDecoder.decode(sourceURL.getFile(), "UTF8");
0318: final int index = temp.lastIndexOf(".");
0319: if (index != -1)
0320: temp = temp.substring(0, index);
0321: final File propertiesFile = new File(new StringBuffer(temp)
0322: .append(".properties").toString());
0323: assert propertiesFile.exists() && propertiesFile.isFile();
0324: final Properties properties = new Properties();
0325: properties.load(new BufferedInputStream(new FileInputStream(
0326: propertiesFile)));
0327:
0328: // load the envelope
0329: final String envelope = properties.getProperty("Envelope2D");
0330: String[] pairs = envelope.split(" ");
0331: final double cornersV[][] = new double[2][2];
0332: String pair[];
0333: for (int i = 0; i < 2; i++) {
0334: pair = pairs[i].split(",");
0335: cornersV[i][0] = Double.parseDouble(pair[0]);
0336: cornersV[i][1] = Double.parseDouble(pair[1]);
0337: }
0338: this .originalEnvelope = new GeneralEnvelope(cornersV[0],
0339: cornersV[1]);
0340: this .originalEnvelope.setCoordinateReferenceSystem(crs);
0341:
0342: // resolutions levels
0343: numOverviews = Integer.parseInt(properties
0344: .getProperty("LevelsNum")) - 1;
0345: final String levels = properties.getProperty("Levels");
0346: pairs = levels.split(" ");
0347: overViewResolutions = numOverviews > 1 ? new double[numOverviews][2]
0348: : null;
0349: pair = pairs[0].split(",");
0350: highestRes = new double[2];
0351: highestRes[0] = Double.parseDouble(pair[0]);
0352: highestRes[1] = Double.parseDouble(pair[1]);
0353:
0354: if (LOGGER.isLoggable(Level.FINE))
0355: LOGGER.fine(new StringBuffer("Highest res ").append(
0356: highestRes[0]).append(" ").append(highestRes[1])
0357: .toString());
0358:
0359: for (int i = 1; i < numOverviews + 1; i++) {
0360: pair = pairs[i].split(",");
0361: overViewResolutions[i - 1][0] = Double.parseDouble(pair[0]);
0362: overViewResolutions[i - 1][1] = Double.parseDouble(pair[1]);
0363: }
0364:
0365: // name
0366: coverageName = properties.getProperty("Name");
0367:
0368: // need a color expansion?
0369: // this is a newly added property we have to be ready to the case where
0370: // we do not find it.
0371: try {
0372: expandMe = properties.getProperty("ExpandToRGB")
0373: .equalsIgnoreCase("true");
0374: } catch (Exception e) {
0375: expandMe = false;
0376: }
0377:
0378: // original gridrange (estimated)
0379: originalGridRange = new GeneralGridRange(new Rectangle(
0380: (int) Math.round(originalEnvelope.getLength(0)
0381: / highestRes[0]), (int) Math
0382: .round(originalEnvelope.getLength(1)
0383: / highestRes[1])));
0384: }
0385:
0386: /**
0387: * Constructor.
0388: *
0389: * @param source
0390: * The source object.
0391: * @throws IOException
0392: * @throws UnsupportedEncodingException
0393: *
0394: */
0395: public ImageMosaicReader(Object source) throws IOException {
0396: this (source, null);
0397:
0398: }
0399:
0400: /*
0401: * (non-Javadoc)
0402: *
0403: * @see org.opengis.coverage.grid.GridCoverageReader#getFormat()
0404: */
0405: public Format getFormat() {
0406: return new ImageMosaicFormat();
0407: }
0408:
0409: /*
0410: * (non-Javadoc)
0411: *
0412: * @see org.opengis.coverage.grid.GridCoverageReader#read(org.opengis.parameter.GeneralParameterValue[])
0413: */
0414: public GridCoverage read(GeneralParameterValue[] params)
0415: throws IOException {
0416:
0417: if (LOGGER.isLoggable(Level.FINE)) {
0418: LOGGER.fine("Reading mosaic from " + sourceURL.toString());
0419: LOGGER.fine(new StringBuffer("Highest res ").append(
0420: highestRes[0]).append(" ").append(highestRes[1])
0421: .toString());
0422: }
0423:
0424: // /////////////////////////////////////////////////////////////////////
0425: //
0426: // Checking params
0427: //
0428: // /////////////////////////////////////////////////////////////////////
0429: Color inputTransparentColor = (Color) ImageMosaicFormat.INPUT_TRANSPARENT_COLOR
0430: .getDefaultValue();
0431: Color outputTransparentColor = (Color) ImageMosaicFormat.OUTPUT_TRANSPARENT_COLOR
0432: .getDefaultValue();
0433: double inputImageThreshold = ((Double) ImageMosaicFormat.INPUT_IMAGE_THRESHOLD_VALUE
0434: .getDefaultValue()).doubleValue();
0435: Parameter param = null;
0436: GeneralEnvelope requestedEnvelope = null;
0437: Rectangle dim = null;
0438: boolean blend = false;
0439: if (params != null) {
0440: final int length = params.length;
0441: for (int i = 0; i < length; i++) {
0442: param = (Parameter) params[i];
0443:
0444: if (param.getDescriptor().getName().getCode().equals(
0445: ImageMosaicFormat.READ_GRIDGEOMETRY2D.getName()
0446: .toString())) {
0447: final GridGeometry2D gg = (GridGeometry2D) param
0448: .getValue();
0449: requestedEnvelope = (GeneralEnvelope) gg
0450: .getEnvelope();
0451: dim = gg.getGridRange2D().getBounds();
0452: } else if (param
0453: .getDescriptor()
0454: .getName()
0455: .getCode()
0456: .equals(
0457: ImageMosaicFormat.INPUT_TRANSPARENT_COLOR
0458: .getName().toString())) {
0459: inputTransparentColor = (Color) param.getValue();
0460:
0461: } else if (param
0462: .getDescriptor()
0463: .getName()
0464: .getCode()
0465: .equals(
0466: ImageMosaicFormat.INPUT_IMAGE_THRESHOLD_VALUE
0467: .getName().toString())) {
0468: inputImageThreshold = ((Double) param.getValue())
0469: .doubleValue();
0470:
0471: } else if (param.getDescriptor().getName().getCode()
0472: .equals(
0473: ImageMosaicFormat.FADING.getName()
0474: .toString())) {
0475: blend = ((Boolean) param.getValue()).booleanValue();
0476:
0477: } else if (param
0478: .getDescriptor()
0479: .getName()
0480: .getCode()
0481: .equals(
0482: ImageMosaicFormat.OUTPUT_TRANSPARENT_COLOR
0483: .getName().toString())) {
0484: outputTransparentColor = (Color) param.getValue();
0485:
0486: }
0487: }
0488: }
0489: // /////////////////////////////////////////////////////////////////////
0490: //
0491: // Loading tiles trying to optimize as much as possible
0492: //
0493: // /////////////////////////////////////////////////////////////////////
0494: return loadTiles(requestedEnvelope, inputTransparentColor,
0495: outputTransparentColor, inputImageThreshold, dim, blend);
0496: }
0497:
0498: /**
0499: * Loading the tiles which overlap with the requested envelope with control
0500: * over the <code>inputImageThresholdValue</code>, the fading effect
0501: * between different images, abd the <code>transparentColor</code> for the
0502: * input images.
0503: *
0504: * @param requestedOriginalEnvelope
0505: * bounds the tiles that we will load. Tile outside ths
0506: * {@link GeneralEnvelope} won't even be considered.
0507: *
0508: *
0509: * @param transparentColor
0510: * should be used to control transparency on input images.
0511: * @param outputTransparentColor
0512: * @param inputImageThresholdValue
0513: * should be used to create ROIs on the input images
0514: * @param pixelDimension
0515: * is the dimension in pixels of the requested coverage.
0516: * @param fading
0517: * tells to ask for {@link MosaicDescriptor#MOSAIC_TYPE_BLEND}
0518: * instead of the classic
0519: * {@link MosaicDescriptor#MOSAIC_TYPE_OVERLAY}.
0520: * @return a {@link GridCoverage2D} matching as close as possible the
0521: * requested {@link GeneralEnvelope} and <code>pixelDimension</code>,
0522: * or null in case nothing existed in the requested area.
0523: * @throws IOException
0524: */
0525: private GridCoverage loadTiles(
0526: GeneralEnvelope requestedOriginalEnvelope,
0527: Color transparentColor, Color outputTransparentColor,
0528: double inputImageThresholdValue, Rectangle pixelDimension,
0529: boolean fading) throws IOException {
0530:
0531: if (LOGGER.isLoggable(Level.FINE))
0532: LOGGER
0533: .fine(new StringBuffer(
0534: "Creating mosaic to comply with envelope ")
0535: .append(
0536: requestedOriginalEnvelope != null ? requestedOriginalEnvelope
0537: .toString()
0538: : null)
0539: .append(" crs ")
0540: .append(crs.toWKT())
0541: .append(" dim ")
0542: .append(
0543: pixelDimension == null ? " null"
0544: : pixelDimension.toString())
0545: .toString());
0546: // /////////////////////////////////////////////////////////////////////
0547: //
0548: // Check if we have something to load by intersecting the requested
0549: // envelope with the bounds of the data set.
0550: //
0551: // If the requested envelope is not in the same crs of the data set crs
0552: // we have to perform a conversion towards the latter crs before
0553: // intersecting anything.
0554: //
0555: // /////////////////////////////////////////////////////////////////////
0556: GeneralEnvelope intersectionEnvelope = null;
0557: if (requestedOriginalEnvelope != null) {
0558: if (!CRS.equalsIgnoreMetadata(requestedOriginalEnvelope
0559: .getCoordinateReferenceSystem(), this .crs)) {
0560: try {
0561: // transforming the envelope back to the dataset crs in
0562: // order to interact with the original envelope for this
0563: // mosaic.
0564: final MathTransform transform = operationFactory
0565: .createOperation(
0566: requestedOriginalEnvelope
0567: .getCoordinateReferenceSystem(),
0568: crs).getMathTransform();
0569: if (!transform.isIdentity()) {
0570: requestedOriginalEnvelope = CRSUtilities
0571: .transform(transform,
0572: requestedOriginalEnvelope);
0573: requestedOriginalEnvelope
0574: .setCoordinateReferenceSystem(this .crs);
0575:
0576: if (LOGGER.isLoggable(Level.FINE))
0577: LOGGER.fine(new StringBuffer(
0578: "Reprojected envelope ").append(
0579: requestedOriginalEnvelope
0580: .toString())
0581: .append(" crs ")
0582: .append(crs.toWKT()).toString());
0583: }
0584: } catch (TransformException e) {
0585: throw new DataSourceException(
0586: "Unable to create a coverage for this source",
0587: e);
0588: } catch (FactoryException e) {
0589: throw new DataSourceException(
0590: "Unable to create a coverage for this source",
0591: e);
0592: }
0593: }
0594: if (!requestedOriginalEnvelope.intersects(
0595: this .originalEnvelope, true)) {
0596: if (LOGGER.isLoggable(Level.WARNING))
0597: LOGGER
0598: .warning("The requested envelope does not intersect the envelope of this mosaic, we will return a null coverage.");
0599: return null;
0600: }
0601: intersectionEnvelope = new GeneralEnvelope(
0602: requestedOriginalEnvelope);
0603: // intersect the requested area with the bounds of this layer
0604: intersectionEnvelope.intersect(originalEnvelope);
0605:
0606: } else {
0607: requestedOriginalEnvelope = new GeneralEnvelope(
0608: originalEnvelope);
0609: intersectionEnvelope = requestedOriginalEnvelope;
0610:
0611: }
0612: requestedOriginalEnvelope
0613: .setCoordinateReferenceSystem(this .crs);
0614: intersectionEnvelope.setCoordinateReferenceSystem(this .crs);
0615: // ok we got something to return, let's load records from the index
0616: // /////////////////////////////////////////////////////////////////////
0617: //
0618: // Prepare the filter for loading th needed layers
0619: //
0620: // /////////////////////////////////////////////////////////////////////
0621: final ReferencedEnvelope intersectionJTSEnvelope = new ReferencedEnvelope(
0622: intersectionEnvelope.getMinimum(0),
0623: intersectionEnvelope.getMaximum(0),
0624: intersectionEnvelope.getMinimum(1),
0625: intersectionEnvelope.getMaximum(1), crs);
0626:
0627: // /////////////////////////////////////////////////////////////////////
0628: //
0629: // Load feaures from the index
0630: // In case there are no features under the requested bbox which is legal
0631: // in case the mosaic is not a real sqare, we return a fake mosaic.
0632: //
0633: // /////////////////////////////////////////////////////////////////////
0634: if (LOGGER.isLoggable(Level.FINE))
0635: LOGGER.fine("loading tile for envelope "
0636: + intersectionJTSEnvelope.toString());
0637: final List features = getFeaturesFromIndex(intersectionJTSEnvelope);
0638: if (features == null) {
0639: return fakeMosaic(requestedOriginalEnvelope, pixelDimension);
0640: }
0641: // do we have any feature to load
0642: final Iterator it = features.iterator();
0643: if (!it.hasNext())
0644: return fakeMosaic(requestedOriginalEnvelope, pixelDimension);
0645: final int size = features.size();
0646: if (size > MAX_TILES) {
0647: LOGGER
0648: .warning(new StringBuffer("We can load at most ")
0649: .append(MAX_TILES)
0650: .append(
0651: " tiles while there were requested ")
0652: .append(features)
0653: .append(
0654: "\nI am going to print out a fake coverage, sorry about it!")
0655: .toString());
0656: return fakeMosaic(requestedOriginalEnvelope, pixelDimension);
0657: }
0658: if (LOGGER.isLoggable(Level.FINE))
0659: LOGGER.fine("We have " + size + " tiles to load");
0660: try {
0661: return loadRequestedTiles(requestedOriginalEnvelope,
0662: intersectionEnvelope, transparentColor,
0663: outputTransparentColor, intersectionJTSEnvelope,
0664: features, it, inputImageThresholdValue,
0665: pixelDimension, size, fading);
0666: } catch (DataSourceException e) {
0667: LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
0668: } catch (TransformException e) {
0669: LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
0670: }
0671: return null;
0672:
0673: }
0674:
0675: /**
0676: * Builds up a fake mosaic that consists of a transparent image with a red
0677: * cross.
0678: *
0679: * <p>
0680: * The purpose of the fake mosaic
0681: *
0682: * @param requestedEnvelope
0683: * is the envelope requested by the user.
0684: * @param dim
0685: * indicates the requested dimension for this
0686: * {@link GridCoverage2D} in terms of pixels.
0687: * @param features
0688: * is the number of features touching the requsted envelope.
0689: * @return a {@link GridCoverage2D}.
0690: */
0691: private GridCoverage fakeMosaic(GeneralEnvelope requestedEnvelope,
0692: Rectangle dim) {
0693:
0694: return coverageFactory
0695: .create(coverageName, PatternDescriptor.create(
0696: unavailableImage, new Integer(dim.width),
0697: new Integer(dim.height),
0698: ImageUtilities.NOCACHE_HINT), requestedEnvelope);
0699:
0700: }
0701:
0702: /**
0703: * This method loads the tiles which overlap the requested
0704: * {@link GeneralEnvelope} using the provided values for alpha and input
0705: * ROI.
0706: *
0707: * @param requestedOriginalEnvelope
0708: * @param intersectionEnvelope
0709: * @param transparentColor
0710: * @param outputTransparentColor
0711: * @param requestedJTSEnvelope
0712: * @param features
0713: * @param it
0714: * @param inputImageThresholdValue
0715: * @param dim
0716: * @param numImages
0717: * @param blend
0718: * @return
0719: * @throws DataSourceException
0720: * @throws TransformException
0721: */
0722: private GridCoverage loadRequestedTiles(
0723: GeneralEnvelope requestedOriginalEnvelope,
0724: GeneralEnvelope intersectionEnvelope,
0725: Color transparentColor, Color outputTransparentColor,
0726: final Envelope requestedJTSEnvelope, final List features,
0727: final Iterator it, double inputImageThresholdValue,
0728: Rectangle dim, int numImages, boolean blend)
0729: throws DataSourceException, TransformException {
0730:
0731: try {
0732: // if we get here we have something to load
0733: // /////////////////////////////////////////////////////////////////////
0734: //
0735: // prepare the params for executing a mosaic operation.
0736: //
0737: // /////////////////////////////////////////////////////////////////////
0738: final ParameterBlockJAI pbjMosaic = new ParameterBlockJAI(
0739: "Mosaic");
0740: pbjMosaic.setParameter("mosaicType",
0741: MosaicDescriptor.MOSAIC_TYPE_OVERLAY);
0742:
0743: // /////////////////////////////////////////////////////////////////////
0744: //
0745: // compute the requested resolution given the requested envelope and
0746: // dimension.
0747: //
0748: // /////////////////////////////////////////////////////////////////////
0749: final ImageReadParam readP = new ImageReadParam();
0750: final Integer imageChoice;
0751: if (dim != null)
0752: imageChoice = setReadParams(readP,
0753: requestedOriginalEnvelope, dim);
0754: else
0755: imageChoice = new Integer(0);
0756: if (LOGGER.isLoggable(Level.FINE))
0757: LOGGER.fine(new StringBuffer("Loading level ").append(
0758: imageChoice.toString()).append(
0759: " with subsampling factors ").append(
0760: readP.getSourceXSubsampling()).append(" ")
0761: .append(readP.getSourceYSubsampling())
0762: .toString());
0763: // /////////////////////////////////////////////////////////////////////
0764: //
0765: // Resolution.
0766: //
0767: // I am implicitly assuming that all the images have the same
0768: // resolution. In principle this is not required but in practice
0769: // having different resolution would surely bring to having small
0770: // displacements in the final mosaic which we do not wnat to happen.
0771: //
0772: // /////////////////////////////////////////////////////////////////////
0773: final double[] res;
0774: if (imageChoice.intValue() == 0) {
0775: res = new double[highestRes.length];
0776: res[0] = highestRes[0];
0777: res[1] = highestRes[1];
0778: } else {
0779: final double temp[] = overViewResolutions[imageChoice
0780: .intValue() - 1];
0781: res = new double[temp.length];
0782: res[0] = temp[0];
0783: res[1] = temp[1];
0784:
0785: }
0786: // adjusting the resolution for the source subsampling
0787: res[0] *= readP.getSourceXSubsampling();
0788: res[1] *= readP.getSourceYSubsampling();
0789: // /////////////////////////////////////////////////////////////////////
0790: //
0791: // Envelope of the loaded dataset and upper left corner of this
0792: // envelope.
0793: //
0794: // Ths envelope corresponds to the union of the envelopes of all the
0795: // tiles that intersect the area that was request by the user. It is
0796: // crucial to understand that this geographic area can be, and it
0797: // usually is, bigger then the requested one. This involves doing a
0798: // crop operation at the end of the mosaic creation.
0799: //
0800: // /////////////////////////////////////////////////////////////////////
0801: final Envelope loadedDataSetBound = getLoadedDataSetBoud(features);
0802: final Point2D ULC = new Point2D.Double(loadedDataSetBound
0803: .getMinX(), loadedDataSetBound.getMaxY());
0804:
0805: // /////////////////////////////////////////////////////////////////////
0806: //
0807: // CORE LOOP
0808: //
0809: // Loop over the single features and load the images which
0810: // intersect the requested envelope. Once all of them have been
0811: // loaded, next step is to create the mosaic and then
0812: // crop it as requested.
0813: //
0814: // /////////////////////////////////////////////////////////////////////
0815: final File tempFile = new File(this .sourceURL.getFile());
0816: final String parentLocation = tempFile.getParent();
0817: Feature feature;
0818: String location;
0819: Envelope bound;
0820: PlanarImage loadedImage;
0821: File imageFile;
0822: final ROI[] rois = new ROI[numImages];
0823: final PlanarImage[] alphaChannels = new PlanarImage[numImages];
0824: final Area finalLayout = new Area();
0825:
0826: // reusable parameters
0827: boolean alphaIn = false;
0828: boolean doTransparentColor = false;
0829: boolean doInputImageThreshold = false;
0830: int[] alphaIndex = null;
0831: int i = 0;
0832: Boolean readMetadata = Boolean.FALSE;
0833: Boolean readThumbnails = Boolean.FALSE;
0834: Boolean verifyInput = Boolean.FALSE;
0835: ParameterBlock pbjImageRead;
0836: ColorModel model;
0837:
0838: do {
0839: // /////////////////////////////////////////////////////////////////////
0840: //
0841: // Get location and envelope of the image to load.
0842: //
0843: // /////////////////////////////////////////////////////////////////////
0844: feature = (Feature) it.next();
0845: location = (String) feature.getAttribute("location");
0846: bound = feature.getBounds();
0847:
0848: // /////////////////////////////////////////////////////////////////////
0849: //
0850: // lLad a tile from disk as requested.
0851: //
0852: // /////////////////////////////////////////////////////////////////////
0853: if (LOGGER.isLoggable(Level.FINE))
0854: LOGGER.fine("About to read image number " + i);
0855: imageFile = new File(new StringBuffer(parentLocation)
0856: .append(File.separatorChar).append(location)
0857: .toString());
0858: pbjImageRead = new ParameterBlock();
0859: pbjImageRead.add(ImageIO
0860: .createImageInputStream(imageFile));
0861: pbjImageRead.add(imageChoice);
0862: pbjImageRead.add(readMetadata);
0863: pbjImageRead.add(readThumbnails);
0864: pbjImageRead.add(verifyInput);
0865: pbjImageRead.add(null);
0866: pbjImageRead.add(null);
0867: pbjImageRead.add(readP);
0868: pbjImageRead.add(null);
0869: loadedImage = JAI.create("ImageRead", pbjImageRead);
0870: if (LOGGER.isLoggable(Level.FINE))
0871: LOGGER.fine("Just read image number " + i);
0872:
0873: // /////////////////////////////////////////////////////////////
0874: //
0875: // Input alpha, ROI and transparent color management.
0876: //
0877: // Once I get the first image Ican acquire all the information I
0878: // need in order to decide which actions to while and after
0879: // loading the images.
0880: //
0881: // Specifically, I have to check if the loaded image have
0882: // transparency, because if we do a ROI and/or we have a
0883: // transparent color to set we have to remove it.
0884: //
0885: // /////////////////////////////////////////////////////////////
0886: if (i == 0) {
0887: // //
0888: //
0889: // We check here if the images have an alpha channel or some
0890: // other sort of transparency. In case we have transparency
0891: // I also save the index of the transparent channel.
0892: //
0893: // //
0894: model = loadedImage.getColorModel();
0895: alphaIn = model.hasAlpha();
0896: if (alphaIn)
0897: alphaIndex = new int[] { model
0898: .getNumComponents() - 1 };
0899:
0900: // //
0901: //
0902: // ROI has to be computed depending on the value of the
0903: // input threshold and on the data type of the images.
0904: //
0905: // If I request a threshod of 0 on a byte image, I can skip
0906: // doing the ROI!
0907: //
0908: // //
0909: doInputImageThreshold = checkIfThresholdIsNeeded(
0910: loadedImage, inputImageThresholdValue);
0911:
0912: // //
0913: //
0914: // Checking if we have to do something against the final
0915: // transparent color.
0916: //
0917: // If we have a valid transparent color we have to remove
0918: // the input alpha information.
0919: //
0920: // However a possible optimization is to check for index
0921: // color model images with transparency where the
0922: // transparent color is the same requested here and no ROIs
0923: // requested.
0924: //
0925: // //
0926: if (transparentColor != null) {
0927: // paranoiac check on the provided transparent color
0928: transparentColor = new Color(transparentColor
0929: .getRed(), transparentColor.getGreen(),
0930: transparentColor.getBlue());
0931: doTransparentColor = true;
0932: //
0933: // If the images use an IndexColorModel Bitamsk where
0934: // the transparent color is the same that was requested,
0935: // the optimization is to avoid removing the alpha
0936: // information just to readd it at the end. We can
0937: // simply go with what we have from the input.
0938: //
0939: // However, we have to take into account that no action
0940: // has to be take if a ROI is requested on the input
0941: // images since that would imply doing an RGB
0942: // conversion.
0943: //
0944: //
0945: if (model instanceof IndexColorModel
0946: && alphaIn
0947: && model.getTransparency() == Transparency.BITMASK) {
0948: final IndexColorModel icm = (IndexColorModel) model;
0949: final int transparentPixel = icm
0950: .getTransparentPixel();
0951: if (transparentPixel != -1) {
0952: final int oldTransparentColor = icm
0953: .getRGB(transparentPixel);
0954: if (oldTransparentColor == transparentColor
0955: .getRGB()) {
0956: doTransparentColor = false;
0957: }
0958:
0959: }
0960:
0961: }
0962:
0963: }
0964:
0965: }
0966:
0967: // /////////////////////////////////////////////////////////////////////
0968: //
0969: // add to the mosaic collection
0970: //
0971: // /////////////////////////////////////////////////////////////////////
0972: if (LOGGER.isLoggable(Level.FINE))
0973: LOGGER.fine("Adding to mosaic image number " + i);
0974: addToMosaic(pbjMosaic, bound, ULC, res, loadedImage,
0975: doInputImageThreshold, rois, i,
0976: inputImageThresholdValue, alphaIn, alphaIndex,
0977: alphaChannels, finalLayout, imageFile,
0978: doTransparentColor, transparentColor);
0979:
0980: i++;
0981: } while (i < numImages);
0982:
0983: // /////////////////////////////////////////////////////////////////////
0984: //
0985: // Prepare the last parameters for the mosaic.
0986: //
0987: // First of all we set the input threshold accordingly to the input
0988: // image data type. I find the default value (which is 0) very bad
0989: // for data type other than byte and ushort. With float and double
0990: // it can cut off a large par of fthe dynamic.
0991: //
0992: // Second step is the the management of the input threshold that is
0993: // converted into a roi because the way we want to manage such
0994: // threshold is by applying it on the intensitiy of the input image.
0995: // Note that this ROI has to be mutually exclusive with the alpha
0996: // management due to the rules of the JAI Mosaic Operation which
0997: // ignore the ROIs in case an alpha information is provided for the
0998: // input images.
0999: //
1000: // Third step is the management of the alpha information which can
1001: // be the result of a masking operation upong the request for a
1002: // transparent color or the result of input images with internal
1003: // transparency.
1004: //
1005: // Fourth step is the blending for having nice Fading effect at
1006: // overlapping regions.
1007: //
1008: // /////////////////////////////////////////////////////////////////////
1009: double th = getThreshold(loadedImage.getSampleModel()
1010: .getDataType());
1011: pbjMosaic.setParameter("sourceThreshold",
1012: new double[][] { { th } });
1013: if (doInputImageThreshold) {
1014: // //
1015: //
1016: // Set the ROI parameter in case it was requested by setting a
1017: // threshold.
1018: //
1019: // //
1020: pbjMosaic.setParameter("sourceROI", rois);
1021:
1022: } else if (alphaIn || doTransparentColor) {
1023: // //
1024: //
1025: // In case the input images have transparency information this
1026: // way we can handle it.
1027: //
1028: // //
1029: pbjMosaic.setParameter("sourceAlpha", alphaChannels);
1030:
1031: }
1032: // //
1033: //
1034: // It might important to set the mosaic tpe to blend otherwise
1035: // sometimes strange results jump in.
1036: //
1037: // //
1038: if (blend) {
1039: pbjMosaic.setParameter("mosaicType",
1040: MosaicDescriptor.MOSAIC_TYPE_BLEND);
1041:
1042: }
1043:
1044: // /////////////////////////////////////////////////////////////////////
1045: //
1046: // Create the mosaic image by doing a crop if necessary and also
1047: // managing the transparent color if applicablw. Be aware that
1048: // management of the transparent color involves removing
1049: // transparency information from the input images.
1050: //
1051: // /////////////////////////////////////////////////////////////////////
1052: return prepareMosaic(location, requestedOriginalEnvelope,
1053: intersectionEnvelope, res, loadedDataSetBound,
1054: pbjMosaic, finalLayout, outputTransparentColor);
1055: } catch (IOException e) {
1056: throw new DataSourceException(
1057: "Unable to create this mosaic", e);
1058: }
1059: }
1060:
1061: /**
1062: * ROI has to be computed depending on the value of the input threshold and
1063: * on the data type of the images.
1064: *
1065: * If I request a threshod of 0 on a byte image, I can skip doing the ROI!
1066: *
1067: * @param loadedImage
1068: * to check before applying a threshold.
1069: * @param thresholdValue
1070: * is the value that is suggested to be used for the threshold.
1071: * @return true in case the threshold is to be performed, false otherwise.
1072: */
1073: private boolean checkIfThresholdIsNeeded(PlanarImage loadedImage,
1074: double thresholdValue) {
1075: if (Double.isNaN(thresholdValue)
1076: || Double.isInfinite(thresholdValue))
1077: return false;
1078: switch (loadedImage.getSampleModel().getDataType()) {
1079: case DataBuffer.TYPE_BYTE:
1080: int bTh = (int) thresholdValue;
1081: if (bTh <= 0 || bTh >= 255)
1082: return false;
1083: case DataBuffer.TYPE_USHORT:
1084: int usTh = (int) thresholdValue;
1085: if (usTh <= 0 || usTh >= 65535)
1086: return false;
1087: case DataBuffer.TYPE_SHORT:
1088: int sTh = (int) thresholdValue;
1089: if (sTh <= Short.MIN_VALUE || sTh >= Short.MAX_VALUE)
1090: return false;
1091: case DataBuffer.TYPE_INT:
1092: int iTh = (int) thresholdValue;
1093: if (iTh <= Integer.MIN_VALUE || iTh >= Integer.MAX_VALUE)
1094: return false;
1095: case DataBuffer.TYPE_FLOAT:
1096: float fTh = (float) thresholdValue;
1097: if (fTh <= -Float.MAX_VALUE || fTh >= Float.MAX_VALUE
1098: || Float.isInfinite(fTh) || Float.isNaN(fTh))
1099: return false;
1100: case DataBuffer.TYPE_DOUBLE:
1101: double dTh = (double) thresholdValue;
1102: if (dTh <= -Double.MAX_VALUE || dTh >= Double.MAX_VALUE
1103: || Double.isInfinite(dTh) || Double.isNaN(dTh))
1104: return false;
1105:
1106: }
1107: return true;
1108: }
1109:
1110: /**
1111: * Returns a suitable threshold depending on the {@link DataBuffer} type.
1112: *
1113: * <p>
1114: * Remember that the threshold works with >=.
1115: *
1116: * @param dataType
1117: * to create a low threshold for.
1118: * @return a minimum threshold value suitable for this data type.
1119: */
1120: private double getThreshold(int dataType) {
1121: switch (dataType) {
1122: case DataBuffer.TYPE_BYTE:
1123: case DataBuffer.TYPE_USHORT:
1124: // XXX change to zero when bug fixed
1125: return 1.0;
1126: case DataBuffer.TYPE_INT:
1127: return Integer.MIN_VALUE;
1128: case DataBuffer.TYPE_SHORT:
1129: return Short.MIN_VALUE;
1130: case DataBuffer.TYPE_DOUBLE:
1131: return -Double.MAX_VALUE;
1132: case DataBuffer.TYPE_FLOAT:
1133: return -Float.MAX_VALUE;
1134: }
1135: return 0;
1136: }
1137:
1138: /**
1139: * Retrieves the ULC of the BBOX composed by all the tiles we need to load.
1140: *
1141: * @param double
1142: * @return A {@link Point2D} pointing to the ULC of the smalles area made by
1143: * mosaicking all the tile that actually intersect the passed
1144: * envelope.
1145: * @throws IOException
1146: */
1147: private Envelope getLoadedDataSetBoud(List features)
1148: throws IOException {
1149: // /////////////////////////////////////////////////////////////////////
1150: //
1151: // Load feaures and evaluate envelope
1152: //
1153: // /////////////////////////////////////////////////////////////////////
1154: final Envelope loadedULC = new Envelope();
1155: Iterator it = features.iterator();
1156: while (it.hasNext()) {
1157: loadedULC.expandToInclude(((Feature) it.next())
1158: .getDefaultGeometry().getEnvelopeInternal());
1159: }
1160: return loadedULC;
1161:
1162: }
1163:
1164: /**
1165: * Retrieves the list of features that intersect the provided evelope
1166: * loadinf them inside an index in memory where beeded.
1167: *
1168: * @param envelope
1169: * Envelope for selectig features that intersect.
1170: * @return A list of fetaures.
1171: * @throws IOException
1172: * In case loading the needed features failes.
1173: */
1174: private List getFeaturesFromIndex(final Envelope envelope)
1175: throws IOException {
1176: List features = null;
1177: Object o;
1178: synchronized (index) {
1179: if (LOGGER.isLoggable(Level.FINE))
1180: LOGGER.fine("Trying to use the index...");
1181: o = index.get();
1182: if (o != null) {
1183: if (LOGGER.isLoggable(Level.FINE))
1184: LOGGER.fine("Index does not need to be created...");
1185:
1186: } else {
1187: if (LOGGER.isLoggable(Level.FINE))
1188: LOGGER.fine("Index needa to be recreated...");
1189: o = new MemorySpatialIndex(featureSource.getFeatures());
1190: }
1191: if (LOGGER.isLoggable(Level.FINE))
1192: LOGGER.fine("Index Loaded");
1193: }
1194: features = ((MemorySpatialIndex) o).findFeatures(envelope);
1195: return features;
1196: }
1197:
1198: /**
1199: * Once we reach this method it means that we have loaded all the images
1200: * which were intersecting the requested nevelope. Next step is to create
1201: * the final mosaic image and cropping it to the exact requested envelope.
1202: *
1203: * @param location
1204: *
1205: * @param envelope
1206: * @param requestedEnvelope
1207: * @param intersectionEnvelope
1208: * @param res
1209: * @param loadedTilesEnvelope
1210: * @param pbjMosaic
1211: * @param transparentColor
1212: * @param doAlpha
1213: * @param doTransparentColor
1214: * @param finalLayout
1215: * @param outputTransparentColor
1216: * @param singleImageROI
1217: * @return A {@link GridCoverage}, wewll actually a {@link GridCoverage2D}.
1218: * @throws IllegalArgumentException
1219: * @throws FactoryRegistryException
1220: * @throws DataSourceException
1221: */
1222: private GridCoverage prepareMosaic(String location,
1223: GeneralEnvelope requestedOriginalEnvelope,
1224: GeneralEnvelope intersectionEnvelope, double[] res,
1225: final Envelope loadedTilesEnvelope,
1226: ParameterBlockJAI pbjMosaic, Area finalLayout,
1227: Color outputTransparentColor) throws DataSourceException {
1228: GeneralEnvelope finalenvelope = null;
1229: PlanarImage preparationImage;
1230: Rectangle loadedTilePixelsBound = finalLayout.getBounds();
1231: if (LOGGER.isLoggable(Level.FINE))
1232: LOGGER.fine(new StringBuffer("Loaded bbox ").append(
1233: loadedTilesEnvelope.toString()).append(
1234: " while requested bbox ").append(
1235: requestedOriginalEnvelope.toString()).toString());
1236:
1237: // /////////////////////////////////////////////////////////////////////
1238: //
1239: // Check if we need to do a crop on the loaded tiles or not. Keep into
1240: // account that most part of the time the loaded tiles will be go
1241: // beyoind the requested area, hence there is a need for cropping them
1242: // while mosaicking them.
1243: //
1244: // /////////////////////////////////////////////////////////////////////
1245: final GeneralEnvelope loadedTilesBoundEnv = new GeneralEnvelope(
1246: new double[] { loadedTilesEnvelope.getMinX(),
1247: loadedTilesEnvelope.getMinY() }, new double[] {
1248: loadedTilesEnvelope.getMaxX(),
1249: loadedTilesEnvelope.getMaxY() });
1250: loadedTilesBoundEnv.setCoordinateReferenceSystem(crs);
1251: final double loadedTilesEnvelopeDim0 = loadedTilesBoundEnv
1252: .getLength(0);
1253: final double loadedTilesEnvelopeDim1 = loadedTilesBoundEnv
1254: .getLength(1);
1255: if (!intersectionEnvelope.equals(loadedTilesBoundEnv, Math.min(
1256: (loadedTilesEnvelopeDim0 / loadedTilePixelsBound
1257: .getWidth()) / 2.0,
1258: (loadedTilesEnvelopeDim1 / loadedTilePixelsBound
1259: .getHeight()) / 2.0), false)) {
1260:
1261: // /////////////////////////////////////////////////////////////////////
1262: //
1263: // CROP the mosaic image to the requested BBOX
1264: //
1265: // /////////////////////////////////////////////////////////////////////
1266: // intersect them
1267: final GeneralEnvelope intersection = new GeneralEnvelope(
1268: intersectionEnvelope);
1269: intersection.intersect(loadedTilesBoundEnv);
1270:
1271: // get the transform for going from world to grid
1272: try {
1273: final GridToEnvelopeMapper gridToEnvelopeMapper = new GridToEnvelopeMapper(
1274: new GeneralGridRange(loadedTilePixelsBound),
1275: loadedTilesBoundEnv);
1276: gridToEnvelopeMapper
1277: .setGridType(PixelInCell.CELL_CORNER);
1278: final MathTransform transform = gridToEnvelopeMapper
1279: .createTransform().inverse();
1280: final GeneralGridRange finalRange = new GeneralGridRange(
1281: CRSUtilities.transform(transform, intersection));
1282: // CROP
1283: finalLayout
1284: .intersect(new Area(finalRange.toRectangle()));
1285: Rectangle tempRect = finalLayout.getBounds();
1286:
1287: preparationImage = JAI
1288: .create("Mosaic", pbjMosaic,
1289: new RenderingHints(
1290: JAI.KEY_IMAGE_LAYOUT,
1291: new ImageLayout(tempRect.x,
1292: tempRect.y,
1293: tempRect.width,
1294: tempRect.height)));
1295:
1296: finalenvelope = intersection;
1297:
1298: } catch (MismatchedDimensionException e) {
1299: throw new DataSourceException(
1300: "Problem when creating this mosaic.", e);
1301: } catch (NoninvertibleTransformException e) {
1302: throw new DataSourceException(
1303: "Problem when creating this mosaic.", e);
1304: } catch (TransformException e) {
1305: throw new DataSourceException(
1306: "Problem when creating this mosaic.", e);
1307: }
1308:
1309: } else {
1310: preparationImage = JAI.create("Mosaic", pbjMosaic);
1311: finalenvelope = new GeneralEnvelope(intersectionEnvelope);
1312: }
1313: if (LOGGER.isLoggable(Level.FINE))
1314: LOGGER.fine(new StringBuffer("Mosaic created ").toString());
1315:
1316: //
1317: // ///////////////////////////////////////////////////////////////////
1318: //
1319: // FINAL ALPHA
1320: //
1321: //
1322: // ///////////////////////////////////////////////////////////////////
1323: if (outputTransparentColor != null) {
1324: if (LOGGER.isLoggable(Level.FINE))
1325: LOGGER.fine(new StringBuffer("Support for alpha")
1326: .toString());
1327: //
1328: // ///////////////////////////////////////////////////////////////////
1329: //
1330: // If requested I can perform the ROI operation on the prepared ROI
1331: // image for building up the alpha band
1332: //
1333: //
1334: // ///////////////////////////////////////////////////////////////////
1335: ImageWorker w = new ImageWorker(preparationImage);
1336: if (preparationImage.getColorModel() instanceof IndexColorModel) {
1337: preparationImage = w.maskIndexColorModelByte(
1338: outputTransparentColor).getPlanarImage();
1339: } else
1340: preparationImage = w.maskComponentColorModelByte(
1341: outputTransparentColor).getPlanarImage();
1342:
1343: // ///////////////////////////////////////////////////////////////////
1344: //
1345: // create the coverage
1346: //
1347: //
1348: // ///////////////////////////////////////////////////////////////////
1349: return coverageFactory.create(coverageName,
1350: preparationImage, finalenvelope);
1351: }
1352: // ///////////////////////////////////////////////////////////////////
1353: //
1354: // create the coverage
1355: //
1356: // ///////////////////////////////////////////////////////////////////
1357: return coverageFactory.create(coverageName, preparationImage,
1358: finalenvelope);
1359:
1360: }
1361:
1362: /**
1363: * Adding an image which intersect the requested envelope to the final
1364: * moisaic. This operation means computing the translation factor keeping
1365: * into account the resolution of the actual image, the envelope of the
1366: * loaded dataset and the envelope of this image.
1367: *
1368: * @param pbjMosaic
1369: * @param bound
1370: * Lon-Lat bounds of the loaded image
1371: * @param ulc
1372: * @param res
1373: * @param loadedImage
1374: * @param removeAlpha
1375: * @param rois
1376: * @param i
1377: * @param inputImageThresholdValue
1378: * @param alphaChannels
1379: * @param alphaIndex
1380: * @param alphaIn
1381: * @param finalLayout
1382: * @param imageFile
1383: * @param transparentColor
1384: * @param doTransparentColor
1385: * @throws FileNotFoundException
1386: * @throws IOException
1387: */
1388: private void addToMosaic(ParameterBlockJAI pbjMosaic,
1389: Envelope bound, Point2D ulc, double[] res,
1390: PlanarImage loadedImage, boolean doInputImageThreshold,
1391: ROI[] rois, int i, double inputImageThresholdValue,
1392: boolean alphaIn, int[] alphaIndex,
1393: PlanarImage[] alphaChannels, Area finalLayout,
1394: File imageFile, boolean doTransparentColor,
1395: Color transparentColor) {
1396: // /////////////////////////////////////////////////////////////////////
1397: //
1398: // Computing TRANSLATION AND SCALING FACTORS
1399: //
1400: // Using the spatial resolution we compute the translation factors for
1401: // positioning the actual image correctly in final mosaic.
1402: //
1403: // /////////////////////////////////////////////////////////////////////
1404: PlanarImage readyToMosaicImage = scaleAndTranslate(bound, ulc,
1405: res, loadedImage);
1406:
1407: // ///////////////////////////////////////////////////////////////////
1408: //
1409: // INDEX COLOR MODEL EXPANSION
1410: //
1411: // Take into account the need for an expansions of the original color
1412: // model.
1413: //
1414: // If the original color model is an index color model an expansion
1415: // might be requested in case the differemt palettes are not all the
1416: // same. In this case the mosaic operator from JAI would provide wrong
1417: // results since it would take the first palette and use that one for
1418: // all the other images.
1419: //
1420: // There is a special case to take into account here. In case the input
1421: // images use an IndexColorModel t might happen that the transparent
1422: // color is present in some of them while it is not present in some
1423: // others. This case is the case where for sure a color expansion is
1424: // needed. However we have to take into account that during the masking
1425: // phase the images where the requested transparent color was present
1426: // willl have 4 bands, the other 3. If we want the mosaic to work we
1427: // have to add na extra band to the latter type of images for providing
1428: // alpha information to them.
1429: //
1430: //
1431: // ///////////////////////////////////////////////////////////////////
1432: if (expandMe
1433: && readyToMosaicImage.getColorModel() instanceof IndexColorModel) {
1434: readyToMosaicImage = new ImageWorker(readyToMosaicImage)
1435: .forceComponentColorModel().getPlanarImage();
1436: }
1437:
1438: // ///////////////////////////////////////////////////////////////////
1439: //
1440: // TRANSPARENT COLOR MANAGEMENT
1441: //
1442: //
1443: // ///////////////////////////////////////////////////////////////////
1444: if (doTransparentColor) {
1445: if (LOGGER.isLoggable(Level.FINE))
1446: LOGGER.fine(new StringBuffer(
1447: "Support for alpha on input image number " + i)
1448: .toString());
1449: // /////////////////////////////////////////////////////////////////////
1450: //
1451: // If requested I can perform the ROI operation on the prepared ROI
1452: // image for building up the alpha band
1453: //
1454: // /////////////////////////////////////////////////////////////////////
1455: ImageWorker w = new ImageWorker(readyToMosaicImage);
1456: if (readyToMosaicImage.getColorModel() instanceof IndexColorModel) {
1457: readyToMosaicImage = w.maskIndexColorModelByte(
1458: transparentColor).getPlanarImage();
1459: } else
1460: readyToMosaicImage = w.maskComponentColorModelByte(
1461: transparentColor).getPlanarImage();
1462: alphaIndex = new int[] { readyToMosaicImage.getColorModel()
1463: .getNumComponents() - 1 };
1464:
1465: }
1466: // ///////////////////////////////////////////////////////////////////
1467: //
1468: // ROI
1469: //
1470: // ///////////////////////////////////////////////////////////////////
1471: if (doInputImageThreshold) {
1472: ImageWorker w = new ImageWorker(readyToMosaicImage);
1473: w.tileCacheEnabled(false).intensity().binarize(
1474: inputImageThresholdValue);
1475: rois[i] = w.getImageAsROI();
1476:
1477: } else if (alphaIn || doTransparentColor) {
1478: ImageWorker w = new ImageWorker(readyToMosaicImage);
1479: // /////////////////////////////////////////////////////////////////////
1480: //
1481: // ALPHA in INPUT
1482: //
1483: // I have to select the alpha band and provide it to the final
1484: // mosaic operator. I have to force going to ComponentColorModel in
1485: // case the image is indexed.
1486: //
1487: // /////////////////////////////////////////////////////////////////////
1488: if (readyToMosaicImage.getColorModel() instanceof IndexColorModel) {
1489: alphaChannels[i] = w.forceComponentColorModel()
1490: .retainLastBand().getPlanarImage();
1491: }
1492:
1493: else
1494: alphaChannels[i] = w.retainBands(alphaIndex)
1495: .getPlanarImage();
1496:
1497: }
1498:
1499: // /////////////////////////////////////////////////////////////////////
1500: //
1501: // ADD TO MOSAIC
1502: //
1503: // /////////////////////////////////////////////////////////////////////
1504: pbjMosaic.addSource(readyToMosaicImage);
1505: finalLayout.add(new Area(readyToMosaicImage.getBounds()));
1506:
1507: }
1508:
1509: /**
1510: * Computing TRANSLATION AND SCALING FACTORS
1511: *
1512: * Using the spatial resolution we compute the translation factors for
1513: * positioning the actual image correctly in final mosaic.
1514: *
1515: * @param bound
1516: * @param ulc
1517: * @param res
1518: * @param image
1519: * @return
1520: */
1521: private PlanarImage scaleAndTranslate(Envelope bound, Point2D ulc,
1522: double[] res, PlanarImage image) {
1523: // evaluate translation and scaling factors.
1524: double resX = (bound.getMaxX() - bound.getMinX())
1525: / image.getWidth();
1526: double resY = (bound.getMaxY() - bound.getMinY())
1527: / image.getHeight();
1528: double scaleX = 1.0, scaleY = 1.0;
1529: double xTrans = 0.0, yTrans = 0.0;
1530: if (Math.abs((resX - res[0]) / resX) > EPS
1531: || Math.abs(resY - res[1]) > EPS) {
1532: scaleX = res[0] / resX;
1533: scaleY = res[1] / resY;
1534:
1535: }
1536: xTrans = (bound.getMinX() - ulc.getX()) / res[0];
1537: yTrans = (ulc.getY() - bound.getMaxY()) / res[1];
1538: //
1539: // Optimising scale and translate.
1540: //
1541: // In case the scale factors are very close to 1 we have two
1542: // optimizarions: if fthe translation factors are close to zero we do
1543: // thing, otherwise if thery are integers we do a simple translate.
1544: //
1545: // In the general case when wew have translation and scaling we do a
1546: // warp affine which is the most precise operation we can perform.
1547: //
1548: // //
1549: final ParameterBlock pbjAffine = new ParameterBlock();
1550: if (Math.abs(xTrans - (int) xTrans) < Math.pow(10, -3)
1551: && Math.abs(yTrans - (int) yTrans) < Math.pow(10, -3)
1552: && Math.abs(scaleX - 1) < Math.pow(10, -6)
1553: && Math.abs(scaleY - 1) < Math.pow(10, -6)) {
1554:
1555: // return the original image
1556: if (Math.abs(xTrans) < Math.pow(10, -3)
1557: && Math.abs(yTrans) < Math.pow(10, -3)) {
1558: return image;
1559:
1560: }
1561:
1562: // translation
1563: pbjAffine.addSource(image).add(new Float(xTrans)).add(
1564: new Float(yTrans)).add(
1565: ImageUtilities.NN_INTERPOLATION_HINT
1566: .get(JAI.KEY_INTERPOLATION));
1567: // avoid doing the color expansion now since it might not be needed
1568: return JAI.create("Translate", pbjAffine,
1569: ImageUtilities.DONT_REPLACE_INDEX_COLOR_MODEL);
1570:
1571: }
1572: // translation and scaling
1573: pbjAffine.addSource(image).add(
1574: new AffineTransform(scaleX, 0, 0, scaleY, xTrans,
1575: yTrans)).add(
1576: ImageUtilities.NN_INTERPOLATION_HINT
1577: .get(JAI.KEY_INTERPOLATION));
1578: // avoid doing the color expansion now since it might not be needed
1579: final RenderingHints hints = (RenderingHints) ImageUtilities.DONT_REPLACE_INDEX_COLOR_MODEL
1580: .clone();
1581: // adding the capability to do a border extension which is great when
1582: // doing
1583: hints.add(ImageUtilities.EXTEND_BORDER_BY_COPYING);
1584: return JAI.create("Affine", pbjAffine, hints);
1585:
1586: }
1587: }
|