/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2006-2008, 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.gce.imagemosaic; import java.awt.Rectangle; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URL; import java.util.Collection; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.spi.ImageReaderSpi; import org.geotools.coverage.CoverageFactoryFinder; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridCoverageFactory; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.data.DataSourceException; import org.geotools.data.FeatureSource; import org.geotools.data.shapefile.ShapefileDataStore; import org.geotools.factory.Hints; import org.geotools.gce.imagemosaic.ImageMosaicUtils.MosaicConfigurationBean; import org.geotools.geometry.GeneralEnvelope; import org.geotools.referencing.operation.builder.GridToEnvelopeMapper; import org.opengis.coverage.grid.Format; import org.opengis.coverage.grid.GridCoverageReader; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.ParameterValue; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.MathTransform; /** * This reader is responsible for providing access to mosaic of georeferenced * images. Citing JAI documentation: * * The "Mosaic" operation creates a mosaic of two or more source images. This * operation could be used for example to assemble a set of overlapping * geospatially rectified images into a contiguous image. It could also be used * to create a montage of photographs such as a panorama. * * All source images are assumed to have been geometrically mapped into a common * coordinate space. The origin (minX, minY) of each image is therefore taken to * represent the location of the respective image in the common coordinate * system of the sour ce images. This coordinate space will also be that of the * destination image. * * All source images must have the same data type and sample size for all bands * and have the same number of bands as color components. The destination will * have the same data type, sample size, and number of bands and color * components as the sources. * * * @author Simone Giannecchini, GeoSolutions S.A.S * @author Stefan Alfons Krueger (alfonx), Wikisquare.de : Support for jar:file:foo.jar/bar.properties URLs * @since 2.3 * */ @SuppressWarnings("deprecation") public final class ImageMosaicReader extends AbstractGridCoverage2DReader implements GridCoverageReader { /** Logger. */ private final static Logger LOGGER = org.geotools.util.logging.Logging.getLogger(ImageMosaicReader.class); final static ExecutorService multiThreadedLoader= new ThreadPoolExecutor(4,8,30,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>()); /** * Number of coverages for this reader is 1 * * @return the number of coverages for this reader. */ @Override public int getGridCoverageCount() { return 1; } /** * Releases resources held by this reader. * */ @Override public void dispose() { super.dispose(); rasterManager.dispose(); } /** * The source {@link URL} pointing to the index shapefile for this * {@link ImageMosaicReader}. */ URL sourceURL; boolean expandMe; PathType pathType; String locationAttributeName="location"; private RasterManager rasterManager; int maxAllowedTiles=ImageMosaicFormat.MAX_ALLOWED_TILES.getDefaultValue(); /** The suggested SPI to avoid SPI lookup*/ ImageReaderSpi suggestedSPI; /** * Constructor. * * @param source * The source object. * @throws IOException * @throws UnsupportedEncodingException * */ public ImageMosaicReader(Object source, Hints uHints) throws IOException { // // // // managing hints // // // if (this.hints == null) this.hints= new Hints(); if (uHints != null) { this.hints.add(uHints); } this.coverageFactory= CoverageFactoryFinder.getGridCoverageFactory(this.hints); if(this.hints.containsKey(Hints.MAX_ALLOWED_TILES)) { this.maxAllowedTiles= ((Integer)this.hints.get(Hints.MAX_ALLOWED_TILES)); } // ///////////////////////////////////////////////////////////////////// // // Check source // // ///////////////////////////////////////////////////////////////////// if (source == null) { final IOException ex = new IOException("ImageMosaicReader:No source set to read this coverage."); if (LOGGER.isLoggable(Level.WARNING)) LOGGER.log(Level.WARNING, ex.getLocalizedMessage(), ex); throw new DataSourceException(ex); } this.source = source; this.sourceURL=ImageMosaicUtils.checkSource(source); if(this.sourceURL==null) throw new DataSourceException("This plugin accepts File, URL or String. The string may describe a File or an URL"); ShapefileDataStore tileIndexStore=null; // ///////////////////////////////////////////////////////////////////// // // Load tiles informations, especially the bounds, which will be // reused // // ///////////////////////////////////////////////////////////////////// try{ tileIndexStore = new ShapefileDataStore(this.sourceURL); if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Connected mosaic reader to its data store " + sourceURL.toString()); final String[] typeNames = tileIndexStore.getTypeNames(); if (typeNames.length <= 0) throw new IllegalArgumentException("Problems when opening the index, no typenames for the schema are defined"); String typeName = typeNames[0]; FeatureSource<SimpleFeatureType, SimpleFeature> featureSource = tileIndexStore.getFeatureSource(typeName); final SimpleFeatureType schema = featureSource.getSchema(); // // // // get the crs if able to // // // final Object tempCRS = this.hints.get(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM); if (tempCRS != null) { this.crs = (CoordinateReferenceSystem) tempCRS; LOGGER.log(Level.WARNING, "Using forced coordinate reference system "+crs.toWKT().toString()); } else { final CoordinateReferenceSystem tempcrs = featureSource.getSchema().getGeometryDescriptor().getCoordinateReferenceSystem(); if (tempcrs == null) { // use the default crs crs = AbstractGridFormat.getDefaultCRS(); LOGGER.log(Level.WARNING,"Unable to find a CRS for this coverage, using a default one: "+crs.toWKT()); } else crs = tempcrs; } // ///////////////////////////////////////////////////////////////////// // // Load properties file with information about levels and envelope // // ///////////////////////////////////////////////////////////////////// // property file final boolean retValue = loadProperties(); if(!retValue) throw new DataSourceException("Unable to create reader for this mosaic."); // // location attribute field checks // //location attribute override if(this.hints.containsKey(Hints.MOSAIC_LOCATION_ATTRIBUTE)) this.locationAttributeName=((String)this.hints.get(Hints.MOSAIC_LOCATION_ATTRIBUTE)); if(this.locationAttributeName==null) { //get the first string for(AttributeDescriptor attribute: schema.getAttributeDescriptors()){ if(attribute.getType().getBinding().equals(String.class)) this.locationAttributeName=attribute.getName().toString(); } } if(schema.getDescriptor(this.locationAttributeName)==null) throw new DataSourceException("The provided name for the location attribute is invalid."); // creating the raster manager rasterManager= new RasterManager(this); } catch (Throwable e) { throw new DataSourceException(e); } finally{ try { if(tileIndexStore!=null) tileIndexStore.dispose(); } catch (Throwable e) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, e.getLocalizedMessage(), e); } finally{ tileIndexStore=null; } } } /** * Loads the properties file that contains useful information about this * coverage. * * @throws UnsupportedEncodingException * @throws IOException */ private boolean loadProperties(){ final MosaicConfigurationBean configuration=ImageMosaicUtils.loadPropertiesFile(sourceURL, crs,this.locationAttributeName); if(configuration==null) return false; // set properties this.originalEnvelope = new GeneralEnvelope((org.opengis.geometry.Envelope)configuration.getEnvelope2D()); // resolutions levels numOverviews = configuration.getLevelsNum()-1; final double[][] resolutions = configuration.getLevels(); overViewResolutions = numOverviews >= 1 ? new double[numOverviews][2]: null; highestRes = new double[2]; highestRes[0] = resolutions[0][0]; highestRes[1] =resolutions[0][1]; if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine(new StringBuffer("Highest res ").append(highestRes[0]) .append(" ").append(highestRes[1]).toString()); if(numOverviews>0){ for (int i = 0; i < numOverviews; i++) { overViewResolutions[i][0] = resolutions[i+1][0]; overViewResolutions[i][1] = resolutions[i+1][1]; } } // name coverageName = configuration.getName(); // need a color expansion? // this is a newly added property we have to be ready to the case where // we do not find it. expandMe = configuration.isExpandToRGB(); // original gridrange (estimated) originalGridRange = new GridEnvelope2D( new Rectangle( (int) Math.round(originalEnvelope.getSpan(0)/ highestRes[0]), (int) Math.round(originalEnvelope.getSpan(1)/ highestRes[1]) ) ); final GridToEnvelopeMapper geMapper= new GridToEnvelopeMapper(originalGridRange,originalEnvelope); geMapper.setPixelAnchor(PixelInCell.CELL_CENTER); raster2Model= geMapper.createTransform(); // absolute or relative path pathType =configuration.isAbsolutePath()?PathType.ABSOLUTE:PathType.RELATIVE; // // location attribute // locationAttributeName=configuration.getLocationAttribute(); // suggested SPI final String suggestedSPIClass= configuration.getSuggestedSPI(); if(suggestedSPIClass!=null){ try { final Class<?> clazz=Class.forName(suggestedSPIClass); if(clazz.newInstance() instanceof ImageReaderSpi) suggestedSPI=(ImageReaderSpi)clazz.newInstance(); else suggestedSPI=null; } catch (ClassNotFoundException e) { if(LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE,e.getLocalizedMessage(),e); suggestedSPI=null; } catch (InstantiationException e) { if(LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE,e.getLocalizedMessage(),e); suggestedSPI=null; } catch (IllegalAccessException e) { if(LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE,e.getLocalizedMessage(),e); suggestedSPI=null; } } return true; } /** * Constructor. * * @param source * The source object. * @throws IOException * @throws UnsupportedEncodingException * */ public ImageMosaicReader(Object source) throws IOException { this(source, null); } /* * (non-Javadoc) * * @see org.opengis.coverage.grid.GridCoverageReader#getFormat() */ public Format getFormat() { return new ImageMosaicFormat(); } /* * (non-Javadoc) * * @see org.opengis.coverage.grid.GridCoverageReader#read(org.opengis.parameter.GeneralParameterValue[]) */ public GridCoverage2D read(GeneralParameterValue[] params) throws IOException { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Reading mosaic from " + sourceURL.toString()); LOGGER.fine(new StringBuffer("Highest res ").append(highestRes[0]) .append(" ").append(highestRes[1]).toString()); } // // add max allowed tiles if missing // if(this.maxAllowedTiles!=Integer.MAX_VALUE){ if(params!=null){ // first thing let's see if we have it already, in which case we do nothing since a read parameter override a Hint boolean found=false; for(GeneralParameterValue pv:params) { if(pv.getDescriptor().getName().equals(ImageMosaicFormat.MAX_ALLOWED_TILES.getName())) { found=true; break; } } //ok, we did not find it, let's add it back if(!found) { final GeneralParameterValue[] temp = new GeneralParameterValue[params.length+1]; System.arraycopy(params, 0, temp, 0, params.length); ParameterValue<Integer> tempVal = ImageMosaicFormat.MAX_ALLOWED_TILES.createValue(); tempVal.setValue(this.maxAllowedTiles); temp[params.length]=tempVal; } } else { // we do not have nay read params, we have to create the array for them ParameterValue<Integer> tempVal = ImageMosaicFormat.MAX_ALLOWED_TILES.createValue(); tempVal.setValue(this.maxAllowedTiles); params= new GeneralParameterValue[]{tempVal}; } } // ///////////////////////////////////////////////////////////////////// // // Loading tiles trying to optimize as much as possible // // ///////////////////////////////////////////////////////////////////// final Collection<GridCoverage2D> response = rasterManager.read(params); if(response.isEmpty()) return null; else return response.iterator().next(); } /** * Package private accessor for {@link Hints}. * * @return this {@link Hints} used by this reader. */ Hints getHints(){ return super.hints; } /** * Package private accessor for the highest resolution values. * * @return the highest resolution values. */ double[] getHighestRes(){ return super.highestRes; } /** * * @return */ double[][] getOverviewsResolution(){ return super.overViewResolutions; } int getNumberOfOvervies(){ return super.numOverviews; } /** Package scope grid to world transformation accessor */ MathTransform getRaster2Model() { return raster2Model; } /** * Let us retrieve the {@link GridCoverageFactory} that we want to use. * * @return * retrieves the {@link GridCoverageFactory} that we want to use. */ GridCoverageFactory getGridCoverageFactory(){ return coverageFactory; } String getName() { return super.coverageName; } }