001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/branches/2.2_testing/src/org/deegree/tools/raster/RasterTreeUpdater.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2008 by:
006     EXSE, Department of Geography, University of Bonn
007     http://www.giub.uni-bonn.de/deegree/
008     lat/lon GmbH
009     http://www.lat-lon.de
010    
011     This library is free software; you can redistribute it and/or
012     modify it under the terms of the GNU Lesser General Public
013     License as published by the Free Software Foundation; either
014     version 2.1 of the License, or (at your option) any later version.
015    
016     This library is distributed in the hope that it will be useful,
017     but WITHOUT ANY WARRANTY; without even the implied warranty of
018     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
019     Lesser General Public License for more details.
020    
021     You should have received a copy of the GNU Lesser General Public
022     License along with this library; if not, write to the Free Software
023     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
024    
025     Contact:
026    
027     Andreas Poth
028     lat/lon GmbH
029     Aennchenstr. 19
030     53115 Bonn
031     Germany
032     E-Mail: poth@lat-lon.de
033    
034     Prof. Dr. Klaus Greve
035     Department of Geography
036     University of Bonn
037     Meckenheimer Allee 166
038     53115 Bonn
039     Germany
040     E-Mail: greve@giub.uni-bonn.de
041    
042     
043     ---------------------------------------------------------------------------*/
044    package org.deegree.tools.raster;
045    
046    import java.awt.image.BufferedImage;
047    import java.io.File;
048    import java.io.IOException;
049    import java.net.MalformedURLException;
050    import java.net.URL;
051    import java.util.ArrayList;
052    import java.util.List;
053    import java.util.Properties;
054    import java.util.SortedMap;
055    import java.util.TreeMap;
056    
057    import javax.media.jai.Interpolation;
058    import javax.media.jai.JAI;
059    import javax.media.jai.RenderedOp;
060    import javax.media.jai.TiledImage;
061    
062    import net.sf.ehcache.Cache;
063    import net.sf.ehcache.CacheException;
064    import net.sf.ehcache.CacheManager;
065    import net.sf.ehcache.Element;
066    import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
067    
068    import org.deegree.framework.log.ILogger;
069    import org.deegree.framework.log.LoggerFactory;
070    import org.deegree.framework.util.ImageUtils;
071    import org.deegree.framework.util.StringTools;
072    import org.deegree.io.dbaseapi.DBaseException;
073    import org.deegree.io.shpapi.HasNoDBaseFileException;
074    import org.deegree.io.shpapi.ShapeFile;
075    import org.deegree.model.coverage.grid.WorldFile;
076    import org.deegree.model.crs.UnknownCRSException;
077    import org.deegree.model.feature.Feature;
078    import org.deegree.model.feature.FeatureProperty;
079    import org.deegree.model.spatialschema.Envelope;
080    import org.deegree.model.spatialschema.Geometry;
081    import org.deegree.ogcwebservices.wcs.configuration.Resolution;
082    import org.deegree.ogcwebservices.wcs.configuration.ShapeResolution;
083    import org.deegree.ogcwebservices.wcs.describecoverage.CoverageDescriptionDocument;
084    import org.deegree.ogcwebservices.wcs.describecoverage.CoverageOffering;
085    import org.deegree.ogcwebservices.wcs.describecoverage.InvalidCoverageDescriptionExcpetion;
086    import org.xml.sax.SAXException;
087    
088    import com.sun.media.jai.codec.FileSeekableStream;
089    
090    /**
091     * The <code>RasterTreeUpdater</code> is a command line utility that can be used in addition to
092     * the <code>RasterTreeBuilder</code> to update a previously generated raster tree.
093     * 
094     * @author <a href="mailto:schmitz@lat-lon.de">Andreas Schmitz</a>
095     * @author last edited by: $Author: apoth $
096     * 
097     * @version 2.0, $Revision: 9346 $, $Date: 2007-12-27 17:39:07 +0100 (Do, 27 Dez 2007) $
098     * 
099     * @since 2.0
100     */
101    public class RasterTreeUpdater {
102    
103        private static final ILogger LOG = LoggerFactory.getLogger( RasterTreeUpdater.class );
104    
105        private RTUConfiguration config;
106    
107        private SortedMap<Double, ShapeResolution> shapeFiles;
108    
109        private Cache imgCache;
110    
111        // is determined automatically off one of the output filenames
112        private String format;
113    
114        /**
115         * Creates a new <code>RasterTreeUpdater</code> configured through the options contained in
116         * the passed configuration
117         * 
118         * @param config
119         * @throws IllegalStateException
120         * @throws CacheException
121         * @throws IOException
122         */
123        public RasterTreeUpdater( RTUConfiguration config ) throws IllegalStateException, CacheException, IOException {
124            this.config = config;
125    
126            // a lot of lines just for a simple cache, but what the heck...
127            CacheManager singletonManager = CacheManager.create();
128            if ( singletonManager.getCache( "imgCache" ) == null ) {
129                Cache cache = new Cache( "imgCache", 10, MemoryStoreEvictionPolicy.LFU, false, ".", false, 3600, 3600,
130                                         false, 240, null );
131                singletonManager.addCache( cache );
132                imgCache = singletonManager.getCache( "imgCache" );
133            } else {
134                imgCache = singletonManager.getCache( "imgCache" );
135                imgCache.removeAll();
136            }
137        }
138    
139        /**
140         * loads an image
141         * @param imageSource
142         * @return
143         * @throws IOException
144         */
145        private TiledImage loadImage( String imageSource )
146                                throws IOException {
147    
148            TiledImage ti = null;
149            Element elem = imgCache.get( imageSource );
150            if ( elem != null ) {
151                ti = (TiledImage) elem.getObjectValue();
152            }
153    
154            if ( ti == null ) {
155                if ( config.verbose ) {
156                    LOG.logInfo( "Cache size: " + imgCache.getSize() );
157                    LOG.logInfo( "Reading image: " + imageSource );
158                }
159    
160                FileSeekableStream fss = new FileSeekableStream( imageSource );
161                RenderedOp rop = JAI.create( "stream", fss );
162                BufferedImage bi = rop.getAsBufferedImage();
163                fss.close();
164                ti = new TiledImage( bi, 500, 500 );
165                imgCache.put( new Element( imageSource, ti ) );
166            }
167    
168            return ti;
169        }
170    
171        /**
172         * Initializes the instance.
173         * 
174         * @throws IOException
175         * @throws SAXException
176         * @throws InvalidCoverageDescriptionExcpetion
177         * @throws UnknownCRSException
178         */
179        public void init()
180                                throws IOException, SAXException, InvalidCoverageDescriptionExcpetion, UnknownCRSException {
181            CoverageDescriptionDocument doc = new CoverageDescriptionDocument();
182            doc.load( config.wcsConfiguration );
183    
184            CoverageOffering offering = null;
185            if ( config.coverageName == null ) {
186                offering = doc.getCoverageOfferings()[0];
187            } else {
188                for ( CoverageOffering of : doc.getCoverageOfferings() ) {
189                    if ( of.getName().equals( config.coverageName ) ) {
190                        offering = of;
191                    }
192                }
193            }
194    
195            Resolution[] rs = offering.getExtension().getResolutions();
196            shapeFiles = new TreeMap<Double, ShapeResolution>();
197            for ( Resolution r : rs ) {
198                shapeFiles.put( new Double( r.getMinScale() ), (ShapeResolution) r );
199            }
200    
201        }
202    
203        /**
204         * extracts the envelopes that correspond to the filenames of getfilenames
205         * @param shapeName
206         * @return
207         * @throws IOException
208         */
209        private ArrayList<Envelope> getEnvelopes( String shapeName )
210                                throws IOException {
211            ShapeFile file = new ShapeFile( shapeName );
212            ArrayList<Envelope> envs = new ArrayList<Envelope>( file.getRecordNum() );
213    
214            for ( int i = 0; i < file.getRecordNum(); ++i ) {
215                Geometry geom = file.getGeometryByRecNo( i + 1 );
216                envs.add( geom.getEnvelope() );
217                if ( config.verbose ) {
218                    LOG.logInfo( StringTools.concat( 200, "Envelope of tile is ", geom.getEnvelope() ) );
219                }
220            }
221            file.close();
222    
223            return envs;
224        }
225    
226        /**
227         * extracts the filenames of the tiles contained within the shape file dbf
228         * @param shapeName
229         * @return
230         * @throws IOException
231         * @throws HasNoDBaseFileException
232         * @throws DBaseException
233         */
234        private ArrayList<String> getTilenames( String shapeName )
235                                throws IOException, HasNoDBaseFileException, DBaseException {
236            ShapeFile file = new ShapeFile( shapeName );
237            String dirName = new File( shapeName ).getParent();
238            if ( dirName == null ) {
239                dirName = "./";
240            }
241    
242            ArrayList<String> tileNames = new ArrayList<String>( file.getRecordNum() );
243            for ( int i = 0; i < file.getRecordNum(); ++i ) {
244                Feature f = file.getFeatureByRecNo( i + 1 );
245                FeatureProperty[] p = f.getProperties();
246                StringBuffer name = new StringBuffer( 200 );
247                name.append( dirName ).append( "/" );
248                name.append( ( p[1].getValue() == null ) ? "" : p[1].getValue() );
249                name.append( "/" ).append( p[0].getValue() );
250                tileNames.add( name.toString() );
251                if ( config.verbose ) {
252                    LOG.logInfo( StringTools.concat( 200, "Found tile ", name ) );
253                }
254            }
255            file.close();
256    
257            return tileNames;
258        }
259    
260        /**
261         * returns the envelopes of the files to be updated
262         * @return
263         * @throws IOException
264         */
265        private ArrayList<Envelope> getUpdatedEnvelopes()
266                                throws IOException {
267            ArrayList<Envelope> updatedEnvelopes = new ArrayList<Envelope>( config.updatedFiles.size() );
268    
269            for ( String filename : config.updatedFiles ) {
270                WorldFile wf = WorldFile.readWorldFile( filename, config.worldfileType );
271                updatedEnvelopes.add( wf.getEnvelope() );
272                if ( config.verbose ) {
273                    LOG.logInfo( StringTools.concat( 200, "Updating from file ", filename, " with envelope ",
274                                                     wf.getEnvelope() ) );
275                }
276                if ( format == null ) {
277                    format = filename.substring( filename.lastIndexOf( '.' ) + 1 );
278                }
279            }
280    
281            return updatedEnvelopes;
282        }
283    
284        /**
285         *  updates the tiles with the image file
286         * @param filename
287         * @param envelope
288         * @param tileNames
289         * @param tileEnvelopes
290         * @param res
291         * @throws IOException
292         */
293        private void updateFile( String filename, Envelope envelope, List<String> tileNames, List<Envelope> tileEnvelopes,
294                                 double res )
295                                throws IOException {
296    
297            for ( int i = 0; i < tileNames.size(); ++i ) {
298                Envelope env = tileEnvelopes.get( i );
299                if ( !envelope.intersects( env ) ) {
300                    continue;
301                }
302                String tile = tileNames.get( i );
303    
304                // paint the new image on top of the existing one
305                if ( config.verbose ) {
306                    LOG.logInfo( StringTools.concat( 200, "Updating tile ", tile, " with image ", filename ) );
307                }
308    
309                TiledImage tileImage = loadImage( tile );
310                WorldFile wf = WorldFile.readWorldFile( filename, config.worldfileType );
311                TiledImage inputImage = loadImage( filename );
312                Tile t = new Tile( WorldFile.readWorldFile( tile, config.worldfileType ).getEnvelope(), null );
313                BufferedImage out = tileImage.getAsBufferedImage();
314                float[][] data = null;
315                if ( out.getColorModel().getPixelSize() == 16 ) {
316                    // do not use image api if target bitDepth = 16
317                    data = new float[out.getHeight()][out.getWidth()];
318                }
319                RasterTreeBuilder.drawImage( out, data, inputImage, t, wf, res, config.interpolation, null, format,
320                                             config.bitDepth, 0, 1 );
321    
322                String frm = format;
323                if ( "raw".equals( frm ) ) {
324                    frm = "tif";
325                }
326    
327                File file = new File( tile ).getAbsoluteFile();
328    
329                ImageUtils.saveImage( out, file, config.quality );
330    
331            }
332    
333        }
334    
335        /**
336         * a hack to determine the minimum resolution
337         * @param shapeName
338         * @return
339         * @throws IOException
340         * @throws HasNoDBaseFileException
341         * @throws DBaseException
342         */
343        private double getLevel( String shapeName )
344                                throws IOException, HasNoDBaseFileException, DBaseException {
345            ShapeFile file = new ShapeFile( shapeName );
346            Feature f = file.getFeatureByRecNo( 1 );
347            FeatureProperty[] p = f.getProperties();
348            file.close();
349            return Double.parseDouble( p[1].getValue().toString() );
350        }
351    
352        /**
353         * Updates the images.
354         * 
355         * @throws IOException
356         * @throws DBaseException
357         * @throws HasNoDBaseFileException
358         */
359        public void update()
360                                throws IOException, HasNoDBaseFileException, DBaseException {
361            SortedMap<Double, ShapeResolution> shapes = new TreeMap<Double, ShapeResolution>();
362            shapes.putAll( shapeFiles );
363    
364            // stores the envelopes of the files that are being updated
365            ArrayList<Envelope> updatedEnvelopes = getUpdatedEnvelopes();
366    
367            while ( !shapes.isEmpty() ) {
368                ShapeResolution shape = shapes.remove( shapes.firstKey() );
369                String shapeName = shape.getShape().getRootFileName();
370                double res = getLevel( shapeName );
371    
372                LOG.logInfo( StringTools.concat( 200, "Processing shape file ", shapeName, "..." ) );
373    
374                // these store the image filenames of the existing tiles and their envelopes
375                ArrayList<String> tileNames = getTilenames( shapeName );
376                ArrayList<Envelope> envelopes = getEnvelopes( shapeName );
377    
378                for ( int i = 0; i < config.updatedFiles.size(); ++i ) {
379                    String filename = config.updatedFiles.get( i );
380                    Envelope envelope = updatedEnvelopes.get( i );
381    
382                    updateFile( filename, envelope, tileNames, envelopes, res );
383                }
384            }
385        }
386    
387        /**
388         * Prints out usage information and the message, then <code>System.exit</code>s.
389         * 
390         * @param message
391         *            can be null
392         */
393        private static void printUsage( String message ) {
394            if ( message != null ) {
395                System.out.println( message );
396                System.out.println();
397            }
398    
399            System.out.println( "Usage:" );
400            System.out.println();
401            System.out.println( "<classpath> <rtu> <options>" );
402            System.out.println( "   where" );
403            System.out.println( "  <rtu>:" );
404            System.out.println( "           java <classpath> org.deegree.tools.raster.RasterTreeUpdater" );
405            System.out.println( "  <classpath>:" );
406            System.out.println( "           -cp <the classpath containing the deegree.jar and " );
407            System.out.println( "                additional required libraries>" );
408            System.out.println( "  <option>:" );
409            System.out.println( "           as follows:" );
410            System.out.println();
411            System.out.println( "  -wcs <URL/filename>:" );
412            System.out.println( "           The URL or a filename of the WCS configuration that was" );
413            System.out.println( "           generated by the RasterTreeBuilder. Mandatory." );
414            System.out.println( "  -name <name>:" );
415            System.out.println( "           The name of the coverage to update. Optional." );
416            System.out.println( "  -verbose:" );
417            System.out.println( "           Print out more informational messages." );
418            System.out.println( "  -interpolation <name>: " );
419            System.out.println( "           The name of the interpolation to be used, as specified in the" );
420            System.out.println( "           RasterTreeBuilder. Optional. Default is Nearest Neighbor." );
421            System.out.println( "  -depth <n>:" );
422            System.out.println( "           The bit depth of the output images. Optional. Default is 16." );
423            System.out.println( "  -quality <n>:" );
424            System.out.println( "           The desired output quality, between 0 and 1. Optional. Default is 0.95." );
425            System.out.println( "  -mapFiles <file1,file2...fileN>:" );
426            System.out.println( "           comma seperated list of image files to update. These files" );
427            System.out.println( "           need to have a corresponding worldfile, as usual." );
428            System.out.println( "  -worldFileType <type>:" );
429            System.out.println( "           How to treat worldfiles that are read. Possible values are outer and" );
430            System.out.println( "           center. Center is the default." );
431        }
432    
433        /**
434         * @param args
435         */
436        public static void main( String[] args ) {
437            try {
438                RTUConfiguration config = new RTUConfiguration( args );
439                RasterTreeUpdater updater = new RasterTreeUpdater( config );
440                updater.init();
441                updater.update();
442            } catch ( MalformedURLException e ) {
443                e.printStackTrace();
444                printUsage( "An URL is malformed." );
445            } catch ( ClassCastException e ) {
446                e.printStackTrace();
447                printUsage( "Data is not defined in shapefiles." );
448            } catch ( IOException e ) {
449                e.printStackTrace();
450                printUsage( "The coverage offering document can not be read:" );
451            } catch ( SAXException e ) {
452                e.printStackTrace();
453                printUsage( "The coverage offering document is not in XML format:" );
454            } catch ( InvalidCoverageDescriptionExcpetion e ) {
455                e.printStackTrace();
456                printUsage( "The coverage offering document is not valid:" );
457            } catch ( UnknownCRSException e ) {
458                e.printStackTrace();
459                printUsage( "The coverage offering document is not sound:" );
460            } catch ( HasNoDBaseFileException e ) {
461                e.printStackTrace();
462                printUsage( "A shapefile has no associated .dbf." );
463            } catch ( DBaseException e ) {
464                e.printStackTrace();
465                printUsage( "A shapefile database is in the wrong format or has errors." );
466            }
467    
468        }
469    
470        /**
471         * <code>RTUConfiguration</code> is a class containing configuration options for the
472         * <code>RasterTreeUpdater</code>.
473         * 
474         * @author <a href="mailto:schmitz@lat-lon.de">Andreas Schmitz</a>
475         * @author last edited by: $Author: apoth $
476         * 
477         * @version 2.0, $Revision: 9346 $, $Date: 2007-12-27 17:39:07 +0100 (Do, 27 Dez 2007) $
478         * 
479         * @since 2.0
480         */
481        public static class RTUConfiguration {
482    
483            /**
484             * The location of the WCS configuration document.
485             */
486            URL wcsConfiguration;
487    
488            /**
489             * The list of image files being updated.
490             */
491            List<String> updatedFiles;
492    
493            /**
494             * The coverage name to update.
495             */
496            String coverageName;
497    
498            /**
499             * Whether to be verbose in logging.
500             */
501            boolean verbose;
502    
503            /**
504             * The interpolation method to be used.
505             */
506            Interpolation interpolation;
507    
508            /**
509             * The bit depth for the output images.
510             */
511            int bitDepth;
512    
513            /**
514             * Desired output image quality.
515             */
516            float quality;
517    
518            /**
519             * Worldfile type used for reading.
520             */
521            WorldFile.TYPE worldfileType;
522    
523            /**
524             * 
525             * @param wcsConfiguration
526             * @param updatedFiles
527             * @param coverageName
528             * @param verbose
529             * @param interpolation
530             * @param bitDepth
531             * @param quality
532             * @param worldfileType
533             */
534            public RTUConfiguration( URL wcsConfiguration, List<String> updatedFiles, String coverageName, boolean verbose,
535                                     Interpolation interpolation, int bitDepth, float quality, WorldFile.TYPE worldfileType ) {
536                this.wcsConfiguration = wcsConfiguration;
537                this.updatedFiles = updatedFiles;
538                this.coverageName = coverageName;
539                this.verbose = verbose;
540                this.interpolation = interpolation;
541                this.bitDepth = bitDepth;
542                this.quality = quality;
543                this.worldfileType = worldfileType;
544            }
545    
546            /**
547             * Constructs a new instance through command line arguments.
548             * 
549             * @param args
550             *            the command line arguments
551             * @throws MalformedURLException
552             */
553            public RTUConfiguration( String[] args ) throws MalformedURLException {
554    
555                Properties map = new Properties();
556                int i = 0;
557                while ( i < args.length ) {
558                    if ( args[i].equals( "-verbose" ) ) {
559                        map.put( args[i++], "-" );
560                    } else {
561                        map.put( args[i++], args[i++] );
562                    }
563                }
564    
565                try {
566                    wcsConfiguration = new URL( map.getProperty( "-wcs" ) );
567                } catch ( MalformedURLException e ) {
568                    wcsConfiguration = new File( map.getProperty( "-wcs" ) ).toURI().toURL();
569                }
570    
571                coverageName = map.getProperty( "-name" );
572    
573                verbose = map.getProperty( "-verbose" ) != null;
574    
575                if ( map.getProperty( "-interpolation" ) != null ) {
576                    String t = map.getProperty( "-interpolation" );
577                    interpolation = RasterTreeBuilder.createInterpolation( t );
578                } else {
579                    interpolation = RasterTreeBuilder.createInterpolation( "Nearest Neighbor" );
580                }
581    
582                bitDepth = 32;
583                if ( map.getProperty( "-depth" ) != null ) {
584                    bitDepth = Integer.parseInt( map.getProperty( "-depth" ) );
585                }
586    
587                quality = 0.95f;
588                if ( map.getProperty( "-quality" ) != null ) {
589                    quality = Float.parseFloat( map.getProperty( "-quality" ) );
590                }
591    
592                worldfileType = WorldFile.TYPE.CENTER;
593                if ( map.getProperty( "-worldFileType" ) != null ) {
594                    if ( map.getProperty( "-worldFileType" ).equalsIgnoreCase( "outer" ) ) {
595                        worldfileType = WorldFile.TYPE.OUTER;
596                    }
597                }
598    
599                updatedFiles = StringTools.toList( map.getProperty( "-mapFiles" ), ",;", true );
600            }
601    
602            /**
603             * @return true, if the configuration values are sound
604             */
605            public boolean isValidConfiguration() {
606                return updatedFiles.size() > 0 && wcsConfiguration != null;
607            }
608    
609        }
610    
611    }