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 }