001: /*
002: * Geotools 2 - OpenSource mapping toolkit
003: * (C) 2006, Geotools Project Managment Committee (PMC)
004: *
005: * This library is free software; you can redistribute it and/or
006: * modify it under the terms of the GNU Lesser General Public
007: * License as published by the Free Software Foundation; either
008: * version 2.1 of the License, or (at your option) any later version.
009: *
010: * This library is distributed in the hope that it will be useful,
011: * but WITHOUT ANY WARRANTY; without even the implied warranty of
012: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013: * Lesser General Public License for more details.
014: *
015: * You should have received a copy of the GNU Lesser General Public
016: * License along with this library; if not, write to the Free Software
017: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018: */
019: package org.geotools.gce.imagepyramid;
020:
021: import java.awt.Rectangle;
022: import java.io.BufferedInputStream;
023: import java.io.File;
024: import java.io.FileInputStream;
025: import java.io.FileNotFoundException;
026: import java.io.IOException;
027: import java.io.RandomAccessFile;
028: import java.io.UnsupportedEncodingException;
029: import java.net.URL;
030: import java.net.URLDecoder;
031: import java.util.Collections;
032: import java.util.Map;
033: import java.util.Properties;
034: import java.util.logging.Level;
035: import java.util.logging.Logger;
036:
037: import javax.imageio.ImageReadParam;
038:
039: import org.geotools.coverage.grid.GeneralGridRange;
040: import org.geotools.coverage.grid.GridCoverage2D;
041: import org.geotools.coverage.grid.GridGeometry2D;
042: import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
043: import org.geotools.coverage.grid.io.AbstractGridFormat;
044: import org.geotools.data.DataSourceException;
045: import org.geotools.data.PrjFileReader;
046: import org.geotools.factory.FactoryRegistryException;
047: import org.geotools.factory.Hints;
048: import org.geotools.gce.imagemosaic.ImageMosaicReader;
049: import org.geotools.geometry.GeneralEnvelope;
050: import org.geotools.parameter.Parameter;
051: import org.geotools.referencing.CRS;
052: import org.geotools.resources.CRSUtilities;
053: import org.geotools.util.SoftValueHashMap;
054: import org.opengis.coverage.grid.Format;
055: import org.opengis.coverage.grid.GridCoverage;
056: import org.opengis.coverage.grid.GridCoverageReader;
057: import org.opengis.parameter.GeneralParameterValue;
058: import org.opengis.referencing.FactoryException;
059: import org.opengis.referencing.crs.CoordinateReferenceSystem;
060: import org.opengis.referencing.operation.TransformException;
061:
062: /**
063: * This reader is repsonsible for providing access to a pyramid of mosaics of
064: * georeferenced coverages that are read directly through imageio readers, like
065: * tiff, pngs, etc...
066: *
067: * <p>
068: * Specifically this plugin relies on the image mosaic plugin to handle each
069: * single level of resolutions avaible, hence all the magic is done inside the
070: * mosaic plugin.
071: *
072: *
073: * <p>
074: * For information on how to build a mosaic, please refere to the
075: * {@link ImageMosaicReader} documentation.
076: *
077: * <p>
078: * If you are looking for information on how to create a pyramid, here you go.
079: *
080: * The pyramid itself does no magic. All the magic is performed by the single
081: * mosaic readers that are polled depending on the requeste resolution levels.
082: * Therefore the <b>first step</b> is having a mosaic of images like geotiff,
083: * tiff, jpeg, or png which is going to be the base for te pyramid.
084: *
085: * <p>
086: * The <b>second step</b> is to build the next (lower resolution) levels for
087: * the pyramid. <br>
088: * If you look inside the spike dire of the geotools project you will find a
089: * (growing) set of tools that can be used for doing processing on coverages.
090: * <br>
091: * Specifically there is one tool called PyramidBuilder that can be used to
092: * build the pyramid level by level.
093: *
094: * <p>
095: * <b>Last step</b> is providing a prj file with the projection of the pyramid
096: * (btw all the levels has to be in the same projection) as well as a properties
097: * file with this structure:
098: *
099: * <pre>
100: * #
101: * #Mon Aug 21 22:23:27 CEST 2006
102: * #name of the coverage
103: * Name=ikonos
104: * #different resolution levels available
105: * 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
106: * #where all the levels reside
107: * LevelsDirs=0 2 4 8 16 32
108: * #number of levels availaible
109: * LevelsNum=6
110: * #envelope for this pyramid
111: * Envelope2D=13.398228477973406,43.591366397808976 13.537912459169803,43.67121274528585
112: * </pre>
113: *
114: * @author Simone Giannecchini
115: * @since 2.3
116: *
117: */
118: public final class ImagePyramidReader extends
119: AbstractGridCoverage2DReader implements GridCoverageReader {
120:
121: /** Logger. */
122: private final static Logger LOGGER = org.geotools.util.logging.Logging
123: .getLogger(ImagePyramidReader.class.toString());
124:
125: /**
126: * The input properties file to read the pyramid information from.
127: */
128: private File sourceFile;
129:
130: /**
131: * The directories where to find the different resolutions levels in
132: * descending order.
133: */
134: private String[] levelsDirs;
135:
136: /**
137: * Cache of {@link ImageMosaicReader} objects for the different levels.
138: *
139: */
140: private Map readers;
141:
142: /**
143: * Constructor for an {@link ImagePyramidReader}.
144: *
145: * @param source
146: * The source object.
147: * @param uHints
148: * {@link Hints} to control the behaviour of this reader.
149: * @throws IOException
150: * @throws UnsupportedEncodingException
151: *
152: */
153: public ImagePyramidReader(Object source, Hints uHints)
154: throws IOException {
155: // /////////////////////////////////////////////////////////////////////
156: //
157: // Forcing longitude first since the geotiff specification seems to
158: // assume that we have first longitude the latitude.
159: //
160: // /////////////////////////////////////////////////////////////////////
161: this .hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER,
162: Boolean.TRUE);
163: if (uHints != null) {
164: // prevent the use from reordering axes
165: uHints.remove(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER);
166: this .hints.add(uHints);
167: }
168: if (source == null) {
169:
170: final IOException ex = new IOException(
171: "ImagePyramidReader:No source set to read this coverage.");
172: throw new DataSourceException(ex);
173: }
174: this .source = source;
175:
176: // /////////////////////////////////////////////////////////////////////
177: //
178: // Check source
179: //
180: // /////////////////////////////////////////////////////////////////////
181: if (source instanceof File)
182: this .sourceFile = ((File) source);
183: else if (source instanceof URL) {
184: final URL tempURL = (URL) source;
185: if (tempURL.getProtocol().equalsIgnoreCase("file"))
186: this .sourceFile = new File(URLDecoder.decode(tempURL
187: .getFile(), "UTF-8"));
188: else
189: throw new IllegalArgumentException(
190: "This plugin accepts only File, URL and String pointing to a valid properties file");
191: } else if (source instanceof String) {
192: final File tempFile = new File((java.lang.String) source);
193: if (tempFile.exists()) {
194: this .sourceFile = tempFile;
195: } else
196: throw new IllegalArgumentException(
197: "This plugin accepts only File, URL and String pointing to a file");
198: } else
199: throw new IllegalArgumentException(
200: "This plugin accepts only File, URL and String pointing to a file");
201: //
202: // ///////////////////////////////////////////////////////////////////
203: //
204: // Load tiles informations, especially the bounds, which will be
205: // reused
206: //
207: //
208: // ///////////////////////////////////////////////////////////////////
209: // //
210: //
211: // get the crs if able to
212: //
213: // //
214: String fileName = sourceFile.getAbsolutePath();
215: final int index = fileName.lastIndexOf('.');
216: if (index != -1)
217: fileName = fileName.substring(0, index);
218: PrjFileReader crsReader;
219: try {
220: crsReader = new PrjFileReader(new RandomAccessFile(
221: new StringBuffer(fileName).append(".prj")
222: .toString(), "r").getChannel());
223: } catch (FactoryException e) {
224: throw new DataSourceException(e);
225: }
226: final Object tempCRS = hints
227: .get(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM);
228: if (tempCRS != null) {
229: this .crs = (CoordinateReferenceSystem) tempCRS;
230: LOGGER.log(Level.WARNING, new StringBuffer(
231: "Using forced coordinate reference system ")
232: .append(crs.toWKT()).toString());
233: } else {
234: final CoordinateReferenceSystem tempcrs = crsReader
235: .getCoordinateReferenceSystem();
236: if (tempcrs == null) {
237: // use the default crs
238: crs = AbstractGridFormat.getDefaultCRS();
239: LOGGER
240: .log(
241: Level.WARNING,
242: new StringBuffer(
243: "Unable to find a CRS for this coverage, using a default one: ")
244: .append(crs.toWKT()).toString());
245: } else
246: crs = tempcrs;
247: }
248: //
249: // ///////////////////////////////////////////////////////////////////
250: //
251: // Load properties file with information about levels and envelope
252: //
253: //
254: // ///////////////////////////////////////////////////////////////////
255: // property file
256: assert sourceFile.exists() && sourceFile.isFile();
257: parseMainFile(sourceFile);
258: }
259:
260: /**
261: * Parses the main properties file loading the information regarding
262: * geographic extent and overviews.
263: *
264: * @param sourceFile
265: * @throws IOException
266: * @throws FileNotFoundException
267: */
268: private void parseMainFile(final File sourceFile)
269: throws IOException, FileNotFoundException {
270: final Properties properties = new Properties();
271: properties.load(new BufferedInputStream(new FileInputStream(
272: sourceFile)));
273:
274: // load the envelope
275: final String envelope = properties.getProperty("Envelope2D");
276: String[] pairs = envelope.split(" ");
277: final double cornersV[][] = new double[2][2];
278: String pair[];
279: for (int i = 0; i < 2; i++) {
280: pair = pairs[i].split(",");
281: cornersV[i][0] = Double.parseDouble(pair[0]);
282: cornersV[i][1] = Double.parseDouble(pair[1]);
283: }
284: this .originalEnvelope = new GeneralEnvelope(cornersV[0],
285: cornersV[1]);
286: this .originalEnvelope.setCoordinateReferenceSystem(crs);
287: // overviews dir
288: numOverviews = Integer.parseInt(properties
289: .getProperty("LevelsNum")) - 1;
290: levelsDirs = properties.getProperty("LevelsDirs").split(" ");
291:
292: // readers soft map
293: final int readersCacheSize = (numOverviews + 1) / 3;
294: readers = Collections.synchronizedMap(new SoftValueHashMap(
295: readersCacheSize == 0 ? numOverviews + 1
296: : readersCacheSize));
297:
298: // resolutions levels
299: final String levels = properties.getProperty("Levels");
300: pairs = levels.split(" ");
301: overViewResolutions = numOverviews > 1 ? new double[numOverviews][2]
302: : null;
303: pair = pairs[0].split(",");
304: highestRes = new double[2];
305: highestRes[0] = Double.parseDouble(pair[0]);
306: highestRes[1] = Double.parseDouble(pair[1]);
307: for (int i = 1; i < numOverviews + 1; i++) {
308: pair = pairs[i].split(",");
309: overViewResolutions[i - 1][0] = Double.parseDouble(pair[0]);
310: overViewResolutions[i - 1][1] = Double.parseDouble(pair[1]);
311: }
312:
313: // name
314: coverageName = properties.getProperty("Name");
315:
316: // original gridrange (estimated)
317: originalGridRange = new GeneralGridRange(new Rectangle(
318: (int) Math.round(originalEnvelope.getLength(0)
319: / highestRes[0]), (int) Math
320: .round(originalEnvelope.getLength(1)
321: / highestRes[1])));
322: }
323:
324: /**
325: * Constructor for an {@link ImagePyramidReader}.
326: *
327: * @param source
328: * The source object.
329: * @throws IOException
330: * @throws UnsupportedEncodingException
331: *
332: */
333: public ImagePyramidReader(Object source) throws IOException {
334: this (source, null);
335:
336: }
337:
338: /*
339: * (non-Javadoc)
340: *
341: * @see org.opengis.coverage.grid.GridCoverageReader#getFormat()
342: */
343: public Format getFormat() {
344: return new ImagePyramidFormat();
345: }
346:
347: /*
348: * (non-Javadoc)
349: *
350: * @see org.opengis.coverage.grid.GridCoverageReader#read(org.opengis.parameter.GeneralParameterValue[])
351: */
352: public GridCoverage read(GeneralParameterValue[] params)
353: throws IOException {
354:
355: // /////////////////////////////////////////////////////////////////////
356: //
357: // Checking params
358: //
359: // /////////////////////////////////////////////////////////////////////
360:
361: Parameter param = null;
362: GeneralEnvelope requestedEnvelope = null;
363: Rectangle dim = null;
364:
365: if (params != null) {
366: final int length = params.length;
367: for (int i = 0; i < length; i++) {
368: param = (Parameter) params[i];
369:
370: if (param.getDescriptor().getName().getCode().equals(
371: ImagePyramidFormat.READ_GRIDGEOMETRY2D
372: .getName().toString())) {
373: final GridGeometry2D gg = (GridGeometry2D) param
374: .getValue();
375: requestedEnvelope = (GeneralEnvelope) gg
376: .getEnvelope();
377: dim = gg.getGridRange2D().getBounds();
378:
379: }
380: }
381: }
382: // /////////////////////////////////////////////////////////////////////
383: //
384: // Loading tiles
385: //
386: // /////////////////////////////////////////////////////////////////////
387: return loadTiles(requestedEnvelope, dim, params);
388: }
389:
390: /**
391: * Loading the tiles which overlap with the requested envelope.
392: *
393: * @param envelope
394: * @param alphaThreshold
395: * @param alpha
396: * @param singleImageROIThreshold
397: * @param singleImageROI
398: * @param dim
399: * @param params
400: * @return A {@link GridCoverage}, well actually a {@link GridCoverage2D}.
401: * @throws IOException
402: */
403: private GridCoverage loadTiles(GeneralEnvelope requestedEnvelope,
404: Rectangle dim, GeneralParameterValue[] params)
405: throws IOException {
406:
407: // /////////////////////////////////////////////////////////////////////
408: //
409: // Check if we have something to load by intersecting the requested
410: // envelope with the bounds of the data set.
411: //
412: // If the requested envelope is not in the same crs of the data set crs
413: // we have to perform a conversion towards the latter crs before
414: // intersecting anything.
415: //
416: // /////////////////////////////////////////////////////////////////////
417: if (requestedEnvelope != null) {
418: if (!CRS.equalsIgnoreMetadata(requestedEnvelope
419: .getCoordinateReferenceSystem(), this .crs)) {
420: try {
421: // transforming the envelope back to the data set crs
422: requestedEnvelope = CRSUtilities
423: .transform(
424: operationFactory
425: .createOperation(
426: requestedEnvelope
427: .getCoordinateReferenceSystem(),
428: crs)
429: .getMathTransform(),
430: requestedEnvelope);
431: requestedEnvelope
432: .setCoordinateReferenceSystem(this .crs);
433: } catch (TransformException e) {
434: throw new DataSourceException(
435: "Unable to create a coverage for this source",
436: e);
437: } catch (FactoryException e) {
438: throw new DataSourceException(
439: "Unable to create a coverage for this source",
440: e);
441: }
442: }
443: if (!requestedEnvelope.intersects(this .originalEnvelope,
444: true))
445: return null;
446:
447: // intersect the requested area with the bounds of this layer
448: requestedEnvelope.intersect(originalEnvelope);
449:
450: } else {
451: requestedEnvelope = new GeneralEnvelope(originalEnvelope);
452:
453: }
454: requestedEnvelope.setCoordinateReferenceSystem(this .crs);
455: // ok we got something to return
456: try {
457: return loadRequestedTiles(requestedEnvelope, dim, params);
458: } catch (DataSourceException e) {
459: LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
460: } catch (TransformException e) {
461: LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
462: }
463: return null;
464:
465: }
466:
467: /**
468: * This method loads the tiles which overlap the requested envelope using
469: * the provided values for alpha and input ROI.
470: *
471: * @param requestedEnvelope
472: * @param alpha
473: * @param alphaThreshold
474: * @param singleImageROI
475: * @param singleImageROIThreshold
476: * @param dim
477: * @param ggParam
478: * @return A {@link GridCoverage}, well actually a {@link GridCoverage2D}.
479: * @throws TransformException
480: * @throws IOException
481: * @throws IOException
482: * @throws FileNotFoundException
483: * @throws IllegalArgumentException
484: * @throws FactoryRegistryException
485: */
486: private GridCoverage loadRequestedTiles(
487: GeneralEnvelope requestedEnvelope, Rectangle dim,
488: GeneralParameterValue[] params) throws TransformException,
489: IOException {
490:
491: // if we get here we have something to load
492: // /////////////////////////////////////////////////////////////////////
493: //
494: // compute the requested resolution
495: //
496: // /////////////////////////////////////////////////////////////////////
497: final ImageReadParam readP = new ImageReadParam();
498: final Integer imageChoice;
499: if (dim != null)
500: imageChoice = setReadParams(readP, requestedEnvelope, dim);
501: else
502: imageChoice = new Integer(0);
503: // /////////////////////////////////////////////////////////////////////
504: //
505: // Check to have the needed reader in memory
506: //
507: // /////////////////////////////////////////////////////////////////////
508: ImageMosaicReader reader = null;
509: synchronized (readers) {
510: Object o = readers.get(imageChoice);
511: if (o == null) {
512: final String levelDirName = levelsDirs[imageChoice
513: .intValue()];
514: final File parentDir = new File(sourceFile
515: .getParentFile(), levelDirName);
516: if (parentDir.exists() && parentDir.isDirectory()) {
517: final File shpFile = new File(parentDir,
518: new StringBuffer(coverageName).append(
519: ".shp").toString());
520: reader = new ImageMosaicReader(shpFile.toURL());
521: readers.put(imageChoice, reader);
522: } else
523: throw new DataSourceException(
524: "Impossible to read the needed resolution level!");
525:
526: } else
527: reader = (ImageMosaicReader) o;
528: }
529:
530: // /////////////////////////////////////////////////////////////////////
531: //
532: // Abusing of the created ImageMosaicreader for getting a
533: // gridcoverage2d.
534: //
535: // /////////////////////////////////////////////////////////////////////
536: return reader.read(params);
537: }
538:
539: /*
540: * (non-Javadoc)
541: *
542: * @see org.opengis.coverage.grid.GridCoverageReader#dispose()
543: */
544: public void dispose() {
545: super.dispose();
546: readers.clear();
547:
548: }
549:
550: }
|