001    //$HeadURL: svn+ssh://developername@svn.wald.intevation.org/deegree/base/trunk/src/org/deegree/tools/raster/RasterTreeBuilder.java $
002    /*----------------------------------------------------------------------------
003     This file is part of deegree, http://deegree.org/
004     Copyright (C) 2001-2009 by:
005     Department of Geography, University of Bonn
006     and
007     lat/lon GmbH
008    
009     This library is free software; you can redistribute it and/or modify it under
010     the terms of the GNU Lesser General Public License as published by the Free
011     Software Foundation; either version 2.1 of the License, or (at your option)
012     any later version.
013     This library is distributed in the hope that it will be useful, but WITHOUT
014     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
015     FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
016     details.
017     You should have received a copy of the GNU Lesser General Public License
018     along with this library; if not, write to the Free Software Foundation, Inc.,
019     59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020    
021     Contact information:
022    
023     lat/lon GmbH
024     Aennchenstr. 19, 53177 Bonn
025     Germany
026     http://lat-lon.de/
027    
028     Department of Geography, University of Bonn
029     Prof. Dr. Klaus Greve
030     Postfach 1147, 53001 Bonn
031     Germany
032     http://www.geographie.uni-bonn.de/deegree/
033    
034     e-mail: info@deegree.org
035     ----------------------------------------------------------------------------*/
036    
037    package org.deegree.tools.raster;
038    
039    import static java.io.File.separator;
040    import static java.util.Arrays.sort;
041    
042    import java.awt.Color;
043    import java.awt.Graphics;
044    import java.awt.Graphics2D;
045    import java.awt.RenderingHints;
046    import java.awt.color.ColorSpace;
047    import java.awt.geom.AffineTransform;
048    import java.awt.image.BufferedImage;
049    import java.awt.image.ColorModel;
050    import java.awt.image.ComponentColorModel;
051    import java.awt.image.DataBuffer;
052    import java.awt.image.Raster;
053    import java.awt.image.WritableRaster;
054    import java.io.File;
055    import java.io.FileOutputStream;
056    import java.io.FileWriter;
057    import java.io.FilenameFilter;
058    import java.io.IOException;
059    import java.io.InputStream;
060    import java.io.InputStreamReader;
061    import java.io.PrintStream;
062    import java.io.PrintWriter;
063    import java.io.Reader;
064    import java.net.URI;
065    import java.net.URL;
066    import java.util.ArrayList;
067    import java.util.Arrays;
068    import java.util.Comparator;
069    import java.util.HashMap;
070    import java.util.Hashtable;
071    import java.util.Iterator;
072    import java.util.List;
073    import java.util.Map;
074    import java.util.Properties;
075    
076    import javax.media.jai.BorderExtender;
077    import javax.media.jai.JAI;
078    import javax.media.jai.RenderedOp;
079    
080    import net.sf.ehcache.Cache;
081    import net.sf.ehcache.CacheManager;
082    import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
083    
084    import org.deegree.datatypes.QualifiedName;
085    import org.deegree.datatypes.Types;
086    import org.deegree.framework.log.ILogger;
087    import org.deegree.framework.log.LoggerFactory;
088    import org.deegree.framework.util.ImageUtils;
089    import org.deegree.framework.util.StringTools;
090    import org.deegree.framework.xml.XMLFragment;
091    import org.deegree.framework.xml.XSLTDocument;
092    import org.deegree.graphics.transformation.GeoTransform;
093    import org.deegree.graphics.transformation.WorldToScreenTransform;
094    import org.deegree.io.dbaseapi.DBaseFile;
095    import org.deegree.io.quadtree.IndexException;
096    import org.deegree.io.rtree.HyperBoundingBox;
097    import org.deegree.io.rtree.HyperPoint;
098    import org.deegree.io.rtree.RTree;
099    import org.deegree.io.shpapi.ShapeFile;
100    import org.deegree.model.coverage.grid.GridCoverageExchange;
101    import org.deegree.model.coverage.grid.WorldFile;
102    import org.deegree.model.crs.GeoTransformer;
103    import org.deegree.model.feature.Feature;
104    import org.deegree.model.feature.FeatureCollection;
105    import org.deegree.model.feature.FeatureFactory;
106    import org.deegree.model.feature.FeatureProperty;
107    import org.deegree.model.feature.schema.FeatureType;
108    import org.deegree.model.feature.schema.PropertyType;
109    import org.deegree.model.spatialschema.Envelope;
110    import org.deegree.model.spatialschema.Geometry;
111    import org.deegree.model.spatialschema.GeometryFactory;
112    import org.deegree.model.spatialschema.Position;
113    import org.deegree.ogcbase.CommonNamespaces;
114    import org.deegree.processing.raster.converter.Image2RawData;
115    import org.deegree.processing.raster.converter.RawData2Image;
116    
117    import com.sun.media.jai.codec.FileSeekableStream;
118    import com.sun.media.jai.codec.MemoryCacheSeekableStream;
119    import com.sun.media.jai.codec.SeekableStream;
120    
121    /**
122     * This class represents a <code>RasterTreeBuilder</code> object.<br>
123     * It wcan be used to create a resolution pyramid from one or more already existing raster dataset (image). The
124     * resulting pyramid will be described by a set of shapes (containing the image tiles bounding boxes) and a XML coverage
125     * description document that can be used with the deegree WCS. The RTB supports real images like png, tif, jpeg, bmp and
126     * gif as well as raw data image like 16Bit and 32Bit tif-images without color model. <br>
127     * because of the large amount of data that may be process by the RTB it makes use of a caching mechnism. For this the
128     * ehcache project is used. One can configure the cache behavior by placing a file named ehcache.xml defining a cache
129     * named 'imgCache' within the class root when starting the RTB. (For details please see the ehcache documentation). If
130     * no ehcache.xml is available default cache configuration will be used which is set to:
131     * <ul>
132     * <li>maxElementsInMemory = 10
133     * <li>memoryStoreEvictionPolicy = LFU
134     * <li>overflowToDisk = false (notice that overflow to disk is not supported because cached objects are not
135     * serializable)
136     * <li>eternal = false
137     * <li>timeToLiveSeconds = 3600
138     * <li>timeToIdleSeconds = 3600
139     * </ul>
140     * 
141     * 
142     * @author <a href="mailto:mays@lat-lon.de">Judit Mays</a>
143     * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a>
144     * @author last edited by: $Author: apoth $
145     * 
146     * @version 2.0, $Revision: 28915 $, $Date: 2010-12-20 17:01:40 +0100 (Mo, 20 Dez 2010) $
147     * 
148     * @since 2.0
149     */
150    public class RasterTreeBuilderIndexed {
151    
152        private static final ILogger LOG = LoggerFactory.getLogger( RasterTreeBuilderIndexed.class );
153    
154        private static final URI DEEGREEAPP = CommonNamespaces.buildNSURI( "http://www.deegree.org/app" );
155    
156        private static final String APP_PREFIX = "app";
157    
158        // templates and transformation scripts
159        private URL configURL = RasterTreeBuilder.class.getResource( "template_wcs_configuration.xml" );
160    
161        private URL configXSL = RasterTreeBuilder.class.getResource( "updateConfig.xsl" );
162    
163        private URL inputXSL = RasterTreeBuilder.class.getResource( "updateCapabilities.xsl" );
164    
165        private int bitDepth = 16;
166    
167        // input for new MergeRaste object
168        private List<String> imageFiles;
169    
170        private List<WorldFile> imageFilesEnvs;
171    
172        private Map<String, String> imageFilesErrors;
173    
174        private String outputDir;
175    
176        private String baseName;
177    
178        private String outputFormat;
179    
180        private double maxTileSize;
181    
182        private String srs = null;
183    
184        private Object interpolation = null;
185    
186        private WorldFile.TYPE worldFileType = null;
187    
188        private float quality = 0;
189    
190        private String bgColor = null;
191    
192        private float offset = 0;
193    
194        private float scaleFactor = 1;
195    
196        // minimum resolution of input images
197        private double minimumRes;
198    
199        // combining image bounding box
200        private Envelope combiningEnvelope;
201    
202        // size of virtual bounding box in px
203        private double pxWidthVirtualBBox;
204    
205        private double pxHeightVirtualBBox;
206    
207        // size of every tile in virtual bounding box in px
208        private long pxWidthTile;
209    
210        private long pxHeightTile;
211    
212        // number of tiles in virtual bounding box
213        private int tileRows;
214    
215        private int tileCols;
216    
217        private FeatureType ftype = null;
218    
219        private FeatureCollection fc = null;
220    
221        private Cache imgCache;
222    
223        private boolean dummy;
224    
225        private RTree rTree;
226    
227        /**
228         * @param imageFiles
229         * @param outputDir
230         * @param baseName
231         * @param outputFormat
232         * @param maxTileSize
233         * @param srs
234         * @param interpolation
235         * @param worldFileType
236         * @param quality
237         * @param bgColor
238         * @param depth
239         * @param resolution
240         * @param offset
241         * @param scaleFactor
242         * @param dummy
243         */
244        public RasterTreeBuilderIndexed( List<String> imageFiles, String outputDir, String baseName, String outputFormat,
245                                         double maxTileSize, String srs, String interpolation,
246                                         WorldFile.TYPE worldFileType, float quality, String bgColor, int depth,
247                                         double resolution, float offset, float scaleFactor, boolean dummy ) {
248            this( imageFiles, outputDir, baseName, outputFormat, maxTileSize, srs, interpolation, worldFileType, quality,
249                  bgColor, depth, resolution, offset, scaleFactor );
250            this.dummy = dummy;
251        }
252    
253        /**
254         * 
255         * @param imageFiles
256         * @param outputDir
257         * @param baseName
258         * @param outputFormat
259         * @param maxTileSize
260         * @param srs
261         * @param interpolation
262         * @param worldFileType
263         * @param quality
264         * @param bgColor
265         * @param depth
266         * @param resolution
267         * @param offset
268         * @param scaleFactor
269         */
270        public RasterTreeBuilderIndexed( List<String> imageFiles, String outputDir, String baseName, String outputFormat,
271                                         double maxTileSize, String srs, String interpolation,
272                                         WorldFile.TYPE worldFileType, float quality, String bgColor, int depth,
273                                         double resolution, float offset, float scaleFactor ) {
274    
275            this.imageFiles = imageFiles;
276            this.imageFilesErrors = new HashMap<String, String>( imageFiles.size() );
277            this.imageFilesEnvs = new ArrayList<WorldFile>( imageFiles.size() );
278            for ( int i = 0; i < imageFiles.size(); i++ ) {
279                this.imageFilesEnvs.add( null );
280            }
281            this.outputDir = outputDir;
282            File dir = new File( outputDir ).getAbsoluteFile();
283            if ( !dir.exists() ) {
284                dir.mkdir();
285            }
286            this.baseName = baseName;
287            this.outputFormat = outputFormat.toLowerCase();
288            this.maxTileSize = maxTileSize;
289            this.srs = srs;
290            this.interpolation = createInterpolation( interpolation );
291            this.worldFileType = worldFileType;
292            this.quality = quality;
293            this.bgColor = bgColor;
294            if ( depth != 0 ) {
295                this.bitDepth = depth;
296            }
297            this.minimumRes = resolution;
298            this.offset = offset;
299            this.scaleFactor = scaleFactor;
300    
301            CacheManager singletonManager = CacheManager.create();
302            if ( singletonManager.getCache( "imgCache" ) == null ) {
303                Cache cache = new Cache( "imgCache", 10, MemoryStoreEvictionPolicy.LFU, false, ".", false, 3600, 3600,
304                                         false, 240, null );
305                singletonManager.addCache( cache );
306                imgCache = singletonManager.getCache( "imgCache" );
307            } else {
308                imgCache = singletonManager.getCache( "imgCache" );
309                try {
310                    imgCache.removeAll();
311                } catch ( IOException e ) {
312                    e.printStackTrace();
313                }
314            }
315    
316            PropertyType[] ftp = new PropertyType[3];
317            ftp[0] = FeatureFactory.createSimplePropertyType( new QualifiedName( "GEOM" ), Types.GEOMETRY, false );
318            ftp[1] = FeatureFactory.createSimplePropertyType(
319                                                              new QualifiedName( GridCoverageExchange.SHAPE_IMAGE_FILENAME ),
320                                                              Types.VARCHAR, false );
321            ftp[2] = FeatureFactory.createSimplePropertyType( new QualifiedName( GridCoverageExchange.SHAPE_DIR_NAME ),
322                                                              Types.VARCHAR, false );
323            ftype = FeatureFactory.createFeatureType( new QualifiedName( "tiles" ), false, ftp );
324        }
325    
326        /**
327         * @throws IOException
328         */
329        public void logCollectedErrors()
330                                throws IOException {
331            FileOutputStream fos = new FileOutputStream( "RasterTreeBuilder" + minimumRes + ".log" );
332            PrintWriter pw = new PrintWriter( fos );
333            pw.println( "processing the following files caused an error" );
334            Iterator<String> iter = imageFilesErrors.keySet().iterator();
335            while ( iter.hasNext() ) {
336                String key = iter.next();
337                String value = imageFilesErrors.get( key );
338                pw.print( key );
339                pw.print( ": " );
340                pw.println( value );
341            }
342            pw.close();
343            LOG.logInfo( "LOG file RasterTreeBuilder.log has been written" );
344        }
345    
346        /**
347         * starts creating of a raster tile level using the current bbox and resolution
348         * 
349         * @throws Exception
350         */
351        public void start()
352                                throws Exception {
353            System.gc();
354            fc = FeatureFactory.createFeatureCollection( Double.toString( minimumRes ), tileRows * tileCols );
355            createTiles( tileRows, tileCols );
356    
357            LOG.logInfo( "creating shape for georeferencing ... " );
358            ShapeFile sf = new ShapeFile( outputDir + "/sh" + minimumRes, "rw" );
359            sf.writeShape( fc );
360            sf.close();
361    
362        }
363    
364        /**
365         * @param env
366         * @param resolution
367         * @throws IndexException
368         */
369        public void init( Envelope env, double resolution )
370                                throws Exception {
371    
372            rTree = new RTree( 2, imageFiles.size() );
373            for ( int i = 0; i < imageFiles.size(); i++ ) {
374    
375                File file = new File( imageFiles.get( i ) );
376                if ( file.exists() && !file.isDirectory() ) {
377                    FileSeekableStream fss = new FileSeekableStream( imageFiles.get( i ) );
378                    RenderedOp rop = JAI.create( "stream", fss );
379                    int iw = ( (Integer) rop.getProperty( "image_width" ) ).intValue();
380                    int ih = ( (Integer) rop.getProperty( "image_height" ) ).intValue();
381                    fss.close();
382    
383                    WorldFile wf = null;
384                    try {
385                        wf = WorldFile.readWorldFile( imageFiles.get( i ), worldFileType, iw, ih );
386                    } catch ( Exception e ) {
387                        LOG.logError( e.getMessage() );
388                        continue;
389                    }
390                    imageFilesEnvs.set( i, wf );
391                }
392            }
393    
394            for ( int i = 0; i < imageFiles.size(); i++ ) {
395                if ( imageFilesEnvs.get( i ) != null ) {
396                    Envelope envelope = imageFilesEnvs.get( i ).getEnvelope();
397                    Position pp = envelope.getMin();
398                    HyperPoint min = new HyperPoint( new double[] { pp.getX(), pp.getY() } );
399                    pp = envelope.getMax();
400                    HyperPoint max = new HyperPoint( new double[] { pp.getX(), pp.getY() } );
401                    HyperBoundingBox hbb = new HyperBoundingBox( min, max );
402                    rTree.insert( i, hbb );
403                }
404            }
405    
406            // set target envelope
407            setEnvelope( env );
408            setResolution( resolution );
409            determineVirtualBBox();
410            determineTileSize();
411        }
412    
413        /**
414         * sets the resolution level to be used for tiling
415         * 
416         * @param resolution
417         */
418        public void setResolution( double resolution ) {
419            minimumRes = resolution;
420        }
421    
422        /**
423         * sets the bounding box used for tiling
424         * 
425         * @param bbox
426         */
427        public void setEnvelope( Envelope bbox ) {
428            combiningEnvelope = bbox;
429        }
430    
431        /**
432         * TODO this is a copy from org.deegree.tools.raster#AutoTiler
433         * 
434         * loads the base image
435         * 
436         * @throws IOException
437         */
438        private RenderedOp loadImage( String imageSource )
439                                throws IOException {
440    
441            File f = new File( imageSource );
442            InputStream is = f.toURL().openStream();
443            SeekableStream fss = new MemoryCacheSeekableStream( is );
444    
445            return JAI.create( "stream", fss );
446        }
447    
448        /**
449         * Determins the necessary size of a bounding box, which is large enough to hold all input image files. The result
450         * is stored in the combining <code>Envelope</code>.
451         * 
452         * @throws Exception
453         */
454        private WorldFile determineCombiningBBox()
455                                throws Exception {
456    
457            System.out.println( "calculating overall bounding box ..." );
458    
459            if ( imageFiles == null || imageFiles.isEmpty() ) {
460                throw new Exception( "No combining BoundingBox to be determined: "
461                                     + "The list of image files is null or empty." );
462            }
463    
464            WorldFile wf1 = null;
465            if ( combiningEnvelope == null ) {
466    
467                // upper left corner of combining bounding box
468                double minX = Double.MAX_VALUE;
469                double maxY = Double.MIN_VALUE;
470                // lower right corner of combining bounding box
471                double maxX = Double.MIN_VALUE;
472                double minY = Double.MAX_VALUE;
473                // minimum resolution within combining bounding box
474                double minResX = Double.MAX_VALUE;
475                double minResY = Double.MAX_VALUE;
476    
477                for ( int i = 0; i < imageFiles.size(); i++ ) {
478    
479                    File file = new File( imageFiles.get( i ) );
480                    if ( file.exists() && !file.isDirectory() ) {
481                        System.out.println( imageFiles.get( i ) );
482                        FileSeekableStream fss = new FileSeekableStream( imageFiles.get( i ) );
483                        RenderedOp rop = JAI.create( "stream", fss );
484                        int iw = ( (Integer) rop.getProperty( "image_width" ) ).intValue();
485                        int ih = ( (Integer) rop.getProperty( "image_height" ) ).intValue();
486                        fss.close();
487    
488                        WorldFile wf = null;
489    
490                        try {
491                            wf = WorldFile.readWorldFile( imageFiles.get( i ), worldFileType, iw, ih );
492                        } catch ( Exception e ) {
493                            LOG.logError( e.getMessage() );
494                            continue;
495                        }
496                        imageFilesEnvs.set( i, wf );
497                        // now the values of resx, resy, envelope of the current image
498                        // (read from the world file) file are available
499    
500                        // find min for x and y
501                        minX = Math.min( minX, wf.getEnvelope().getMin().getX() );
502                        minY = Math.min( minY, wf.getEnvelope().getMin().getY() );
503                        // find max for x and y
504                        maxX = Math.max( maxX, wf.getEnvelope().getMax().getX() );
505                        maxY = Math.max( maxY, wf.getEnvelope().getMax().getY() );
506    
507                        // find min for resolution of x and y
508                        minResX = Math.min( minResX, wf.getResx() );
509                        minResY = Math.min( minResY, wf.getResy() );
510                    } else {
511                        System.out.println( "File: " + imageFiles.get( i ) + " does not exist!" );
512                        System.out.println( "Image will be ignored" );
513                    }
514                    if ( i % 10 == 0 ) {
515                        System.gc();
516                    }
517    
518                }
519                // store minimum resolution
520                if ( minimumRes <= 0 ) {
521                    minimumRes = Math.min( minResX, minResY );
522                }
523                combiningEnvelope = GeometryFactory.createEnvelope( minX, minY, maxX, maxY, null );
524                LOG.logInfo( "determined envelope: ", combiningEnvelope );
525            }
526            wf1 = new WorldFile( minimumRes, minimumRes, 0, 0, combiningEnvelope );
527            return wf1;
528        }
529    
530        /**
531         * Determins a usefull size for the virtual bounding box. It is somewhat larger than the combining bounding box. The
532         * result is stored in the virtual <code>Envelope</code>.
533         * 
534         */
535        private Envelope determineVirtualBBox() {
536    
537            double width = combiningEnvelope.getWidth();
538            double height = combiningEnvelope.getHeight();
539    
540            // set width and height to next higher even-numbered thousand
541            double pxWidth = ( width / minimumRes ) + 1;
542            double pxHeight = ( height / minimumRes ) + 1;
543    
544            pxWidthVirtualBBox = pxWidth;
545            pxHeightVirtualBBox = pxHeight;
546    
547            // lower right corner of virtual bounding box
548    
549            WorldFile wf = new WorldFile( minimumRes, minimumRes, 0, 0, combiningEnvelope );
550            // upper left corner of virtual bounding box
551            double minX = combiningEnvelope.getMin().getX();
552            double maxY = combiningEnvelope.getMax().getY();
553    
554            double maxX = minX + ( ( pxWidth - 1 ) * wf.getResx() );
555            double minY = maxY - ( ( pxHeight - 1 ) * wf.getResx() );
556    
557            return GeometryFactory.createEnvelope( minX, minY, maxX, maxY, null );
558    
559            // return combiningEnvelope;
560        }
561    
562        /**
563         * This method determins and sets the size of the tiles in pixel both horizontally (pxWidthTile) and vertically
564         * (pxHeightTile). It also sets the necessary number of <code>tileCols</code> (depending on the tileWidth) and
565         * <code>tileRows</code> (depending on the tileHeight).
566         * 
567         * By default, all tiles have a size of close to but less than 6000 pixel either way.
568         */
569        private void determineTileSize() {
570            /*
571             * The size of the virtual bbox gets divided by maxTileSize to find an approximat number of tiles (a).
572             * 
573             * If the virtual bbox is in any direction (horizontally or vertically) smaler than maxTileSize px, then it has
574             * only 1 tile in that direction. In this case, the size of the tile equals the size of the virtual bbox.
575             * 
576             * Otherwise, divide the size of the pixel size of virtual bbox by the pixel tile size
577             */
578            // determin width of tile
579            double a = ( pxWidthVirtualBBox / maxTileSize );
580            int tileCols = (int) Math.ceil( a );
581            if ( a <= 1.0 ) {
582                pxWidthTile = Math.round( pxWidthVirtualBBox );
583            } else {
584                tileCols = (int) Math.round( ( pxWidthVirtualBBox / ( maxTileSize - 1 ) ) + 1 );
585                pxWidthTile = (int) Math.round( maxTileSize );
586            }
587    
588            // determin height of tile
589            a = ( pxHeightVirtualBBox / maxTileSize );
590            int tileRows = (int) Math.ceil( a );
591            if ( a <= 1.0 ) {
592                pxHeightTile = Math.round( pxHeightVirtualBBox );
593            } else {
594                tileRows = (int) Math.round( ( pxHeightVirtualBBox / ( maxTileSize - 1 ) ) + 1 );
595                pxHeightTile = (int) Math.round( maxTileSize );
596            }
597    
598            this.tileCols = tileCols;
599            this.tileRows = tileRows;
600    
601            LOG.logInfo( "minimum resolution: " + minimumRes );
602            LOG.logInfo( "width = " + pxWidthVirtualBBox + " *** height = " + pxHeightVirtualBBox );
603            LOG.logInfo( "pxWidthTile = " + pxWidthTile + " *** pxHeightTile = " + pxHeightTile );
604            LOG.logInfo( "number of tiles: horizontally = " + tileCols + ", vertically = " + tileRows );
605        }
606    
607        /**
608         * Creates one <code>Tile</code> object after the other, with the number of tiles being specified by the given
609         * number of <code>rows</code> and <code>cols</code>.
610         * 
611         * Each Tile gets written to the FileOutputStream by the internal call to #paintImagesOnTile.
612         * 
613         * @param rows
614         * @param cols
615         * @throws Exception
616         */
617        private void createTiles( int rows, int cols )
618                                throws Exception {
619    
620            System.out.println( "creating merged image ..." );
621    
622            Envelope virtualEnv = determineVirtualBBox();
623    
624            double tileWidth = minimumRes * ( pxWidthTile );
625            double tileHeight = minimumRes * ( pxHeightTile );
626    
627            double upperY = virtualEnv.getMax().getY();
628    
629            File file = new File( outputDir + '/' + Double.toString( minimumRes ) ).getAbsoluteFile();
630            file.mkdir();
631    
632            for ( int i = 0; i < rows; i++ ) {
633                System.out.println( "processing row " + i );
634                double leftX = virtualEnv.getMin().getX();
635                double lowerY = upperY - tileHeight;
636                for ( int j = 0; j < cols; j++ ) {
637                    System.out.println( "processing tile: " + i + " - " + j );
638                    double rightX = leftX + tileWidth;
639                    Envelope env = GeometryFactory.createEnvelope( leftX, lowerY, rightX, upperY, null );
640                    leftX = rightX;
641                    String postfix = "_" + i + "_" + j;
642                    Tile tile = new Tile( env, postfix );
643    
644                    paintImagesOnTile( tile );
645                    System.gc();
646                }
647                upperY = lowerY;
648            }
649            System.gc();
650    
651        }
652    
653        /**
654         * Paints all image files that intersect with the passed <code>tile</code> onto that tile and creates an output file
655         * in the <code>outputDir</code>. If no image file intersects with the given tile, then an empty output file is
656         * created. The name of the output file is defined by the <code>baseName</code> and the tile's index of row and
657         * column.
658         * 
659         * @param tile
660         *            The tile on which to paint the image.
661         * @throws Exception
662         */
663        private void paintImagesOnTile( Tile tile )
664                                throws Exception {
665    
666            Envelope tileEnv = tile.getTileEnvelope();
667            String postfix = tile.getPostfix();
668    
669            BufferedImage out = createOutputImage();
670            float[][] data = null;
671            if ( bitDepth == 16 && "raw".equals( outputFormat ) ) {
672                // do not use image api if target bitDepth = 16
673                data = new float[(int) pxHeightTile][(int) pxWidthTile];
674            }
675    
676            if ( bgColor != null ) {
677                Graphics g = out.getGraphics();
678                g.setColor( Color.decode( bgColor ) );
679                g.fillRect( 0, 0, out.getWidth(), out.getHeight() );
680                g.dispose();
681            }
682            boolean paint = false;
683            int gcc = 0;
684    
685            if ( dummy ) {
686                paint = true;
687            } else {
688    
689                Position p = tileEnv.getMin();
690                HyperPoint min = new HyperPoint( new double[] { p.getX(), p.getY() } );
691                p = tileEnv.getMax();
692                HyperPoint max = new HyperPoint( new double[] { p.getX(), p.getY() } );
693                HyperBoundingBox hbb = new HyperBoundingBox( min, max );
694                Object[] obj = rTree.intersects( hbb );
695                for ( int i = 0; i < obj.length; i++ ) {
696                    File file = new File( imageFiles.get( ( (Integer) obj[i] ).intValue() ) );
697                    if ( imageFilesErrors.get( imageFiles.get( ( (Integer) obj[i] ).intValue() ) ) == null && file.exists()
698                         && !file.isDirectory() ) {
699                        // now the values of resx, resy, envelope of the current image file are available
700                        RenderedOp result = loadImage( imageFiles.get( ( (Integer) obj[i] ).intValue() ) );
701    
702                        gcc++;
703                        try {
704                            System.out.println( "drawImage" );
705                            drawImage( out, data, result, tile, imageFilesEnvs.get( ( (Integer) obj[i] ).intValue() ),
706                                       minimumRes, interpolation, imageFilesErrors, outputFormat, bitDepth, offset,
707                                       scaleFactor );
708                            paint = true;
709                        } catch ( Exception e ) {
710                            e.printStackTrace();
711                            imageFilesErrors.put( imageFiles.get( i ), e.getMessage() );
712                        }
713                        if ( gcc % 5 == 0 ) {
714                            System.out.println( "garbage collecting" );
715                            System.gc();
716                        }
717                    } else {
718                        imageFilesErrors.put( imageFiles.get( ( (Integer) obj[i] ).intValue() ), "image does not exist!" );
719                    }
720                }
721            }
722            if ( paint ) {
723                if ( !isTransparent( out ) ) {
724                    // just write files if something has been painted
725                    if ( bitDepth == 16 && "raw".equals( outputFormat ) ) {
726                        out = RawData2Image.rawData2Image( data, false, scaleFactor, offset );
727                    }
728                    storeTileImageToFileSystem( postfix, out );
729                    createWorldFile( tile );
730                    String frm = outputFormat;
731                    if ( "raw".equals( outputFormat ) ) {
732                        frm = "tif";
733                    }
734                    storeEnvelope( Double.toString( minimumRes ), baseName + postfix + '.' + frm, tileEnv );
735                }
736            }
737        }
738    
739        /**
740         * creates an instance of a BufferedImage depending on requested target format
741         * 
742         * @return the new image
743         */
744        private BufferedImage createOutputImage() {
745    
746            BufferedImage out = null;
747            if ( "jpg".equals( outputFormat ) || "jpeg".equals( outputFormat ) || "bmp".equals( outputFormat ) ) {
748                // for bmp, jpg, jpeg use 3 byte:
749                out = new BufferedImage( (int) pxWidthTile, (int) pxHeightTile, BufferedImage.TYPE_INT_RGB );
750            } else if ( "tif".equals( outputFormat ) || "tiff".equals( outputFormat ) || "png".equals( outputFormat )
751                        || "gif".equals( outputFormat ) ) {
752                // for tif, tiff and png use 4 byte:
753                out = new BufferedImage( (int) pxWidthTile, (int) pxHeightTile, BufferedImage.TYPE_INT_ARGB );
754            } else {
755                ColorModel ccm;
756    
757                if ( bitDepth == 16 ) {
758                    ccm = new ComponentColorModel( ColorSpace.getInstance( ColorSpace.CS_GRAY ), null, false, false,
759                                                   BufferedImage.OPAQUE, DataBuffer.TYPE_USHORT );
760                    WritableRaster wr = ccm.createCompatibleWritableRaster( (int) pxWidthTile, (int) pxHeightTile );
761    
762                    out = new BufferedImage( ccm, wr, false, new Hashtable<Object, Object>() );
763                } else {
764                    out = new BufferedImage( (int) pxWidthTile, (int) pxHeightTile, BufferedImage.TYPE_INT_ARGB );
765                }
766            }
767    
768            return out;
769    
770        }
771    
772        /**
773         * 
774         * @param postfix
775         *            tile name postfix ( -> tile index $x_$y )
776         * @param out
777         *            tile image to save
778         */
779        private void storeTileImageToFileSystem( String postfix, BufferedImage out ) {
780            try {
781                String frm = outputFormat;
782                if ( "raw".equals( frm ) ) {
783                    frm = "tif";
784                }
785                String imageFile = outputDir + '/' + Double.toString( minimumRes ) + '/' + baseName + postfix + '.' + frm;
786                File file = new File( imageFile ).getAbsoluteFile();
787    
788                ImageUtils.saveImage( out, file, quality );
789    
790            } catch ( IOException e ) {
791                e.printStackTrace();
792            }
793        }
794    
795        /**
796         * Draws an image map to the target tile considering defined interpolation method for rescaling. This method is
797         * static so it can be used easily from the <code>RasterTreeUpdater</code>.
798         * 
799         * @param out
800         *            target image tile
801         * @param data
802         * @param image
803         *            source image map
804         * @param tile
805         *            tile description, must contain the envelope of the target image
806         * @param wf
807         *            must contain the envelope of the TiledImage of the source image
808         * @param minimumRes
809         *            the minimum resolution of input images
810         * @param interpolation
811         *            the interpolation method
812         * @param imageFilesErrors
813         *            a mapping between image files and errors
814         * @param outputFormat
815         *            the output format
816         * @param bitDepth
817         *            the output bit depth
818         * @param offset
819         *            offset used if bitDepth = 16 and outputFormat = raw
820         * @param scaleFactor
821         *            scale factor used if bitDepth = 16 and outputFormat = raw
822         */
823        public static void drawImage( BufferedImage out, float[][] data, final RenderedOp image, Tile tile, WorldFile wf,
824                                      double minimumRes, Object interpolation, Map<String, String> imageFilesErrors,
825                                      String outputFormat, int bitDepth, float offset, float scaleFactor ) {
826    
827            Envelope tileEnv = tile.getTileEnvelope();
828            Envelope mapEnv = wf.getEnvelope();
829    
830            GeoTransform gt2 = new WorldToScreenTransform( mapEnv.getMin().getX(), mapEnv.getMin().getY(),
831                                                           mapEnv.getMax().getX(), mapEnv.getMax().getY(), 0, 0,
832                                                           image.getWidth() - 1, image.getHeight() - 1 );
833    
834            Envelope inter = mapEnv.createIntersection( tileEnv );
835            if ( inter == null )
836                return;
837            int x1 = (int) Math.round( gt2.getDestX( inter.getMin().getX() ) );
838            int y1 = (int) Math.round( gt2.getDestY( inter.getMax().getY() ) );
839            int x2 = (int) Math.round( gt2.getDestX( inter.getMax().getX() ) );
840            int y2 = (int) Math.round( gt2.getDestY( inter.getMin().getY() ) );
841    
842            if ( x2 - x1 >= 0 && y2 - y1 >= 0 && x1 + x2 - x1 < image.getWidth() && y1 + y2 - y1 < image.getHeight()
843                 && x1 >= 0 && y1 >= 0 ) {
844    
845                BufferedImage newImg = null;
846                int w = x2 - x1 + 1;
847                int h = y2 - y1 + 1;
848                // System.out.println( x1 + " " + y1 + " " + w + " " + h + " " + image.getWidth() + " "
849                // + image.getHeight() );
850                WritableRaster jpgRaster = image.getColorModel().createCompatibleWritableRaster( w, h ).createWritableTranslatedChild(
851                                                                                                                                       x1,
852                                                                                                                                       y2 );
853                image.copyExtendedData( jpgRaster, BorderExtender.createInstance( BorderExtender.BORDER_ZERO ) );
854                BufferedImage img = new BufferedImage( image.getColorModel(), jpgRaster.getWritableParent(), false, null );
855    
856                if ( !isTransparent( img ) ) {
857    
858                    // copy source image to a 4 Byte BufferedImage because there are
859                    // problems with handling 8 Bit palette images
860                    if ( img.getColorModel().getPixelSize() == 18 ) {
861                        LOG.logInfo( "copy 8Bit image to 32Bit image" );
862                        BufferedImage bi = new BufferedImage( img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB );
863    
864                        Graphics g = bi.getGraphics();
865                        try {
866                            g.drawImage( img, 0, 0, null );
867                        } catch ( Exception e ) {
868                            System.out.println( e.getMessage() );
869                        }
870                        g.dispose();
871                        img = bi;
872                    }
873                    if ( ( wf.getResx() / minimumRes < 0.9999 ) || ( wf.getResx() / minimumRes > 1.0001 )
874                         || ( wf.getResy() / minimumRes < 0.9999 ) || ( wf.getResy() / minimumRes > 1.0001 ) ) {
875                        double dx = wf.getResx() / minimumRes;
876                        double dy = wf.getResy() / minimumRes;
877                        int destWidth = (int) Math.round( img.getWidth() * dx );
878                        int destHeight = (int) Math.round( img.getHeight() * dy );
879                        newImg = resize( img, destWidth, destHeight, interpolation );
880                    } else {
881                        newImg = img;
882                    }
883                    GeoTransform gt = new WorldToScreenTransform( tileEnv.getMin().getX(), tileEnv.getMin().getY(),
884                                                                  tileEnv.getMax().getX(), tileEnv.getMax().getY(), 0, 0,
885                                                                  out.getWidth() - 1, out.getHeight() - 1 );
886    
887                    x1 = (int) Math.round( gt.getDestX( inter.getMin().getX() ) );
888                    y1 = (int) Math.round( gt.getDestY( inter.getMax().getY() ) );
889                    x2 = (int) Math.round( gt.getDestX( inter.getMax().getX() ) );
890                    y2 = (int) Math.round( gt.getDestY( inter.getMin().getY() ) );
891    
892                    if ( x2 - x1 > 0 && y2 - y1 > 0 ) {
893                        // ensure that there is something to draw
894                        try {
895                            if ( "raw".equals( outputFormat ) ) {
896                                DataBuffer outBuffer = out.getData().getDataBuffer();
897                                DataBuffer newImgBuffer = newImg.getData().getDataBuffer();
898                                int ps = newImg.getColorModel().getPixelSize();
899                                float[][] newData = null;
900                                if ( bitDepth == 16 && ps == 16 ) {
901                                    Image2RawData i2r = new Image2RawData( newImg, 1f / scaleFactor, -1 * offset );
902                                    // do not use image api if target bitDepth = 16
903                                    newData = i2r.parse();
904                                }
905                                for ( int i = 0; i < newImg.getWidth(); i++ ) {
906                                    for ( int j = 0; j < newImg.getHeight(); j++ ) {
907                                        if ( x1 + i < out.getWidth() && y1 + j < out.getHeight() ) {
908                                            int newImgPos = newImg.getWidth() * j + i;
909                                            int outPos = out.getWidth() * ( y1 + j ) + ( x1 + i );
910                                            if ( bitDepth == 16 && ps == 16 ) {
911                                                // int v = newImgBuffer.getElem( newImgPos );
912                                                // outBuffer.setElem( outPos, v );
913                                                data[y1 + j][x1 + i] = newData[j][i];
914                                            } else if ( bitDepth == 16 && ps == 32 ) {
915                                                int v = newImg.getRGB( i, j );
916                                                float f = Float.intBitsToFloat( v ) * 10f;
917                                                outBuffer.setElem( outPos, Math.round( f ) );
918                                                // TODO
919                                                // data[y1 + j][x1 + i] = f;
920                                            } else if ( bitDepth == 32 && ps == 16 ) {
921                                                float f = newImgBuffer.getElem( newImgPos ) / 10f;
922                                                outBuffer.setElem( outPos, Float.floatToIntBits( f ) );
923                                            } else {
924                                                out.setRGB( x1 + i, y1 + j, newImg.getRGB( i, j ) );
925                                            }
926                                        }
927                                    }
928                                }
929                                if ( ( bitDepth == 16 && ps == 16 ) || ( bitDepth == 16 && ps == 32 )
930                                     || ( bitDepth == 32 && ps == 16 ) ) {
931                                    out.setData( Raster.createRaster( out.getSampleModel(), outBuffer, null ) );
932                                }
933                            } else {
934                                Graphics g = out.getGraphics();
935                                g.drawImage( newImg, x1, y1, newImg.getWidth(), newImg.getHeight(), null );
936                                g.dispose();
937                            }
938                        } catch ( Exception e ) {
939                            LOG.logError( "Could not draw upon the image: " );
940                            LOG.logError( "New image is of size " + newImg.getWidth() + ", " + newImg.getHeight() );
941                            LOG.logError( "Position/width tried is (" + x1 + ", " + y1 + ", " + newImg.getWidth() + ", "
942                                          + newImg.getHeight() + ")" );
943                            if ( imageFilesErrors != null ) {
944                                imageFilesErrors.put( tile.getPostfix(), StringTools.stackTraceToString( e ) );
945                            }
946                        }
947                    }
948                }
949            }
950        }
951    
952        public static BufferedImage resize( BufferedImage source, int destWidth, int destHeight, Object interpolation ) {
953    
954            int sourceWidth = source.getWidth();
955            int sourceHeight = source.getHeight();
956            double xScale = ( (double) destWidth ) / (double) sourceWidth;
957            double yScale = ( (double) destHeight ) / (double) sourceHeight;
958            if ( destWidth <= 0 ) {
959                xScale = yScale;
960                destWidth = (int) Math.rint( xScale * sourceWidth );
961            }
962            if ( destHeight <= 0 ) {
963                yScale = xScale;
964                destHeight = (int) Math.rint( yScale * sourceHeight );
965            }
966            WritableRaster ra = source.getRaster().createCompatibleWritableRaster( destWidth, destHeight );
967            BufferedImage result = new BufferedImage( source.getColorModel(), ra, source.isAlphaPremultiplied(), null );
968            Graphics2D g2d = null;
969            try {
970                g2d = result.createGraphics();
971                g2d.setRenderingHint( RenderingHints.KEY_INTERPOLATION, interpolation );
972                AffineTransform at = AffineTransform.getScaleInstance( xScale, yScale );
973                g2d.drawRenderedImage( source, at );
974            } finally {
975                if ( g2d != null )
976                    g2d.dispose();
977            }
978            return result;
979        }
980    
981        private static boolean isTransparent( BufferedImage bi ) {
982            /*
983             * TODO determine if the passed image is completly transparent for ( int i = 0; i < bi.getHeight(); i++ ) { for
984             * ( int j = 0; j < bi.getWidth(); j++ ) { if ( bi.getRGB( i, j ) != 0 && bi.getRGB( i, j ) != -256 ) { return
985             * false; } } } return true;
986             */
987            return false;
988        }
989    
990        /**
991         * @return an interpolation object from a well known name
992         * @param interpolation
993         */
994        public static Object createInterpolation( String interpolation ) {
995            Object interpol = null;
996    
997            if ( interpolation.equalsIgnoreCase( "Nearest Neighbor" ) ) {
998                interpol = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
999            } else if ( interpolation.equalsIgnoreCase( "Bicubic" ) ) {
1000                interpol = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
1001            } else if ( interpolation.equalsIgnoreCase( "Bicubic2" ) ) {
1002                // for downward compability
1003                interpol = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
1004            } else if ( interpolation.equalsIgnoreCase( "Bilinear" ) ) {
1005                interpol = RenderingHints.VALUE_INTERPOLATION_BILINEAR;
1006            } else {
1007                throw new RuntimeException( "invalid interpolation method: " + interpolation );
1008            }
1009    
1010            return interpol;
1011        }
1012    
1013        /**
1014         * Creates a world file for the corresponding tile in the <code>outputDir</code>. The name of the output file is
1015         * defined by the <code>baseName</code> and the tile's index of row and column.
1016         * 
1017         * @param tile
1018         *            The tile for which to create a world file.
1019         * @throws IOException
1020         */
1021        private void createWorldFile( Tile tile )
1022                                throws IOException {
1023    
1024            Envelope env = tile.getTileEnvelope();
1025            String postfix = tile.getPostfix();
1026    
1027            StringBuffer sb = new StringBuffer( 1000 );
1028    
1029            sb.append( minimumRes ).append( "\n" ).append( 0.0 ).append( "\n" ).append( 0.0 );
1030            sb.append( "\n" ).append( ( -1 ) * minimumRes ).append( "\n" );
1031            sb.append( env.getMin().getX() ).append( "\n" ).append( env.getMax().getY() );
1032            sb.append( "\n" );
1033    
1034            File f = new File( outputDir + '/' + Double.toString( minimumRes ) + '/' + baseName + postfix + ".wld" );
1035    
1036            FileWriter fw = new FileWriter( f );
1037            PrintWriter pw = new PrintWriter( fw );
1038    
1039            pw.print( sb.toString() );
1040    
1041            pw.close();
1042            fw.close();
1043        }
1044    
1045        /**
1046         * stores an envelope and the assigend image file information into a feature/featureCollection
1047         * 
1048         * @param dir
1049         *            directory where the image file is stored
1050         * @param file
1051         *            name of the image file
1052         * @param env
1053         *            bbox of the image file
1054         */
1055        private void storeEnvelope( String dir, String file, Envelope env ) {
1056            try {
1057                Geometry geom = GeometryFactory.createSurface( env, null );
1058                FeatureProperty[] props = new FeatureProperty[3];
1059                props[0] = FeatureFactory.createFeatureProperty( new QualifiedName( "GEOM" ), geom );
1060                props[1] = FeatureFactory.createFeatureProperty(
1061                                                                 new QualifiedName(
1062                                                                                    GridCoverageExchange.SHAPE_IMAGE_FILENAME ),
1063                                                                 file );
1064                props[2] = FeatureFactory.createFeatureProperty( new QualifiedName( GridCoverageExchange.SHAPE_DIR_NAME ),
1065                                                                 dir );
1066                Feature feat = FeatureFactory.createFeature( "file", ftype, props );
1067                fc.add( feat );
1068            } catch ( Exception e ) {
1069                e.printStackTrace();
1070            }
1071        }
1072    
1073        /**
1074         * creates a configuration file (extended CoverageDescriotion) for a WCS coverage considering the passed resolution
1075         * levels
1076         * 
1077         * @param targetResolutions
1078         */
1079        private void createConfigurationFile( double[] targetResolutions ) {
1080    
1081            // copy this file to the target directory
1082            String resolutions = "";
1083            sort( targetResolutions );
1084            int length = targetResolutions.length;
1085    
1086            for ( int i = 0; i < length; i++ ) {
1087                resolutions += String.valueOf( targetResolutions[length - 1 - i] );
1088                if ( i < ( length - 1 ) )
1089                    resolutions += ',';
1090            }
1091    
1092            try {
1093                Map<String, String> param = new HashMap<String, String>( 20 );
1094                Envelope llEnv = getLatLonEnvelope( combiningEnvelope );
1095                param.put( "upperleftll", String.valueOf( llEnv.getMin().getX() ) + ','
1096                                          + String.valueOf( llEnv.getMin().getY() ) );
1097                param.put( "lowerrightll", String.valueOf( llEnv.getMax().getX() ) + ','
1098                                           + String.valueOf( llEnv.getMax().getY() ) );
1099                param.put( "upperleft", String.valueOf( combiningEnvelope.getMin().getX() ) + ','
1100                                        + String.valueOf( combiningEnvelope.getMin().getY() ) );
1101                param.put( "lowerright", String.valueOf( combiningEnvelope.getMax().getX() ) + ','
1102                                         + combiningEnvelope.getMax().getY() );
1103                File dir = new File( outputDir );
1104                if ( dir.isAbsolute() ) {
1105                    // param.put( "dataDir", outputDir + '/' );
1106                    param.put( "dataDir", "" );
1107                } else {
1108                    param.put( "dataDir", "" );
1109                }
1110                param.put( "label", baseName );
1111                param.put( "name", baseName );
1112                param.put( "description", "" );
1113                param.put( "keywords", "" );
1114                param.put( "resolutions", resolutions );
1115                String frm = outputFormat;
1116                if ( "raw".equals( outputFormat ) && bitDepth == 32 ) {
1117                    frm = "tif";
1118                } else if ( "raw".equals( outputFormat ) && bitDepth == 16 ) {
1119                    frm = "GeoTiff";
1120                }
1121                param.put( "mimeType", frm );
1122                int p = srs.lastIndexOf( ':' );
1123                param.put( "srs", srs.substring( p + 1, srs.length() ) );
1124                param.put( "srsPre", srs.substring( 0, p + 1 ) );
1125    
1126                Reader reader = new InputStreamReader( configURL.openStream() );
1127    
1128                XSLTDocument xslt = new XSLTDocument();
1129                xslt.load( configXSL );
1130                XMLFragment xml = xslt.transform( reader, XMLFragment.DEFAULT_URL, null, param );
1131                reader.close();
1132    
1133                // write the result
1134                String dstFilename = "wcs_" + baseName + "_configuration.xml";
1135                File dstFile = new File( outputDir, dstFilename );
1136                String configurationFilename = dstFile.getAbsolutePath().toString();
1137                FileOutputStream fos = new FileOutputStream( configurationFilename );
1138                xml.write( fos );
1139                fos.close();
1140    
1141            } catch ( Exception e1 ) {
1142                e1.printStackTrace();
1143            }
1144    
1145        }
1146    
1147        private Envelope getLatLonEnvelope( Envelope env )
1148                                throws Exception {
1149            GeoTransformer gt = new GeoTransformer( "EPSG:4326" );
1150            return gt.transform( env, srs );
1151        }
1152    
1153        /**
1154         *
1155         */
1156        private void updateCapabilitiesFile( File capabilitiesFile ) {
1157    
1158            try {
1159                XSLTDocument xslt = new XSLTDocument();
1160                xslt.load( inputXSL );
1161                Map<String, String> param = new HashMap<String, String>();
1162    
1163                param.put( "dataDirectory", outputDir );
1164                String url = new File( "wcs_" + baseName + "_configuration.xml" ).toURL().toString();
1165                param.put( "configFile", url );
1166                Envelope llEnv = getLatLonEnvelope( combiningEnvelope );
1167                param.put( "upperleftll", String.valueOf( llEnv.getMin().getX() ) + ','
1168                                          + String.valueOf( llEnv.getMin().getY() ) );
1169                param.put( "lowerrightll", String.valueOf( llEnv.getMax().getX() ) + ','
1170                                           + String.valueOf( llEnv.getMax().getY() ) );
1171    
1172                param.put( "name", baseName );
1173                param.put( "label", baseName );
1174    
1175                param.put( "description", "" );
1176                param.put( "keywords", "" );
1177    
1178                XMLFragment xml = new XMLFragment();
1179                xml.load( capabilitiesFile.toURL() );
1180    
1181                xml = xslt.transform( xml, capabilitiesFile.toURL().toExternalForm(), null, param );
1182    
1183                // write the result
1184                FileOutputStream fos = new FileOutputStream( capabilitiesFile );
1185                xml.write( fos );
1186                fos.close();
1187            } catch ( Exception e ) {
1188                e.printStackTrace();
1189            }
1190        }
1191    
1192        /**
1193         * Validates the content of <code>map</code>, to see, if necessary arguments were passed when calling this class.
1194         * 
1195         * @param map
1196         * @throws Exception
1197         */
1198        private static void validate( Properties map )
1199                                throws Exception {
1200    
1201            if ( map.get( "-outDir" ) == null ) {
1202                throw new Exception( "-outDir must be set" );
1203            }
1204            String s = (String) map.get( "-outDir" );
1205            if ( s.endsWith( "/" ) || s.endsWith( "\\" ) ) {
1206                s = s.substring( 0, s.length() - 1 );
1207            }
1208    
1209            if ( map.get( "-baseName" ) == null ) {
1210                throw new Exception( "-baseName must be set" );
1211            }
1212            if ( map.get( "-outputFormat" ) == null ) {
1213                map.put( "-outputFormat", "png" );
1214            } else {
1215                String format = ( (String) map.get( "-outputFormat" ) ).toLowerCase();
1216                if ( !"bmp".equals( format ) && !"png".equals( format ) && !"jpg".equals( format )
1217                     && !"jpeg".equals( format ) && !"tif".equals( format ) && !"tiff".equals( format )
1218                     && !"gif".equals( format ) && !( "raw" ).equals( format ) ) {
1219    
1220                    throw new Exception( "-outputFormat must be one of the following: "
1221                                         + "'bmp', 'jpeg', 'jpg', 'png', 'tif', 'tiff', 'raw'." );
1222                }
1223            }
1224            if ( map.get( "-maxTileSize" ) == null ) {
1225                map.put( "-maxTileSize", "500" );
1226            }
1227            if ( map.get( "-srs" ) == null ) {
1228                map.put( "-srs", "EPSG:4326" );
1229            }
1230            if ( map.get( "-interpolation" ) == null ) {
1231                map.put( "-interpolation", "Nearest Neighbor" );
1232            }
1233            if ( map.get( "-noOfLevel" ) == null ) {
1234                map.put( "-noOfLevel", "1" );
1235            }
1236            if ( map.get( "-worldFileType" ) == null ) {
1237                map.put( "-worldFileType", "center" );
1238            }
1239            if ( map.get( "-quality" ) == null ) {
1240                map.put( "-quality", "0.95" );
1241            }
1242            if ( map.get( "-bbox" ) != null ) {
1243                double[] d = StringTools.toArrayDouble( (String) map.get( "-bbox" ), "," );
1244                Envelope env = GeometryFactory.createEnvelope( d[0], d[1], d[2], d[3], null );
1245                map.put( "-bbox", env );
1246                if ( map.get( "-resolution" ) == null ) {
1247                    throw new Exception( "-resolution must be set if -bbox is set" );
1248                }
1249                map.put( "-resolution", new Double( (String) map.get( "-resolution" ) ) );
1250            } else {
1251                if ( map.get( "-resolution" ) == null ) {
1252                    map.put( "-resolution", new Double( -1 ) );
1253                } else {
1254                    map.put( "-resolution", new Double( (String) map.get( "-resolution" ) ) );
1255                }
1256            }
1257        }
1258    
1259        /**
1260         * @return the list of image map files to consider read from -mapFiles parameter
1261         * 
1262         * @param mapFiles
1263         */
1264        private static List<String> getFileList( String[] mapFiles ) {
1265            List<String> imageFiles = new ArrayList<String>();
1266            for ( int i = 0; i < mapFiles.length; i++ ) {
1267                imageFiles.add( mapFiles[i] );
1268            }
1269            return imageFiles;
1270        }
1271    
1272        /**
1273         * @return the list of image map files to consider read from a defined root directory.
1274         * 
1275         * @param rootDir
1276         *            root directory where to read image map files
1277         * @param subdirs
1278         *            true if subdirectories of the root directory shall be parsed for image maps too
1279         */
1280        private static List<String> getFileList( String rootDir, boolean subdirs ) {
1281            List<String> list = new ArrayList<String>( 10000 );
1282            File file = new File( rootDir );
1283            String[] entries = file.list( new DFileFilter() );
1284            if ( entries != null ) {
1285                for ( int i = 0; i < entries.length; i++ ) {
1286                    File entry = new File( rootDir + '/' + entries[i] );
1287                    if ( entry.isDirectory() && subdirs ) {
1288                        list = readSubDirs( entry, list );
1289                    } else {
1290                        list.add( rootDir + '/' + entries[i] );
1291                    }
1292                }
1293            }
1294            return list;
1295        }
1296    
1297        /**
1298         * 
1299         * @param file
1300         * @param list
1301         * @return the sub directories
1302         */
1303        private static List<String> readSubDirs( File file, List<String> list ) {
1304    
1305            String[] entries = file.list( new DFileFilter() );
1306            if ( entries != null ) {
1307                for ( int i = 0; i < entries.length; i++ ) {
1308                    File entry = new File( file.getAbsolutePath() + '/' + entries[i] );
1309                    if ( entry.isDirectory() ) {
1310                        list = readSubDirs( entry, list );
1311                    } else {
1312                        list.add( file.getAbsolutePath() + '/' + entries[i] );
1313                    }
1314                }
1315            }
1316            return list;
1317        }
1318    
1319        /**
1320         * @return the list of image map files to consider read from a dbase file defined by the dbase parameter
1321         * 
1322         * @param dbaseFile
1323         *            name of the dbase file
1324         * @param fileColumn
1325         *            name of the column containing the image map files names
1326         * @param baseDir
1327         *            name of the directory where the image map files are stored if this parameter is <code>null</code> it
1328         *            is assumed that the image map files are full referenced within the dbase
1329         * @param sort
1330         *            true if map image file names shall be sorted
1331         * @param sortColum
1332         *            name of the column that shall be used for sorting
1333         */
1334        private static List<String> getFileList( String dBaseFile, String fileColumn, String baseDir, boolean sort,
1335                                                 String sortColum, String sortDirection )
1336                                throws Exception {
1337    
1338            // handle dbase file extension and file location/reading problems
1339            if ( dBaseFile.endsWith( ".dbf" ) ) {
1340                dBaseFile = dBaseFile.substring( 0, dBaseFile.lastIndexOf( "." ) );
1341            }
1342            DBaseFile dbf = new DBaseFile( dBaseFile );
1343    
1344            // sort dbase file contents chronologicaly (oldest first)
1345            int cnt = dbf.getRecordNum();
1346    
1347            Object[][] mapItems = new Object[cnt][2];
1348            QualifiedName fileC = new QualifiedName( APP_PREFIX, fileColumn.toUpperCase(), DEEGREEAPP );
1349            QualifiedName sortC = null;
1350            if ( sort ) {
1351                sortC = new QualifiedName( APP_PREFIX, sortColum.toUpperCase(), DEEGREEAPP );
1352            }
1353            for ( int i = 0; i < cnt; i++ ) {
1354                if ( sort ) {
1355                    mapItems[i][0] = dbf.getFRow( i + 1 ).getDefaultProperty( sortC ).getValue();
1356                } else {
1357                    mapItems[i][0] = new Integer( 1 );
1358                }
1359                // name of map file
1360                mapItems[i][1] = dbf.getFRow( i + 1 ).getDefaultProperty( fileC ).getValue();
1361            }
1362            Arrays.sort( mapItems, new MapAgeComparator( sortDirection ) );
1363    
1364            // extract names of image files from dBase file and attach them to rootDir
1365            if ( baseDir == null ) {
1366                baseDir = "";
1367            } else if ( !baseDir.endsWith( "/" ) && !baseDir.endsWith( "\\" ) ) {
1368                baseDir = baseDir + "/";
1369            }
1370            List<String> imageFiles = new ArrayList<String>( mapItems.length );
1371            for ( int i = 0; i < mapItems.length; i++ ) {
1372                if ( mapItems[i][0] != null ) {
1373                    LOG.logDebug( "" + mapItems[i][0] );
1374                    imageFiles.add( baseDir + mapItems[i][1] );
1375                }
1376            }
1377    
1378            return imageFiles;
1379        }
1380    
1381        private static void printHelp() {
1382    
1383            System.out.println( "-outDir directory where resulting tiles and describing shape(s) will be stored (mandatory)\r\n"
1384                                + "-redirect whether to redirect the standard output/error streams to a file rtb.log in the output directory. Default is false.\r\n"
1385                                + "-baseName base name used for creating names of the raster tile files. It also will be the name of the created coverage. (mandatory)\r\n"
1386                                + "-outputFormat name of the image format used for created tiles (png|jpg|jpeg|bmp|tif|tiff|gif|raw default png)\r\n"
1387                                + "-maxTileSize maximum size of created raster tiles in pixel (default 500)\r\n"
1388                                + "-srs name of the spatial reference system used for the coverage (default EPSG:4326)\r\n"
1389                                + "-interpolation interpolation method used for rescaling raster images (Nearest Neighbor|Bicubic|Bicubic2|Bilinear default Nearest Neighbor)\r\n"
1390                                + "               be careful using Bicubic and Bicubic2 interpolation; there seems to be a problem with JAI\r\n"
1391                                + "               If you use the proogram with images (tif) containing raw data like DEMs just use \r\n"
1392                                + "               Nearest Neighbor interpolation. All other interpolation methods will cause artefacts."
1393                                + "-bbox boundingbox of the the resulting coverage. If not set the bbox will be determined by analysing the input map files. (optional)\r\n"
1394                                + "-resolution spatial resolution of the resulting coverage. If not set the resolution will determined by analysing the input map files. This parameter is conditional; if -bbox is defined -resolution must be defined too.\r\n"
1395                                + "-noOfLevel number of tree levels created (optional default = 1)\r\n"
1396                                + "-capabilitiesFile name of a deegree WCS capabilities/configuration file. If defined the program will add the created rastertree as a new coverage to the WCS configuration.\r\n"
1397                                + "-h or -? print this help\r\n"
1398                                + "\r\n"
1399                                + "Input files\r\n"
1400                                + "there are three alternative ways/parameters to define which input files shall be used for creating a raster tree:\r\n"
1401                                + "1)\r\n"
1402                                + "-mapFiles defines a list of image file names (including full path information) seperated by \',\', \';\' or \'|\'\r\n"
1403                                + "\r\n"
1404                                + "2)\r\n"
1405                                + "-rootDir defines a directory that shall be parsed for files in a known image format. Each file found will be used as input.\r\n"
1406                                + "-subDirs conditional parameter used with -rootDir. It defines if all sub directories of -rootDir shall be parsed too (true|false default false)\r\n"
1407                                + "\r\n"
1408                                + "3)\r\n"
1409                                + "-dbaseFile name a dBase file that contains a column listing all files to be considered by the program\r\n"
1410                                + "-fileColumn name of the column containing the file names (mandatory if -dbaseFile is defined)\r\n"
1411                                + "-baseDir name of the directory where the files are stored. If this parameter will not be set the program assumes the -fileColumn contains completely referenced file names (optional)\r\n"
1412                                + "-sortColumn If -dbaseFile is defined one can define a column that shall be used for sorting the files referenced by the -fileColumn (optional)\r\n"
1413                                + "-sortDirection If -sortColumn is defined this parameter will be used for definition of sorting direction (UP|DOWN default UP)\r\n"
1414                                + "-worldFileType two types of are common: \r\n "
1415                                + "               a) the boundingbox is defined on the center of the corner pixels; \r\n "
1416                                + "               b) the boundingbox is defined on the outer corner of the corner pixels; \r\n "
1417                                + "               first is default and will be used if this parameter is not set; second will be use if '-worldFileType outer' is defined.\r\n"
1418                                + "-quality image quality if jpeg is used as output format; valid range is from 0..1 (default 0.95) \r\n"
1419                                + "-bitDepth image bit depth; valid values are 32 and 16, default is 16 \r\n"
1420                                + "-bgColor defines the background color of the created tiles for those region no data are available (e.g. -bgColor 0xFFFFF defines background as white) \r\n"
1421                                + "         If no -bgColor is defined, transparent background will be used for image formats that are transparency enabled (e.g. png) and black is used for all other formats (e.g. bmp) \r\n"
1422                                + "-offset defines the offset added to raster values if -outputFormat = raw and -bitDepth (default 0) \r\n"
1423                                + "-scaleFactor defines the factor by which raster values are multiplied if -outputFormat = raw and -bitDepth (default 1) \r\n"
1424                                + "\r\n"
1425                                + "Common to all option defining the input files is that each referenced file must be in a known image format (png, tif, jpeg, bmp, gif) and if must be geo-referenced by a world file or must be a GeoTIFF." );
1426            System.out.println();
1427            System.out.println( "caching:" );
1428            System.out.println( "To use default caching mechanism you just have to start RTB as before; to define " );
1429            System.out.println( "your own caching behavior you have to place a file named ehcache.xml within the " );
1430            System.out.println( "root of your classpath. The content of this file is described by the ehcache documentation " );
1431            System.out.println( "(http://ehcache.sourceforge.net/documentation); at least it must provide a cache named 'imgCache'." );
1432            System.out.println( " When defining your own cache please consider that just 'inMemory' caching is supported because " );
1433            System.out.println( "the objects cached by RTB are not serializable." );
1434            System.out.println();
1435            System.out.println( "Example invoking RTB (windows):" );
1436            System.out.println( "java -Xms300m -Xmx1000m -classpath .;.\\lib\\deegree2.jar;.\\lib\\acme.jar;"
1437                                + ".\\lib\\batik-awt-util.jar;.\\lib\\commons-beanutils-1.5.jar;"
1438                                + ".\\lib\\commons-codec-1.3.jar;.\\lib\\commons-collections-3.1.jar;"
1439                                + ".\\lib\\commons-digester-1.7.jar;.\\lib\\commons-discovery-0.2.jar;"
1440                                + ".\\lib\\commons-logging.jar;.\\lib\\jai_codec.jar;.\\lib\\jai_core.jar;"
1441                                + ".\\lib\\mlibwrapper_jai.jar;.\\lib\\j3dcore.jar;.\\lib\\j3dutils.jar;"
1442                                + ".\\lib\\vecmath.jar;.\\lib\\jts-1.6.jar;.\\lib\\log4j-1.2.9.jar;"
1443                                + ".\\lib\\axis.jar;.\\lib\\jaxen-1.1-beta-7.jar;.\\lib\\ehcache-1.2.0_03.jar "
1444                                + "org.deegree.tools.raster.RasterTreeBuilder "
1445                                + "-dbaseFile D:/lgv/resources/data/dbase/dip.dbf -outDir D:/lgv/output/ "
1446                                + "-baseName out -outputFormat jpg -maxTileSize 500 -noOfLevel 4 -interpolation "
1447                                + "Bilinear -bbox 3542428,5918168,3593354,5957043 -resolution 0.2 -sortColumn "
1448                                + "PLANJAHR -fileColumn NAME_PNG -sortDirection UP -quality 0.91 -baseDir "
1449                                + "D:/lgv/resources/data/images/ " );
1450        }
1451    
1452        /**
1453         * 
1454         * @param args
1455         *            Example arguments to pass when calling are:
1456         *            <ul>
1457         *            <li>-mapFiles D:/temp/europe_DK.jpg,D:/temp/europe_BeNeLux.jpg</li>
1458         *            <li>-outDir D:/temp/out/</li>
1459         *            <li>-baseName pretty</li>
1460         *            <li>-outputFormat png</li>
1461         *            <li>-maxTileSize 600</li>
1462         *            </ul>
1463         * 
1464         * @throws Exception
1465         */
1466        public static void main( String[] args )
1467                                throws Exception {
1468            long tsp = System.currentTimeMillis();
1469            Properties map = new Properties();
1470            for ( int i = 0; i < args.length; i += 2 ) {
1471                map.put( args[i], args[i + 1] );
1472            }
1473    
1474            if ( map.get( "-?" ) != null || map.get( "-h" ) != null ) {
1475                printHelp();
1476                return;
1477            }
1478    
1479            try {
1480                validate( map );
1481            } catch ( Exception e ) {
1482                LOG.logInfo( map.toString() );
1483                System.out.println( e.getMessage() );
1484                System.out.println();
1485                printHelp();
1486                return;
1487            }
1488    
1489            String outDir = map.getProperty( "-outDir" );
1490    
1491            // set up stderr/stdout redirection
1492            String redirect = map.getProperty( "-redirect" );
1493            if ( redirect != null && redirect.equals( "true" ) ) {
1494                File f = new File( outDir + separator + "rtb.log" );
1495                PrintStream out = new PrintStream( new FileOutputStream( f ) );
1496                System.setOut( out );
1497                System.setErr( out );
1498            }
1499    
1500            // read input parameters
1501            String baseName = map.getProperty( "-baseName" );
1502            String outputFormat = map.getProperty( "-outputFormat" );
1503            String srs = map.getProperty( "-srs" );
1504            if ( srs == null ) {
1505                srs = "EPSG:4326";
1506            }
1507            String interpolation = map.getProperty( "-interpolation" );
1508            Envelope env = (Envelope) map.get( "-bbox" );
1509            double resolution = ( (Double) map.get( "-resolution" ) ).doubleValue();
1510            int level = Integer.parseInt( map.getProperty( "-noOfLevel" ) );
1511            double maxTileSize = ( Double.valueOf( map.getProperty( "-maxTileSize" ) ) ).doubleValue();
1512            WorldFile.TYPE worldFileType = WorldFile.TYPE.CENTER;
1513            if ( "outer".equals( map.getProperty( "-worldFileType" ) ) ) {
1514                worldFileType = WorldFile.TYPE.OUTER;
1515            }
1516            float quality = Float.parseFloat( map.getProperty( "-quality" ) );
1517            String backgroundColor = map.getProperty( "-bgColor" );
1518    
1519            int depth = 0;
1520    
1521            if ( map.get( "-bitDepth" ) != null ) {
1522                depth = Integer.parseInt( map.getProperty( "-bitDepth" ) );
1523            }
1524    
1525            boolean dummy = false;
1526            if ( map.get( "-dummy" ) != null ) {
1527                dummy = true;
1528            }
1529    
1530            float offset = 0;
1531            if ( map.get( "-offset" ) != null ) {
1532                offset = Float.parseFloat( map.getProperty( "-offset" ) );
1533            }
1534    
1535            float scaleFactor = 1;
1536            if ( map.get( "-scaleFactor" ) != null ) {
1537                scaleFactor = Float.parseFloat( map.getProperty( "-scaleFactor" ) );
1538            }
1539    
1540            List<String> imageFiles = null;
1541            if ( map.get( "-mapFiles" ) != null ) {
1542                String[] mapFiles = StringTools.toArray( map.getProperty( "-mapFiles" ), ",;|", true );
1543                imageFiles = getFileList( mapFiles );
1544            } else if ( map.get( "-dbaseFile" ) != null ) {
1545                String dBaseFile = map.getProperty( "-dbaseFile" );
1546                String fileColum = map.getProperty( "-fileColumn" );
1547                String baseDir = map.getProperty( "-baseDir" );
1548                if ( baseDir == null ) {
1549                    baseDir = map.getProperty( "-rootDir" );
1550                }
1551                boolean sort = map.get( "-sortColumn" ) != null;
1552                String sortColumn = map.getProperty( "-sortColumn" );
1553                if ( map.get( "-sortDirection" ) == null ) {
1554                    map.put( "-sortDirection", "UP" );
1555                }
1556                String sortDirection = map.getProperty( "-sortDirection" );
1557                imageFiles = getFileList( dBaseFile, fileColum, baseDir, sort, sortColumn, sortDirection );
1558            } else if ( map.get( "-rootDir" ) != null ) {
1559                String rootDir = map.getProperty( "-rootDir" );
1560                boolean subDirs = "true".equals( map.get( "-subDirs" ) );
1561                imageFiles = getFileList( rootDir, subDirs );
1562            } else {
1563                LOG.logInfo( map.toString() );
1564                System.out.println( "-mapFiles, -rootDir or -dbaseFile parameter must be defined" );
1565                printHelp();
1566                return;
1567            }
1568    
1569            LOG.logDebug( imageFiles.toString() );
1570            LOG.logInfo( map.toString() );
1571    
1572            // initialize RasterTreeBuilder
1573            RasterTreeBuilderIndexed rtb = new RasterTreeBuilderIndexed( imageFiles, outDir, baseName, outputFormat,
1574                                                                         maxTileSize, srs, interpolation, worldFileType,
1575                                                                         quality, backgroundColor, depth, resolution,
1576                                                                         offset, scaleFactor, dummy );
1577    
1578            // calculate bbox and resolution from input images if parameters are not set
1579            if ( env == null ) {
1580                WorldFile wf = rtb.determineCombiningBBox();
1581                env = wf.getEnvelope();
1582                resolution = wf.getResx();
1583            }
1584    
1585            // Calculate necessary number of levels to get not more than 4
1586            // tiles in highest resolution
1587            if ( level == -1 ) {
1588                rtb.init( env, resolution );
1589                level = 0;
1590                int numTilesMax = Math.min( rtb.tileCols, rtb.tileRows );
1591                int numTiles = 4;
1592                while ( numTiles < numTilesMax ) {
1593                    level += 1;
1594                    numTiles *= 2;
1595                }
1596            }
1597            if ( level == 0 ) {
1598                level = 1;
1599            }
1600            System.out.println( "Number of Levels: " + level );
1601    
1602            // create tree where for each loop resolution will be halfed
1603            double[] re = new double[level];
1604            for ( int i = 0; i < level; i++ ) {
1605                rtb.init( env, resolution );
1606                rtb.start();
1607                rtb.logCollectedErrors();
1608                re[i] = resolution;
1609                if ( i < level - 1 ) {
1610                    String dir = outDir + '/' + Double.toString( resolution );
1611                    imageFiles = getFileList( dir, false );
1612                    rtb = new RasterTreeBuilderIndexed( imageFiles, outDir, baseName, outputFormat, maxTileSize, srs,
1613                                                        interpolation, WorldFile.TYPE.CENTER, quality, backgroundColor,
1614                                                        depth, resolution, offset, scaleFactor );
1615                }
1616                resolution = resolution * 2;
1617            }
1618    
1619            LOG.logInfo( "create configuration files ..." );
1620            rtb.createConfigurationFile( re );
1621    
1622            if ( map.get( "-capabilitiesFile" ) != null ) {
1623                LOG.logInfo( "adjust capabilities ..." );
1624                File file = new File( map.getProperty( "-capabilitiesFile" ) );
1625                rtb.updateCapabilitiesFile( file );
1626            }
1627    
1628            rtb.logCollectedErrors();
1629    
1630            System.out.println( ( System.currentTimeMillis() - tsp ) / 1000 );
1631        }
1632    
1633        /**
1634         * class: official version of a FilenameFilter
1635         */
1636        static class DFileFilter implements FilenameFilter {
1637    
1638            private List<String> extensions = null;
1639    
1640            /**
1641             *
1642             */
1643            public DFileFilter() {
1644                extensions = new ArrayList<String>();
1645                extensions.add( "JPEG" );
1646                extensions.add( "JPG" );
1647                extensions.add( "BMP" );
1648                extensions.add( "PNG" );
1649                extensions.add( "GIF" );
1650                extensions.add( "TIF" );
1651                extensions.add( "TIFF" );
1652                extensions.add( "GEOTIFF" );
1653            }
1654    
1655            /**
1656             * @return "*.*"
1657             */
1658            public String getDescription() {
1659                return "*.*";
1660            }
1661    
1662            /*
1663             * (non-Javadoc)
1664             * 
1665             * @see java.io.FilenameFilter#accept(java.io.File, java.lang.String)
1666             */
1667            public boolean accept( java.io.File file, String name ) {
1668                int pos = name.lastIndexOf( "." );
1669                String ext = name.substring( pos + 1 ).toUpperCase();
1670                if ( file.isDirectory() ) {
1671                    String s = file.getAbsolutePath() + '/' + name;
1672                    File tmp = new File( s );
1673                    if ( tmp.isDirectory() ) {
1674                        return true;
1675                    }
1676                }
1677                return extensions.contains( ext );
1678            }
1679        }
1680    
1681        /**
1682         * 
1683         * This class enables sorting of dBaseFile objects in chronological order (lowest first, highest last).
1684         * 
1685         * @author <a href="mailto:mays@lat-lon.de">Judit Mays</a>
1686         * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a>
1687         * @author last edited by: $Author: apoth $
1688         * 
1689         * @version 2.0, $Revision: 28915 $, $Date: 2010-12-20 17:01:40 +0100 (Mo, 20 Dez 2010) $
1690         * 
1691         * @since 2.0
1692         */
1693        private static class MapAgeComparator implements Comparator<Object> {
1694    
1695            private String direction = null;
1696    
1697            /**
1698             * @param direction
1699             */
1700            public MapAgeComparator( String direction ) {
1701                this.direction = direction.toUpperCase();
1702            }
1703    
1704            public int compare( Object o1, Object o2 ) {
1705                Object[] o1a = (Object[]) o1;
1706                Object[] o2a = (Object[]) o2;
1707    
1708                if ( o1a[0] == null || o2a[0] == null ) {
1709                    return 0;
1710                }
1711                if ( direction.equals( "UP" ) ) {
1712                    return o1a[0].toString().compareTo( o2a[0].toString() );
1713                }
1714                return o2a[0].toString().compareTo( o1a[0].toString() );
1715            }
1716        }
1717    }