Java tutorial
/* * 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.imagepyramid; import java.awt.Rectangle; import java.io.BufferedInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URL; import java.nio.channels.Channels; import java.util.Map.Entry; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageReadParam; import org.apache.commons.io.IOUtils; import org.geotools.coverage.CoverageFactoryFinder; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.coverage.grid.io.OverviewPolicy; import org.geotools.data.DataSourceException; import org.geotools.data.DataUtilities; import org.geotools.data.PrjFileReader; import org.geotools.factory.FactoryRegistryException; import org.geotools.factory.Hints; import org.geotools.gce.imagemosaic.ImageMosaicReader; import org.geotools.geometry.GeneralEnvelope; import org.geotools.referencing.CRS; import org.geotools.referencing.operation.builder.GridToEnvelopeMapper; import org.opengis.coverage.grid.Format; import org.opengis.coverage.grid.GridCoverage; import org.opengis.coverage.grid.GridCoverageReader; import org.opengis.geometry.Envelope; import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.ParameterValue; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; /** * This reader is repsonsible for providing access to a pyramid of mosaics of * georeferenced coverages that are read directly through imageio readers, like * tiff, pngs, etc... * * <p> * Specifically this plugin relies on the image mosaic plugin to handle each * single level of resolutions avaible, hence all the magic is done inside the * mosaic plugin. * * * <p> * For information on how to build a mosaic, please refere to the * {@link ImageMosaicReader} documentation. * * <p> * If you are looking for information on how to create a pyramid, here you go. * * The pyramid itself does no magic. All the magic is performed by the single * mosaic readers that are polled depending on the requeste resolution levels. * Therefore the <b>first step</b> is having a mosaic of images like geotiff, * tiff, jpeg, or png which is going to be the base for te pyramid. * * <p> * The <b>second step</b> is to build the next (lower resolution) levels for * the pyramid. <br> * If you look inside the spike dire of the geotools project you will find a * (growing) set of tools that can be used for doing processing on coverages. * <br> * Specifically there is one tool called PyramidBuilder that can be used to * build the pyramid level by level. * * <p> * <b>Last step</b> is providing a prj file with the projection of the pyramid * (btw all the levels has to be in the same projection) as well as a properties * file with this structure: * * <pre> * # * #Mon Aug 21 22:23:27 CEST 2006 * #name of the coverage * Name=ikonos * #different resolution levels available * Levels=1.2218682749859724E-5,9.220132503102996E-6 2.4428817977683634E-5,1.844026500620314E-5 4.8840552865873626E-5,3.686350299024973E-5 9.781791400307775E-5,7.372700598049946E-5 1.956358280061555E-4,1.4786360643866836E-4 3.901787184256844E-4,2.9572721287731037E-4 * #where all the levels reside * LevelsDirs=0 2 4 8 16 32 * #number of levels availaible * LevelsNum=6 * #envelope for this pyramid * Envelope2D=13.398228477973406,43.591366397808976 13.537912459169803,43.67121274528585 * </pre> * * @author Simone Giannecchini * @author Stefan Alfons Krueger (alfonx), Wikisquare.de : Support for jar:file:foo.jar/bar.properties like URLs * @since 2.3 * * * * @source $URL$ */ public final class ImagePyramidReader extends AbstractGridCoverage2DReader implements GridCoverageReader { /** Logger. */ private final static Logger LOGGER = org.geotools.util.logging.Logging .getLogger(ImagePyramidReader.class.toString()); /** * The input properties file to read the pyramid information from. */ private URL sourceURL; /** * The directories where to find the different resolutions levels in * descending order. */ private String[] levelsDirs; /** * Cache of {@link ImageMosaicReader} objects for the different levels. * */ private ConcurrentHashMap<Integer, ImageMosaicReader> readers = new ConcurrentHashMap<Integer, ImageMosaicReader>(); /** * Constructor for an {@link ImagePyramidReader}. * * @param source * The source object. * @param uHints * {@link Hints} to control the behaviour of this reader. * @throws IOException * @throws UnsupportedEncodingException * */ public ImagePyramidReader(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); // // // // Check source // // // if (source == null) { throw new DataSourceException("ImagePyramidReader:null source set to read this coverage."); } this.source = source; this.sourceURL = Utils.checkSource(source, uHints); if (sourceURL == null) { throw new IllegalArgumentException("This plugin accepts only File, URL and String pointing to a file"); } // // // // // // get the crs if able to // // // final URL prjURL = DataUtilities.changeUrlExt(sourceURL, "prj"); PrjFileReader crsReader = null; try { crsReader = new PrjFileReader(Channels.newChannel(prjURL.openStream())); } catch (FactoryException e) { throw new DataSourceException(e); } finally { try { crsReader.close(); } catch (Throwable e) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, e.getLocalizedMessage(), e); } } final Object tempCRS = 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()); } else { final CoordinateReferenceSystem tempcrs = crsReader.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 // parseMainFile(sourceURL); } /** * Parses the main properties file loading the information regarding * geographic extent and overviews. * * @param sourceFile * @throws IOException * @throws FileNotFoundException */ private void parseMainFile(final URL sourceURL) throws IOException { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Parsing pyramid properties file at:" + sourceURL.toExternalForm()); } BufferedInputStream propertyStream = null; InputStream openStream = null; try { openStream = sourceURL.openStream(); propertyStream = new BufferedInputStream(openStream); final Properties properties = new Properties(); properties.load(propertyStream); // load the envelope final String envelope = properties.getProperty("Envelope2D"); String[] pairs = envelope.split(" "); final double cornersV[][] = new double[2][2]; String pair[]; for (int i = 0; i < 2; i++) { pair = pairs[i].split(","); cornersV[i][0] = Double.parseDouble(pair[0]); cornersV[i][1] = Double.parseDouble(pair[1]); } this.originalEnvelope = new GeneralEnvelope(cornersV[0], cornersV[1]); this.originalEnvelope.setCoordinateReferenceSystem(crs); // overviews dir numOverviews = Integer.parseInt(properties.getProperty("LevelsNum")) - 1; levelsDirs = properties.getProperty("LevelsDirs").split(" "); // resolutions levels final String levels = properties.getProperty("Levels"); pairs = levels.split(" "); overViewResolutions = numOverviews >= 1 ? new double[numOverviews][2] : null; pair = pairs[0].split(","); highestRes = new double[2]; highestRes[0] = Double.parseDouble(pair[0].trim()); highestRes[1] = Double.parseDouble(pair[1].trim()); for (int i = 1; i < numOverviews + 1; i++) { pair = pairs[i].split(","); overViewResolutions[i - 1][0] = Double.parseDouble(pair[0].trim()); overViewResolutions[i - 1][1] = Double.parseDouble(pair[1].trim()); } // name coverageName = properties.getProperty("Name"); // 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_CORNER); raster2Model = geMapper.createTransform(); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Parsed pyramid properties file at:" + sourceURL.toExternalForm()); } } finally { // close input stream if (propertyStream != null) IOUtils.closeQuietly(propertyStream); if (openStream != null) IOUtils.closeQuietly(openStream); } } /** * Constructor for an {@link ImagePyramidReader}. * * @param source * The source object. * @throws IOException * @throws UnsupportedEncodingException * */ public ImagePyramidReader(Object source) throws IOException { this(source, null); } /* * (non-Javadoc) * * @see org.opengis.coverage.grid.GridCoverageReader#getFormat() */ public Format getFormat() { return new ImagePyramidFormat(); } /* * (non-Javadoc) * * @see org.opengis.coverage.grid.GridCoverageReader#read(org.opengis.parameter.GeneralParameterValue[]) */ public GridCoverage2D read(GeneralParameterValue[] params) throws IOException { GeneralEnvelope requestedEnvelope = null; Rectangle dim = null; OverviewPolicy overviewPolicy = null; if (params != null) { // ///////////////////////////////////////////////////////////////////// // // Checking params // // ///////////////////////////////////////////////////////////////////// if (params != null) { for (int i = 0; i < params.length; i++) { @SuppressWarnings("rawtypes") final ParameterValue param = (ParameterValue) params[i]; if (param == null) { continue; } final String name = param.getDescriptor().getName().getCode(); if (name.equals(AbstractGridFormat.READ_GRIDGEOMETRY2D.getName().toString())) { final GridGeometry2D gg = (GridGeometry2D) param.getValue(); requestedEnvelope = new GeneralEnvelope((Envelope) gg.getEnvelope2D()); dim = gg.getGridRange2D().getBounds(); continue; } if (name.equals(AbstractGridFormat.OVERVIEW_POLICY.getName().toString())) { overviewPolicy = (OverviewPolicy) param.getValue(); continue; } } } } // // Loading tiles // return loadTiles(requestedEnvelope, dim, params, overviewPolicy); } /** * Loading the tiles which overlap with the requested envelope. * * * * @param requestedEnvelope * @param dim * @param params * @param overviewPolicy * @return A {@link GridCoverage}, well actually a {@link GridCoverage2D}. * @throws IOException */ private GridCoverage2D loadTiles(GeneralEnvelope requestedEnvelope, Rectangle dim, GeneralParameterValue[] params, OverviewPolicy overviewPolicy) throws IOException { // // Check if we have something to load by intersecting the requested // envelope with the bounds of the data set. // // If the requested envelope is not in the same crs of the data set crs // we have to perform a conversion towards the latter crs before // intersecting anything. // if (requestedEnvelope != null) { if (!CRS.equalsIgnoreMetadata(requestedEnvelope.getCoordinateReferenceSystem(), this.crs)) { try { // transforming the envelope back to the data set crs final MathTransform transform = CRS .findMathTransform(requestedEnvelope.getCoordinateReferenceSystem(), crs, true); if (!transform.isIdentity()) { requestedEnvelope = CRS.transform(transform, requestedEnvelope); requestedEnvelope.setCoordinateReferenceSystem(this.crs); if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine( new StringBuilder("Reprojected envelope ").append(requestedEnvelope.toString()) .append(" crs ").append(crs.toWKT()).toString()); } } catch (TransformException e) { throw new DataSourceException("Unable to create a coverage for this source", e); } catch (FactoryException e) { throw new DataSourceException("Unable to create a coverage for this source", e); } } if (!requestedEnvelope.intersects(this.originalEnvelope, false)) return null; // intersect the requested area with the bounds of this layer requestedEnvelope.intersect(originalEnvelope); } else { requestedEnvelope = new GeneralEnvelope(originalEnvelope); } requestedEnvelope.setCoordinateReferenceSystem(this.crs); // ok we got something to return try { return loadRequestedTiles(requestedEnvelope, dim, params, overviewPolicy); } catch (TransformException e) { throw new DataSourceException(e); } } /** * This method loads the tiles which overlap the requested envelope using * the provided values for alpha and input ROI. * * @param requestedEnvelope * @param alpha * @param alphaThreshold * @param singleImageROI * @param singleImageROIThreshold * @param dim * @param overviewPolicy * @param ggParam * @return A {@link GridCoverage}, well actually a {@link GridCoverage2D}. * @throws TransformException * @throws IOException * @throws IOException * @throws FileNotFoundException * @throws IllegalArgumentException * @throws FactoryRegistryException */ private GridCoverage2D loadRequestedTiles(GeneralEnvelope requestedEnvelope, Rectangle dim, GeneralParameterValue[] params, OverviewPolicy overviewPolicy) throws TransformException, IOException { // if we get here we have something to load // // compute the requested resolution // final ImageReadParam readP = new ImageReadParam(); Integer imageChoice = 0; if (dim != null) imageChoice = setReadParams(overviewPolicy, readP, requestedEnvelope, dim); // // Check to have the needed reader in memory // // light check to see if this reader had been disposed, not synching for performance. if (readers == null) { throw new IllegalStateException("This ImagePyramidReader has already been disposed"); } ImageMosaicReader reader = readers.get(imageChoice); if (reader == null) { // // we must create the underlying mosaic // final String levelDirName = levelsDirs[imageChoice.intValue()]; final URL parentUrl = DataUtilities.getParentUrl(sourceURL); // look for a shapefile first final String extension = new StringBuilder(levelDirName).append("/").append(coverageName).append(".shp") .toString(); final URL shpFileUrl = DataUtilities.extendURL(parentUrl, extension); if (shpFileUrl.getProtocol() != null && shpFileUrl.getProtocol().equalsIgnoreCase("file") && !DataUtilities.urlToFile(shpFileUrl).exists()) reader = new ImageMosaicReader(DataUtilities.extendURL(parentUrl, levelDirName), hints); else reader = new ImageMosaicReader(shpFileUrl, hints); final ImageMosaicReader putByOtherThreadJustNow = readers.putIfAbsent(imageChoice, reader); if (putByOtherThreadJustNow != null) { // some other thread just did inserted this try { reader.dispose(); } catch (Exception e) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, e.getLocalizedMessage(), e); } } //use the other one reader = putByOtherThreadJustNow; } } // // Abusing of the created ImageMosaicreader for getting a // gridcoverage2d, then rename it // GridCoverage2D mosaicCoverage = reader.read(params); if (mosaicCoverage != null) { return new GridCoverage2D(coverageName, mosaicCoverage); } else { // the mosaic can still return null in corner cases, handle that gracefully return null; } } /** * @see org.opengis.coverage.grid.GridCoverageReader#dispose() */ @Override public synchronized void dispose() { super.dispose(); for (Entry<Integer, ImageMosaicReader> element : readers.entrySet()) element.getValue().dispose(); readers.clear(); } /** * Number of coverages for this reader is 1 * * @return the number of coverages for this reader. */ @Override public int getGridCoverageCount() { return 1; } /** * Returns the highest resolution available. * * TODO The instance variable comes from * {@link AbstractGridCoverage2DReader}, so maybe we should move the getter * there. * */ double[] getHighestRes() { return highestRes; } }