/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-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.image; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.image.renderable.ParameterBlock; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.net.URL; import java.net.URLDecoder; import java.nio.channels.FileChannel; import java.util.Iterator; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import javax.media.jai.JAI; import javax.media.jai.RenderedOp; 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.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.PrjFileReader; import org.geotools.data.WorldFileReader; import org.geotools.factory.Hints; import org.geotools.geometry.GeneralEnvelope; import org.geotools.image.io.ImageIOExt; import org.geotools.referencing.CRS; import org.geotools.referencing.operation.builder.GridToEnvelopeMapper; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.referencing.operation.transform.ProjectiveTransform; import org.geotools.resources.image.ImageUtilities; import org.opengis.coverage.grid.Format; import org.opengis.coverage.grid.GridCoverageReader; import org.opengis.geometry.Envelope; import org.opengis.geometry.MismatchedDimensionException; import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.ParameterValue; import org.opengis.referencing.FactoryException; import org.opengis.referencing.NoSuchAuthorityCodeException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.TransformException; /** * Reads a GridCoverage from a given source. WorldImage sources only support one * GridCoverage so hasMoreGridCoverages() will return true until the only * GridCoverage is read. No metadata is currently supported, so all related * methods return null. In the early future we will start (hopefully supporting * them). * * @author simone giannecchini * @author alessio fabiani * @author rgould * * * @source $URL$ */ public final class WorldImageReader extends AbstractGridCoverage2DReader implements GridCoverageReader { /** Logger. */ private Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.gce.image"); private boolean wmsRequest; private boolean metaFile; private String parentPath; private String extension; private ImageReaderSpi readerSPI; /** * Class constructor. Construct a new ImageWorldReader to read a * GridCoverage from the source object. The source must point to the raster * file itself, not the world file. If the source is a Java URL it checks if * it is ponting to a file and if so it converts the url into a file. * * @param input * The source of a GridCoverage, can be a File, a URL or an input * stream. * @throws DataSourceException */ public WorldImageReader(Object input) throws DataSourceException { this(input, null); } /** * Class constructor. Construct a new ImageWorldReader to read a * GridCoverage from the source object. The source must point to the raster * file itself, not the world file. If the source is a Java URL it checks if * it is ponting to a file and if so it converts the url into a file. * * @param input * The source of a GridCoverage, can be a File, a URL or an input * stream. * @throws DataSourceException */ public WorldImageReader(Object input,final Hints hints) throws DataSourceException { // ///////////////////////////////////////////////////////////////////// // // Checking input // // ///////////////////////////////////////////////////////////////////// if (input == null) { final IOException ex = new IOException( "WorldImage:No source set to read this coverage."); LOGGER.logp(Level.SEVERE, WorldImageReader.class.toString(), "WorldImageReader", ex.getLocalizedMessage(), ex); throw new DataSourceException(ex); } this.source=input; // // // // managing hints // // // if (this.hints == null) this.hints= new Hints(); if (hints != null) { this.hints.add(hints); } // GridCoverageFactory initialization if (this.hints.containsKey(Hints.GRID_COVERAGE_FACTORY)) { final Object factory = this.hints.get(Hints.GRID_COVERAGE_FACTORY); if (factory != null && factory instanceof GridCoverageFactory) { this.coverageFactory = (GridCoverageFactory) factory; } } if (this.coverageFactory == null) { this.coverageFactory = CoverageFactoryFinder.getGridCoverageFactory(this.hints); } coverageName = "image_coverage"; try { boolean closeMe = true; // ///////////////////////////////////////////////////////////////////// // // Source management // // ///////////////////////////////////////////////////////////////////// if (input instanceof URL) { // URL that point to a file final URL sourceURL = ((URL) input); if (sourceURL.getProtocol().compareToIgnoreCase("file") == 0) { String auth = sourceURL.getAuthority(); String path = sourceURL.getPath(); if (auth != null && !auth.equals("")) { path = "//"+auth+path; } this.source = input = new File(URLDecoder.decode(path, "UTF-8")); } else if (sourceURL.getProtocol().equalsIgnoreCase("http")) { // // getting a stream to the reader // this.source = sourceURL.openStream(); // ///////////////////////////////////////////////////////////////////// // // WMS Request? I want to be able to handle that case too // // ///////////////////////////////////////////////////////////////////// wmsRequest = WMSRequest(input); } } // // // // Name, path, etc... // // // if (input instanceof File) { final File sourceFile = (File) input; final String filename = sourceFile.getName(); final int i = filename.lastIndexOf('.'); final int length = filename.length(); if (i > 0 && i < length - 1) { extension = filename.substring(i + 1).toLowerCase(); } this.parentPath = sourceFile.getParent(); this.coverageName = filename; final int dotIndex = coverageName.lastIndexOf("."); coverageName = (dotIndex == -1) ? coverageName : coverageName .substring(0, dotIndex); } else if (input instanceof URL) input = ((URL) input).openStream(); // // // // Get a stream in order to read from it for getting the basic // information for this coverfage // // // if (input instanceof ImageInputStream){ closeMe = false; }else{ inStreamSPI= ImageIOExt.getImageInputStreamSPI(source); if (inStreamSPI == null) throw new DataSourceException( "No input stream for the provided source"); inStream = inStreamSPI.createInputStreamInstance( this.source, ImageIO.getUseCache(), ImageIO.getCacheDirectory()); } if (inStream == null) throw new IllegalArgumentException( "No input stream for the provided source"); // ///////////////////////////////////////////////////////////////////// // // CRS // // ///////////////////////////////////////////////////////////////////// if (!wmsRequest) { 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 "); } else readCRS(); } // ///////////////////////////////////////////////////////////////////// // // Informations about multiple levels and such // // ///////////////////////////////////////////////////////////////////// getHRInfo(); // release the stream if (closeMe) inStream.close(); } catch (IOException e) { LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e); throw new DataSourceException(e); } catch (TransformException e) { LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e); throw new DataSourceException(e); } } /** * Gets the relevant information for the underlying raster. * * @throws IOException * @throws TransformException */ private void getHRInfo() throws IOException, TransformException { // // // // Get a reader for this format // TODO optimize this using image file extension when possible // // // final Iterator<ImageReader> it = ImageIO.getImageReaders(inStream); if (!it.hasNext()) throw new DataSourceException("No reader avalaible for this source"); final ImageReader reader = (ImageReader) it.next(); readerSPI = reader.getOriginatingProvider(); reader.setInput(inStream); // // // // get the dimension of the hr image and build the model as well as // computing the resolution // // numOverviews = wmsRequest ? 0 : reader.getNumImages(true) - 1; int hrWidth = reader.getWidth(0); int hrHeight = reader.getHeight(0); final Rectangle actualDim = new Rectangle(0, 0, hrWidth, hrHeight); originalGridRange = new GridEnvelope2D(actualDim); // ///////////////////////////////////////////////////////////////////// // // Envelope, coverage name and other resolution information // // ///////////////////////////////////////////////////////////////////// if (source instanceof File) { prepareWorldImageGridToWorldTransform(); // // // // In case we read from a real world file we have toget the envelope // // // if (!metaFile) { final AffineTransform tempTransform = new AffineTransform( (AffineTransform) raster2Model); tempTransform.translate(-0.5, -0.5); originalEnvelope = CRS.transform(ProjectiveTransform .create(tempTransform), new GeneralEnvelope(actualDim)); originalEnvelope.setCoordinateReferenceSystem(crs); highestRes = new double[2]; highestRes[0]=XAffineTransform.getScaleX0(tempTransform); highestRes[1]=XAffineTransform.getScaleY0(tempTransform); } else { // /// // // setting the higher resolution available for this coverage // // /// highestRes = getResolution(originalEnvelope, actualDim, crs); final GridToEnvelopeMapper mapper = new GridToEnvelopeMapper(originalGridRange, originalEnvelope); mapper.setPixelAnchor(PixelInCell.CELL_CENTER); this.raster2Model=mapper.createTransform(); } } // // // // get information for the overviews in case ony exists // // // if (numOverviews >=1) { overViewResolutions = new double[numOverviews][2]; for (int i = 0; i < numOverviews; i++) { overViewResolutions[i][0] = (highestRes[0]*this.originalGridRange.getSpan(0))/reader.getWidth(i+1); overViewResolutions[i][1] = (highestRes[1]*this.originalGridRange.getSpan(1))/reader.getHeight(i+1); } } else overViewResolutions = null; } /** * Returns the format that this Reader accepts. * * @return a new WorldImageFormat class */ public Format getFormat() { return new WorldImageFormat(); } /** * Reads an image from a source stream. Loads an image from a source stream, * then loads the values from the world file and constructs a new * GridCoverage from this information. When reading from a remote stream we * do not look for a world fiel but we suppose those information comes from * a different way (xml, gml, pigeon?) * * @param params * WorldImageReader supports no parameters, it just ignores them. * * @return a new GridCoverage read from the source. * * @throws IllegalArgumentException * DOCUMENT ME! * @throws IOException * DOCUMENT ME! */ public GridCoverage2D read(GeneralParameterValue[] params) throws IllegalArgumentException, IOException { // ///////////////////////////////////////////////////////////////////// // // do we have parameters to use for reading from the specified source // // ///////////////////////////////////////////////////////////////////// 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++) { final ParameterValue param = (ParameterValue) params[i]; 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; } } } } // ///////////////////////////////////////////////////////////////////// // // set params // // ///////////////////////////////////////////////////////////////////// Integer imageChoice = new Integer(0); final ImageReadParam readP = new ImageReadParam(); if (!wmsRequest) { try { imageChoice = setReadParams(overviewPolicy,readP, requestedEnvelope, dim); } catch (TransformException e) { new DataSourceException(e); } } // ///////////////////////////////////////////////////////////////////// // // Reading the source layer // // ///////////////////////////////////////////////////////////////////// // final ImageReader reader = readerSPI.createReaderInstance(); // final ImageInputStream inStream = wmsRequest ? ImageIO // .createImageInputStream(((URL) source).openStream()) : ImageIO // .createImageInputStream(source); // final Hints newHints = (Hints) hints.clone(); // if (!wmsRequest) { // reader.setInput(inStream); // if (!reader.isImageTiled(imageChoice.intValue())) { // final Dimension tileSize = ImageUtilities // .toTileSize(new Dimension(reader.getWidth(imageChoice // .intValue()), reader.getHeight(imageChoice // .intValue()))); // final ImageLayout layout = new ImageLayout(); // layout.setTileGridXOffset(0); // layout.setTileGridYOffset(0); // layout.setTileHeight(tileSize.height); // layout.setTileWidth(tileSize.width); // newHints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout)); // } // } // inStream.close(); final ParameterBlock pbjRead = new ParameterBlock(); pbjRead.add(inStreamSPI!=null?inStreamSPI.createInputStreamInstance(source, ImageIO.getUseCache(), ImageIO.getCacheDirectory()):ImageIO.createImageInputStream(source)); // pbjRead.add(wmsRequest ? ImageIO // .createImageInputStream(((URL) source).openStream()) : ImageIO // .createImageInputStream(source)); pbjRead.add(imageChoice); pbjRead.add(Boolean.FALSE); pbjRead.add(Boolean.FALSE); pbjRead.add(Boolean.FALSE); pbjRead.add(null); pbjRead.add(null); pbjRead.add(readP); pbjRead.add(readerSPI.createReaderInstance()); final RenderedOp coverageRaster=JAI.create("ImageRead", pbjRead, (RenderingHints) newHints); // ///////////////////////////////////////////////////////////////////// // // BUILDING COVERAGE // // ///////////////////////////////////////////////////////////////////// // I need to calculate a new transformation (raster2Model) // between the cropped image and the required // adjustedRequestEnvelope final int ssWidth = coverageRaster.getWidth(); final int ssHeight = coverageRaster.getHeight(); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Coverage read: width = " + ssWidth + " height = " + ssHeight); } // // // // setting new coefficients to define a new affineTransformation // to be applied to the grid to world transformation // ----------------------------------------------------------------------------------- // // With respect to the original envelope, the obtained planarImage // needs to be rescaled. The scaling factors are computed as the // ratio between the cropped source region sizes and the read // image sizes. // // // final double scaleX = originalGridRange.getSpan(0) / (1.0 * ssWidth); final double scaleY = originalGridRange.getSpan(1) / (1.0 * ssHeight); final AffineTransform tempRaster2Model = new AffineTransform((AffineTransform) raster2Model); tempRaster2Model.concatenate(new AffineTransform(scaleX, 0, 0, scaleY, 0, 0)); return createImageCoverage(coverageRaster, ProjectiveTransform.create((AffineTransform) tempRaster2Model)); } /** * This method is used to check if we are connecting directly to a WMS with * a getmap request. In such a case we skip reading all the parameters we * can read from this http string. * * @param input * * @return true if we are dealing with a WMS request, false otherwise. */ private boolean WMSRequest(Object input) { // TODO do we need the requested envelope? if (input instanceof URL && (((URL) input).getProtocol().equalsIgnoreCase("http"))) { try { // getting the query final String query = java.net.URLDecoder.decode(((URL) input) .getQuery().intern(), "UTF-8"); // should we proceed? Let's look for a getmap WMS request if (query.intern().indexOf("GetMap") == -1) { return false; } // tokenizer on $ final String[] pairs = query.split("&"); // parse each pair final int numPairs = pairs.length; String[] kvp = null; for (int i = 0; i < numPairs; i++) { // splitting the pairs kvp = pairs[i].split("="); // checking the fields // BBOX if (kvp[0].equalsIgnoreCase("BBOX")) { // splitting fields kvp = kvp[1].split(","); originalEnvelope = new GeneralEnvelope(new double[] { Double.parseDouble(kvp[0]), Double.parseDouble(kvp[1]) }, new double[] { Double.parseDouble(kvp[2]), Double.parseDouble(kvp[3]) }); } // SRS if (kvp[0].equalsIgnoreCase("SRS")) { crs = CRS.decode(kvp[1], true); } // layers if (kvp[0].equalsIgnoreCase("layers")) { this.coverageName = kvp[1].replaceAll(",", "_"); } } } catch (IOException e) { // TODO how to handle this? return false; } catch (NoSuchAuthorityCodeException e) { // TODO how to handle this? return false; } catch (MismatchedDimensionException e) { // TODO how to handle this? return false; } catch (IndexOutOfBoundsException e) { // TODO how to handle this? return false; } catch (FactoryException e) { // TODO how to handle this? return false; } return true; } return false; } /** * This method is responsible for reading the CRS whhther a projection file * is provided. If no projection file is provided the second choice is the * CRS supplied via the crs paramter. If even this one is not avalaible we * default to EPSG:4326. * * @throws IOException */ private void readCRS() throws IOException { // check to see if there is a projection file if (source instanceof File || (source instanceof URL && (((URL) source).getProtocol() == "file"))) { // getting name for the prj file final String sourceAsString; if (source instanceof File) { sourceAsString = ((File) source).getAbsolutePath(); } else { String auth = ((URL) source).getAuthority(); String path = ((URL) source).getPath(); if (auth != null && !auth.equals("")) { sourceAsString = "//" + auth + path; } else { sourceAsString = path; } } final int index = sourceAsString.lastIndexOf("."); final StringBuffer base = new StringBuffer(sourceAsString.substring(0, index)) .append(".prj"); // does it exist? final File prjFile = new File(base.toString()); if (prjFile.exists()) { // it exists then we have top read it PrjFileReader projReader = null; try { final FileChannel channel = new FileInputStream(prjFile).getChannel(); projReader = new PrjFileReader(channel); crs = projReader.getCoordinateReferenceSystem(); } catch (FileNotFoundException e) { // warn about the error but proceed, it is not fatal // we have at least the default crs to use LOGGER.log(Level.INFO, e.getLocalizedMessage(), e); } catch (IOException e) { // warn about the error but proceed, it is not fatal // we have at least the default crs to use LOGGER.log(Level.INFO, e.getLocalizedMessage(), e); } catch (FactoryException e) { // warn about the error but proceed, it is not fatal // we have at least the default crs to use LOGGER.log(Level.INFO, e.getLocalizedMessage(), e); } finally { if (projReader != null) try { projReader.close(); } catch (IOException e) { // warn about the error but proceed, it is not fatal // we have at least the default crs to use LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e); } } } } if (crs == null) { crs = AbstractGridFormat.getDefaultCRS(); LOGGER.info("Unable to find crs, continuing with default CRS"); } } /** * This method is in charge for reading the metadata file and for creating a * valid envelope (whether possible); * * TODO it would be great to having a centralized management for the world * file * * @throws IOException */ private void prepareWorldImageGridToWorldTransform() throws IOException { // getting name and extension final String base = (parentPath != null) ? new StringBuffer( this.parentPath).append(File.separator).append(coverageName) .toString() : coverageName; // We can now construct the baseURL from this string. File file2Parse = new File(new StringBuffer(base).append(".wld") .toString()); if (file2Parse.exists()) { final WorldFileReader reader = new WorldFileReader(file2Parse); raster2Model = reader.getTransform(); } else { // looking for another extension final Set<String> ext=WorldImageFormat.getWorldExtension(extension); final Iterator<String> it=ext.iterator(); if(!it.hasNext()) throw new DataSourceException("Unable to parse extension "+extension); do{ file2Parse = new File(new StringBuffer(base).append((String)it.next() ).toString()); }while(!file2Parse.exists()&&it.hasNext()); if (file2Parse.exists()) { // parse world file final WorldFileReader reader = new WorldFileReader(file2Parse); raster2Model = reader.getTransform(); metaFile = false; } else { // looking for a meta file file2Parse = new File(new StringBuffer(base).append(".meta") .toString()); if (file2Parse.exists()) { parseMetaFile(file2Parse); metaFile = true; } else { final IOException ex = new IOException( "No file with meta information found!"); LOGGER .logp( Level.SEVERE, WorldImageReader.class.toString(), "private void prepareWorldImage2Model() throws IOException", ex.getLocalizedMessage(), ex); throw ex; } } } } /** * This method is responsible for parsing a META file which is nothing more * than another format of a WorldFile used by the GIDB database. * * @param file2Parse * * * @throws NumberFormatException * @throws IOException * * @task move me to a separate implementation */ private void parseMetaFile(File file2Parse) throws NumberFormatException, IOException { double xMin = 0.0; double yMax = 0.0; double xMax = 0.0; double yMin = 0.0; // getting a buffered reader final BufferedReader in = new BufferedReader(new FileReader(file2Parse)); // parsing the lines String str = null; int index = 0; double value = 0; while ((str = in.readLine()) != null) { switch (index) { case 1: value = Double.parseDouble(str.substring("Origin Longitude = " .intern().length())); xMin = value; break; case 2: value = Double.parseDouble(str.substring("Origin Latitude = " .intern().length())); yMin = value; break; case 3: value = Double.parseDouble(str.substring("Corner Longitude = " .intern().length())); xMax = value; break; case 4: value = Double.parseDouble(str.substring("Corner Latitude = " .intern().length())); yMax = value; break; default: break; } index++; } in.close(); // building up envelope of this coverage originalEnvelope = new GeneralEnvelope(new double[] { xMin, yMin }, new double[] { xMax, yMax }); originalEnvelope.setCoordinateReferenceSystem(crs); } /** * Number of coverages for this reader is 1 * * @return the number of coverages for this reader. */ @Override public int getGridCoverageCount() { return 1; } /** * Returns the file extension of the image. * * @since 2.7 */ public String getExtension() { return extension; } }