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