001: /*
002: * Geotools2 - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2002, Geotools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: *
016: */
017: package org.geotools.gce.arcgrid;
018:
019: import it.geosolutions.imageio.plugins.arcgrid.AsciiGridsImageMetadata;
020: import it.geosolutions.imageio.plugins.arcgrid.AsciiGridsImageWriter;
021: import it.geosolutions.imageio.plugins.arcgrid.spi.AsciiGridsImageWriterSpi;
022:
023: import java.awt.geom.AffineTransform;
024: import java.awt.image.RenderedImage;
025: import java.io.BufferedWriter;
026: import java.io.File;
027: import java.io.FileWriter;
028: import java.io.IOException;
029: import java.io.OutputStream;
030: import java.io.UnsupportedEncodingException;
031: import java.net.URL;
032: import java.net.URLDecoder;
033: import java.util.Iterator;
034: import java.util.List;
035: import java.util.logging.Level;
036: import java.util.logging.Logger;
037:
038: import javax.imageio.IIOImage;
039: import javax.imageio.ImageIO;
040: import javax.imageio.stream.ImageOutputStream;
041: import javax.media.jai.Interpolation;
042:
043: import org.geotools.coverage.Category;
044: import org.geotools.coverage.GridSampleDimension;
045: import org.geotools.coverage.grid.GeneralGridRange;
046: import org.geotools.coverage.grid.GridCoverage2D;
047: import org.geotools.coverage.grid.GridGeometry2D;
048: import org.geotools.coverage.grid.io.AbstractGridCoverageWriter;
049: import org.geotools.coverage.grid.io.AbstractGridFormat;
050: import org.geotools.coverage.grid.io.imageio.GeoToolsWriteParams;
051: import org.geotools.coverage.processing.DefaultProcessor;
052: import org.geotools.coverage.processing.operation.Resample;
053: import org.geotools.coverage.processing.operation.SelectSampleDimension;
054: import org.geotools.data.DataSourceException;
055: import org.geotools.factory.Hints;
056: import org.geotools.geometry.GeneralEnvelope;
057: import org.geotools.parameter.Parameter;
058: import org.geotools.referencing.operation.matrix.XAffineTransform;
059: import org.geotools.resources.coverage.CoverageUtilities;
060: import org.geotools.resources.i18n.Vocabulary;
061: import org.geotools.resources.i18n.VocabularyKeys;
062: import org.opengis.coverage.grid.Format;
063: import org.opengis.coverage.grid.GridCoverage;
064: import org.opengis.coverage.grid.GridCoverageWriter;
065: import org.opengis.geometry.Envelope;
066: import org.opengis.parameter.GeneralParameterValue;
067: import org.opengis.parameter.ParameterValueGroup;
068: import org.opengis.referencing.crs.CoordinateReferenceSystem;
069: import org.opengis.referencing.cs.AxisDirection;
070:
071: /**
072: * {@link ArcGridWriter} supports writing of an ArcGrid GridCoverage to a
073: * Desination object
074: *
075: * XXX We cannot write a rotated grid coverage, we have to enforce that!
076: *
077: * @author Daniele Romagnoli
078: * @author Simone Giannecchini (simboss)
079: */
080: public final class ArcGridWriter extends AbstractGridCoverageWriter
081: implements GridCoverageWriter {
082: /** Logger. */
083: private final static Logger LOGGER = org.geotools.util.logging.Logging
084: .getLogger("org.geotools.gce.arcgrid");
085:
086: /** Imageio {@link AsciiGridsImageWriter} we will use to write out. */
087: private AsciiGridsImageWriter mWriter = new AsciiGridsImageWriter(
088: new AsciiGridsImageWriterSpi());
089:
090: /** Default {@link ParameterValueGroup} for doing a bandselect. */
091: private final static ParameterValueGroup bandSelectParams;
092:
093: /** Default {@link ParameterValueGroup} for doing a reshape. */
094: private final static ParameterValueGroup reShapeParams;
095:
096: /** Caching a {@link Resample} operation. */
097: private static final Resample resampleFactory = new Resample();
098:
099: /** Caching a {@link SelectSampleDimension} operation. */
100: private static final SelectSampleDimension bandSelectFactory = new SelectSampleDimension();
101: static {
102: DefaultProcessor processor = new DefaultProcessor(new Hints(
103: Hints.LENIENT_DATUM_SHIFT, Boolean.TRUE));
104: bandSelectParams = (ParameterValueGroup) processor
105: .getOperation("SelectSampleDimension").getParameters();
106:
107: reShapeParams = (ParameterValueGroup) processor.getOperation(
108: "Resample").getParameters();
109: }
110:
111: /** Small number for comparisons of angles in this pugin. */
112: private static final double ROTATION_EPS = 1E-3;
113:
114: /** The band of the provided coverage that we want to write down. */
115: private int writeBand = -1;
116:
117: /**
118: * Takes either a URL or a String that points to an ArcGridCoverage file and
119: * converts it to a URL that can then be written to.
120: *
121: * @param destination
122: * the URL or String pointing to the file to load the ArcGrid
123: * @throws DataSourceException
124: */
125: public ArcGridWriter(Object destination) throws DataSourceException {
126: this (destination, null);
127: }
128:
129: /**
130: * Takes either a URL or a String that points to an ArcGridCoverage file and
131: * converts it to a URL that can then be written to.
132: *
133: * @param destination
134: * the URL or String pointing to the file to load the ArcGrid
135: * @throws DataSourceException
136: */
137: public ArcGridWriter(Object destination, Hints hints)
138: throws DataSourceException {
139: this .destination = destination;
140: if (destination instanceof File)
141: try {
142: super .outStream = ImageIO
143: .createImageOutputStream(destination);
144: } catch (IOException e) {
145: if (LOGGER.isLoggable(Level.SEVERE))
146: LOGGER
147: .log(Level.SEVERE, e.getLocalizedMessage(),
148: e);
149: throw new DataSourceException(e);
150: }
151: else if (destination instanceof URL) {
152: final URL dest = (URL) destination;
153: if (dest.getProtocol().equalsIgnoreCase("file")) {
154: File destFile;
155: try {
156: destFile = new File(URLDecoder.decode(dest
157: .getFile(), "UTF-8"));
158: } catch (UnsupportedEncodingException e) {
159: if (LOGGER.isLoggable(Level.SEVERE))
160: LOGGER.log(Level.SEVERE, e
161: .getLocalizedMessage(), e);
162: throw new DataSourceException(e);
163: }
164: try {
165: super .outStream = ImageIO
166: .createImageOutputStream(destFile);
167: } catch (IOException e) {
168: if (LOGGER.isLoggable(Level.SEVERE))
169: LOGGER.log(Level.SEVERE, e
170: .getLocalizedMessage(), e);
171: throw new DataSourceException(e);
172: }
173: }
174:
175: } else if (destination instanceof OutputStream) {
176:
177: try {
178: super .outStream = ImageIO
179: .createImageOutputStream((OutputStream) destination);
180: } catch (IOException e) {
181: if (LOGGER.isLoggable(Level.SEVERE))
182: LOGGER
183: .log(Level.SEVERE, e.getLocalizedMessage(),
184: e);
185: throw new DataSourceException(e);
186: }
187:
188: } else if (destination instanceof ImageOutputStream)
189: this .destination = outStream = (ImageOutputStream) destination;
190: else
191: throw new DataSourceException(
192: "The provided destination cannot be used!");
193: // //
194: //
195: // managing hints
196: //
197: // //
198: if (hints != null) {
199: if (this .hints == null) {
200: this .hints = new Hints(Hints.LENIENT_DATUM_SHIFT,
201: Boolean.TRUE);
202: }
203: this .hints.add(hints);
204: }
205: }
206:
207: /**
208: * Creates a Format object describing the Arc Grid Format
209: *
210: * @return the format of the data source we will write to. (ArcGridFormat in
211: * this case)
212: *
213: * @see org.opengis.coverage.grid.GridCoverageWriter#getFormat()
214: */
215: public Format getFormat() {
216: return new ArcGridFormat();
217: }
218:
219: /**
220: * This method was copied from ArcGridData source
221: *
222: * @param gc
223: * the grid coverage that will be written to the destination
224: * @param parameters
225: * to control this writing process
226: * @param grass
227: * tells me whether to write this coverage in the grass format.
228: *
229: * @throws DataSourceException
230: * indicates an unexpected exception
231: */
232: private void writeGridCoverage(GridCoverage2D gc,
233: GeneralParameterValue[] parameters)
234: throws DataSourceException {
235: try {
236: // /////////////////////////////////////////////////////////////////////
237: //
238: // Checking writing params
239: //
240: // /////////////////////////////////////////////////////////////////////
241: GeoToolsWriteParams gtParams = null;
242: boolean grass = false;
243: boolean forceCellSize = false;
244: final String grassParam = ArcGridFormat.GRASS.getName()
245: .getCode().toString();
246: final String cellSizeParam = ArcGridFormat.FORCE_CELLSIZE
247: .getName().getCode().toString();
248: if (parameters != null) {
249: for (int i = 0; i < parameters.length; i++) {
250: Parameter param = (Parameter) parameters[i];
251: String name = param.getDescriptor().getName()
252: .toString();
253: if (param
254: .getDescriptor()
255: .getName()
256: .getCode()
257: .equals(
258: AbstractGridFormat.GEOTOOLS_WRITE_PARAMS
259: .getName().toString())) {
260: gtParams = (GeoToolsWriteParams) param
261: .getValue();
262: }
263: if (name.equalsIgnoreCase(grassParam))
264: grass = param.booleanValue();
265: if (name.equalsIgnoreCase(cellSizeParam))
266: forceCellSize = param.booleanValue();
267: }
268: }
269: if (gtParams == null)
270: gtParams = new ArcGridWriteParams();
271: // write band
272: int[] writeBands = gtParams.getSourceBands();
273: writeBand = CoverageUtilities.getVisibleBand(gc
274: .getRenderedImage());
275: if ((writeBands == null || writeBands.length == 0 || writeBands.length > 1)
276: && (writeBand < 0 || writeBand > gc
277: .getNumSampleDimensions()))
278: throw new IllegalArgumentException(
279: "You need to supply a valid index for deciding which band to write.");
280: if (!((writeBands == null || writeBands.length == 0 || writeBands.length > 1)))
281: writeBand = writeBands[0];
282:
283: // /////////////////////////////////////////////////////////////////
284: //
285: // Getting CRS and envelope information
286: //
287: // /////////////////////////////////////////////////////////////////
288: final CoordinateReferenceSystem crs = gc
289: .getCoordinateReferenceSystem2D();
290:
291: // /////////////////////////////////////////////////////////////////
292: //
293: // getting visible band, if needed
294: // /////////////////////////////////////////////////////////////////
295: final int numBands = gc.getNumSampleDimensions();
296: if (numBands > 1) {
297: final int visibleBand;
298: if (writeBand > 0 && writeBand < numBands)
299: visibleBand = writeBand;
300: else
301: visibleBand = CoverageUtilities.getVisibleBand(gc);
302:
303: final ParameterValueGroup param = (ParameterValueGroup) ArcGridWriter.bandSelectParams
304: .clone();
305: param.parameter("source").setValue(gc);
306: param.parameter("SampleDimensions").setValue(
307: new int[] { visibleBand });
308: gc = (GridCoverage2D) bandSelectFactory.doOperation(
309: param, null);
310: }
311: // /////////////////////////////////////////////////////////////////
312: //
313: // checking if the coverage needs to be resampled in order to have
314: // square pixels for the esri format
315: //
316: // /////////////////////////////////////////////////////////////////
317: if (!grass && forceCellSize)
318: gc = reShapeData(gc);
319:
320: // /////////////////////////////////////////////////////////////////
321: //
322: // Preparing to write header information
323: //
324: // /////////////////////////////////////////////////////////////////
325: // getting the new envelope after the reshaping
326: final Envelope newEnv = gc.getEnvelope2D();
327:
328: // trying to prepare the header
329: final AffineTransform gridToWorld = (AffineTransform) ((GridGeometry2D) gc
330: .getGridGeometry()).getGridToCRS2D();
331: final double xl = newEnv.getLowerCorner().getOrdinate(0);
332: final double yl = newEnv.getLowerCorner().getOrdinate(1);
333: final double cellsizeX = Math.abs(gridToWorld.getScaleX());
334: final double cellsizeY = Math.abs(gridToWorld.getScaleY());
335:
336: // /////////////////////////////////////////////////////////////////
337: //
338: // Preparing source image and metadata
339: //
340: // /////////////////////////////////////////////////////////////////
341: final RenderedImage source = gc.getRenderedImage();
342: final int cols = source.getWidth();
343: final int rows = source.getHeight();
344:
345: // Preparing main parameters for JAI imageWrite Operation
346: // //
347: // Setting Output
348: // //
349: //mWriter.reset();
350: mWriter.setOutput(outStream);
351:
352: // //
353: // no data management
354: // //
355: double inNoData = getCandidateNoData(gc);
356:
357: // //
358: // Construct a proper asciiGridRaster which supports metadata
359: // setting
360: // //
361:
362: // Setting the source for the image write operation
363: mWriter.write(null, new IIOImage(source, null,
364: new AsciiGridsImageMetadata(cols, rows, cellsizeX,
365: cellsizeY, xl, yl, true, grass, inNoData)),
366: null);
367:
368: // writing crs info
369: writeCRSInfo(crs);
370:
371: // /////////////////////////////////////////////////////////////////
372: //
373: // Creating the imageWrite Operation
374: //
375: // /////////////////////////////////////////////////////////////////
376: mWriter.dispose();
377: // TODO: Auto-dispose. Maybe I need to allow a manual dispose call?
378: } catch (IOException e) {
379: if (LOGGER.isLoggable(Level.SEVERE))
380: LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
381: throw new DataSourceException(e);
382: }
383: }
384:
385: /**
386: * Resampling the raster in order to have square pixels instead of
387: * rectangular which are not suitable for an Esrii ascii grid.
388: *
389: * @param gc
390: * Input coverage.
391: * @return A new coverage with square pixels.
392: */
393: private GridCoverage2D reShapeData(GridCoverage2D gc) {
394:
395: // /////////////////////////////////////////////////////////////////////
396: //
397: // Getting the dx and dy for this coverage and checking if they differ
398: // so much that we need to reshape in order to have square pixels
399: //
400: // /////////////////////////////////////////////////////////////////////
401: final AffineTransform gridToWorld = (AffineTransform) ((GridGeometry2D) gc
402: .getGridGeometry()).getGridToCRS2D();
403: final double dx = XAffineTransform.getScaleX0(gridToWorld);
404: final double dy = XAffineTransform.getScaleY0(gridToWorld);
405: if (AsciiGridsImageWriter.resolutionCheck(dx, dy,
406: AsciiGridsImageWriter.EPS)) {
407: return gc;
408: }
409:
410: // /////////////////////////////////////////////////////////////////////
411: //
412: // Getting info about the original image in order to create the new
413: // gridgeometry
414: //
415: // /////////////////////////////////////////////////////////////////////
416: final RenderedImage image = gc.getRenderedImage();
417: int Nx = image.getWidth();
418: int Ny = image.getHeight();
419: final double _Nx;
420: final double _Ny;
421:
422: // /////////////////////////////////////////////////////////////////////
423: //
424: // Getting info about the original evelope n order to create the new
425: // gridgeometry
426: //
427: // /////////////////////////////////////////////////////////////////////
428: final Envelope oldEnv = gc.getEnvelope2D();
429: final double W = oldEnv.getLength(0);
430: final double H = oldEnv.getLength(1);
431: if ((dx - dy) > ArcGridWriter.ROTATION_EPS) {
432: /**
433: * we have higher resolution on the Y axis we have to increase it on
434: * the X axis as well.
435: */
436:
437: // new number of columns
438: _Nx = W / dy;
439: Nx = (int) (_Nx + 0.5);
440: } else {
441: /**
442: * we have higher resolution on the X axis we have to increase it on
443: * the Y axis as well.
444: */
445:
446: // new number of rows
447: _Ny = H / dx;
448: Ny = (int) (_Ny + 0.5);
449: }
450:
451: // new grid range
452: final GeneralGridRange newGridrange = new GeneralGridRange(
453: new int[] { 0, 0 }, new int[] { Nx, Ny });
454: final GridGeometry2D newGridGeometry = new GridGeometry2D(
455: newGridrange, new GeneralEnvelope(gc.getEnvelope()));
456:
457: // /////////////////////////////////////////////////////////////////////
458: //
459: // Reshaping using the resample operation for having best precision.
460: //
461: // /////////////////////////////////////////////////////////////////////
462: final ParameterValueGroup param = (ParameterValueGroup) reShapeParams
463: .clone();
464: param.parameter("source").setValue(gc);
465: param.parameter("CoordinateReferenceSystem").setValue(
466: gc.getCoordinateReferenceSystem2D());
467: param.parameter("GridGeometry").setValue(newGridGeometry);
468: param
469: .parameter("InterpolationType")
470: .setValue(
471: Interpolation
472: .getInstance(Interpolation.INTERP_NEAREST));
473: return (GridCoverage2D) resampleFactory.doOperation(param,
474: hints);
475: }
476:
477: /**
478: * Writing {@link CoordinateReferenceSystem} WKT representation on a prj
479: * file.
480: *
481: * @param crs
482: * the {@link CoordinateReferenceSystem} to be written out.
483: *
484: * @throws IOException
485: */
486: private void writeCRSInfo(CoordinateReferenceSystem crs)
487: throws IOException {
488: // is it null?
489: if (crs == null) {
490: throw new IllegalArgumentException("CRS cannot be null!");
491: }
492:
493: // get the destination path
494: // getting the path of this object and the name
495: URL url = null;
496: String pathname = null;
497: String name = null;
498:
499: if (this .destination instanceof String) {
500: url = (new File((String) this .destination)).toURL();
501: pathname = url.getPath().substring(0,
502: url.getPath().lastIndexOf("/") + 1);
503: name = url.getPath().substring(
504: url.getPath().lastIndexOf("/") + 1,
505: url.getPath().length());
506: } else if (this .destination instanceof File) {
507: url = ((File) this .destination).toURL();
508: pathname = url.getPath().substring(0,
509: url.getPath().lastIndexOf("/") + 1);
510: name = url.getPath().substring(
511: url.getPath().lastIndexOf("/") + 1,
512: url.getPath().length());
513: } else if (this .destination instanceof URL) {
514: url = (URL) this .destination;
515: pathname = url.getPath().substring(0,
516: url.getPath().lastIndexOf("/") + 1);
517: name = url.getPath().substring(
518: url.getPath().lastIndexOf("/") + 1,
519: url.getPath().length());
520: } else {
521: // do nothing for the moment
522: return;
523: }
524:
525: // build up the name
526: name = new StringBuffer(pathname).append(
527: ((name.indexOf(".") > 0) ? name.substring(0, name
528: .indexOf(".")) : name)).append(".prj")
529: .toString();
530:
531: // create the file
532: final BufferedWriter fileWriter = new BufferedWriter(
533: new FileWriter(name));
534:
535: // write information on crs
536: fileWriter.write(crs.toWKT());
537: fileWriter.close();
538: }
539:
540: /**
541: * Note: The geotools GridCoverage class does not implement the geoAPI
542: * GridCoverage Interface so this method shows an error. All other methods
543: * are using the geotools GridCoverage class
544: *
545: * @see org.opengis.coverage.grid.GridCoverageWriter#write(org.opengis.coverage.grid.GridCoverage,
546: * org.opengis.parameter.GeneralParameterValue[])
547: */
548: public void write(GridCoverage coverage,
549: GeneralParameterValue[] parameters)
550: throws IllegalArgumentException, IOException {
551: ensureWeCanWrite(coverage, parameters);
552: writeGridCoverage((GridCoverage2D) coverage, parameters);
553:
554: }
555:
556: /**
557: * Ascii grids have a few limitations.
558: *
559: * <ol>
560: * <li>The gridcoverage they contain must have a gridToWorld transform
561: * which is a compositions of scale and translate, no skew, no rotation.</li>
562: * <li>The PRJ must be lon-lat (this is an assumption form real world).</li>
563: * </ol>
564: *
565: * @param coverage
566: * to check for the possibility to be written b this writer.
567: * @param parameters
568: * to control the writing process.
569: * @throws IOException
570: */
571: private void ensureWeCanWrite(GridCoverage coverage,
572: GeneralParameterValue[] parameters) throws IOException {
573: // /////////////////////////////////////////////////////////////////////
574: //
575: // RULE 1
576: //
577: // Checking the grid to world transform for having only scale and
578: // translate.
579: //
580: // /////////////////////////////////////////////////////////////////////
581: final AffineTransform gridToWorldTransform = new AffineTransform(
582: (AffineTransform) ((GridGeometry2D) coverage
583: .getGridGeometry()).getGridToCRS2D());
584:
585: final int swapXY = XAffineTransform
586: .getSwapXY(gridToWorldTransform);
587: XAffineTransform.round(gridToWorldTransform, ROTATION_EPS);
588: final double rotation = XAffineTransform
589: .getRotation(gridToWorldTransform);
590: if (swapXY == -1)
591: throw new DataSourceException(
592: "Impossible to encode this coverage as an ascii grid since its"
593: + "transformation is not a simple scale and translate");
594: if (rotation != 0)
595: throw new DataSourceException(
596: "Impossible to encode this coverage as an ascii grid since its"
597: + "transformation is not a simple scale and translate");
598:
599: // /////////////////////////////////////////////////////////////////////
600: //
601: // RULE 2
602: //
603: // Checking the CRS to have flip only at first axis
604: //
605: // /////////////////////////////////////////////////////////////////////
606: final int flip = XAffineTransform.getFlip(gridToWorldTransform);
607: final CoordinateReferenceSystem crs2D = ((GridCoverage2D) coverage)
608: .getCoordinateReferenceSystem2D();
609: // flip==-1 means there is a flip.
610: if (flip > 0)
611: throw new DataSourceException(
612: "Impossible to encode this coverage as an ascii grid since its"
613: + "coordinate reference system has strange axes orientation");
614: // let's check that its the Y axis that's flipped
615: if (!AxisDirection.NORTH.equals(crs2D.getCoordinateSystem()
616: .getAxis(1).getDirection()))
617: throw new DataSourceException(
618: "Impossible to encode this coverage as an ascii grid since its"
619: + "coordinate reference system has strange axes orientation");
620: if (!AxisDirection.EAST.equals(crs2D.getCoordinateSystem()
621: .getAxis(0).getDirection()))
622: throw new DataSourceException(
623: "Impossible to encode this coverage as an ascii grid since its"
624: + "coordinate reference system has strange axes orientation");
625:
626: // /////////////////////////////////////////////////////////////////////
627: //
628: // RULE 3
629: //
630: // Check that we are actually writing a GridCoverage2D
631: //
632: // /////////////////////////////////////////////////////////////////////
633: if (coverage instanceof GridCoverage2D
634: && !(coverage.getGridGeometry() instanceof GridGeometry2D))
635: throw new DataSourceException(
636: "The provided coverage is not a GridCoverage2D");
637: }
638:
639: /**
640: * @see org.opengis.coverage.grid.GridCoverageWriter#dispose()
641: */
642: public void dispose() {
643:
644: if (mWriter != null) {
645: mWriter.dispose();
646: mWriter = null;
647: }
648: }
649:
650: static double getCandidateNoData(GridCoverage2D gc) {
651: // no data management
652: final GridSampleDimension sd = (GridSampleDimension) gc
653: .getSampleDimension(0);
654: final List categories = sd.getCategories();
655: final Iterator it = categories.iterator();
656: Category candidate;
657: double inNoData = Double.NaN;
658: final String noDataName = Vocabulary
659: .format(VocabularyKeys.NODATA);
660: while (it.hasNext()) {
661: candidate = (Category) it.next();
662: final String name = candidate.getName().toString();
663: if (name.equalsIgnoreCase("No Data")
664: || name.equalsIgnoreCase(noDataName)) {
665: inNoData = candidate.getRange().getMaximum();
666: }
667: }
668:
669: return inNoData;
670: }
671:
672: }
|