001 //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/branches/2.2_testing/src/org/deegree/ogcwebservices/wmps/DefaultGetMapHandler.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 package org.deegree.ogcwebservices.wmps; 044 045 import java.awt.Color; 046 import java.awt.Font; 047 import java.awt.Graphics; 048 import java.awt.Graphics2D; 049 import java.awt.RenderingHints; 050 import java.awt.image.BufferedImage; 051 import java.util.ArrayList; 052 import java.util.List; 053 054 import org.deegree.framework.log.ILogger; 055 import org.deegree.framework.log.LoggerFactory; 056 import org.deegree.framework.util.CharsetUtils; 057 import org.deegree.framework.util.ImageUtils; 058 import org.deegree.framework.util.MapUtils; 059 import org.deegree.framework.util.StringTools; 060 import org.deegree.framework.xml.XMLParsingException; 061 import org.deegree.graphics.MapFactory; 062 import org.deegree.graphics.MapView; 063 import org.deegree.graphics.Theme; 064 import org.deegree.graphics.optimizers.LabelOptimizer; 065 import org.deegree.graphics.sld.AbstractLayer; 066 import org.deegree.graphics.sld.AbstractStyle; 067 import org.deegree.graphics.sld.NamedLayer; 068 import org.deegree.graphics.sld.NamedStyle; 069 import org.deegree.graphics.sld.SLDFactory; 070 import org.deegree.graphics.sld.StyledLayerDescriptor; 071 import org.deegree.graphics.sld.UserLayer; 072 import org.deegree.graphics.sld.UserStyle; 073 import org.deegree.i18n.Messages; 074 import org.deegree.model.crs.CRSFactory; 075 import org.deegree.model.crs.CoordinateSystem; 076 import org.deegree.model.crs.GeoTransformer; 077 import org.deegree.model.spatialschema.Envelope; 078 import org.deegree.model.spatialschema.Geometry; 079 import org.deegree.model.spatialschema.GeometryFactory; 080 import org.deegree.ogcbase.InvalidSRSException; 081 import org.deegree.ogcwebservices.OGCWebServiceException; 082 import org.deegree.ogcwebservices.wmps.configuration.WMPSConfiguration; 083 import org.deegree.ogcwebservices.wmps.configuration.WMPSDeegreeParams; 084 import org.deegree.ogcwebservices.wms.LayerNotDefinedException; 085 import org.deegree.ogcwebservices.wms.StyleNotDefinedException; 086 import org.deegree.ogcwebservices.wms.capabilities.Layer; 087 import org.deegree.ogcwebservices.wms.capabilities.ScaleHint; 088 import org.deegree.ogcwebservices.wms.configuration.AbstractDataSource; 089 import org.deegree.ogcwebservices.wms.operation.GetMap; 090 091 /** 092 * This is a copy of the WMS package. 093 * 094 * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a> 095 * @author last edited by: $Author: rbezema $ 096 * 097 * @version $Revision: 9446 $, $Date: 2008-01-08 12:00:55 +0100 (Di, 08 Jan 2008) $ 098 */ 099 public class DefaultGetMapHandler implements GetMapHandler { 100 101 private static final ILogger LOG = LoggerFactory.getLogger( DefaultGetMapHandler.class ); 102 103 protected GetMap request; 104 105 private Object[] themes; 106 107 protected double scale = 0; 108 109 private int count = 0; 110 111 protected CoordinateSystem reqCRS; 112 113 private WMPSConfiguration configuration; 114 115 private BufferedImage copyrightImg; 116 117 private Graphics graph; 118 119 /** 120 * Creates a new GetMapHandler object. 121 * 122 * @param configuration 123 * @param request 124 * request to perform 125 * @throws OGCWebServiceException 126 */ 127 public DefaultGetMapHandler( WMPSConfiguration configuration, GetMap request ) throws OGCWebServiceException { 128 this.request = request; 129 this.configuration = configuration; 130 131 try { 132 // get copyright image if possible 133 this.copyrightImg = ImageUtils.loadImage( configuration.getDeegreeParams().getCopyright() ); 134 } catch ( Exception e ) { 135 } 136 137 try { 138 this.reqCRS = CRSFactory.create( this.request.getSrs() ); 139 } catch ( Exception e ) { 140 throw new InvalidSRSException( "SRS: " + request.getSrs() + "is nor known by the deegree WMS" ); 141 } 142 143 } 144 145 /** 146 * returns the configuration used by the handler 147 * 148 * @return WMPSConfiguration 149 */ 150 public WMPSConfiguration getConfiguration() { 151 return this.configuration; 152 } 153 154 /** 155 * increases the counter variable that holds the number of services that has sent a response. 156 * All data are available if the counter value equals the number of requested layers. 157 */ 158 protected synchronized void increaseCounter() { 159 this.count++; 160 } 161 162 /** 163 * performs a GetMap request and retruns the result encapsulated within a <tt>GetMapResult</tt> 164 * object. 165 * <p> 166 * The method throws an WebServiceException that only shall be thrown if an fatal error occurs 167 * that makes it imposible to return a result. If something wents wrong performing the request 168 * (none fatal error) The exception shall be encapsulated within the response object to be 169 * returned to the client as requested (GetMap-Request EXCEPTION-Parameter). 170 * 171 * @param g 172 * @throws OGCWebServiceException 173 */ 174 public void performGetMap( Graphics g ) 175 throws OGCWebServiceException { 176 177 this.graph = g; 178 179 try { 180 CoordinateSystem crs = CRSFactory.create( this.request.getSrs() ); 181 double pixelSize = WMPSConfiguration.INCH2M 182 / configuration.getDeegreeParams().getPrintMapParam().getTargetResolution(); 183 this.scale = MapUtils.calcScale( this.request.getWidth(), this.request.getHeight(), 184 this.request.getBoundingBox(), crs, pixelSize ); 185 LOG.logInfo( "OGC WMPS scale denominator: " + this.scale ); 186 } catch ( Exception e ) { 187 LOG.logDebug( "-", e ); 188 throw new OGCWebServiceException( "Couldn't calculate scale! " + e ); 189 } 190 191 StyledLayerDescriptor sld = null; 192 try { 193 sld = toSLD( this.request.getLayers(), this.request.getStyledLayerDescriptor() ); 194 } catch ( XMLParsingException e1 ) { 195 // should never happen 196 e1.printStackTrace(); 197 } 198 199 AbstractLayer[] layers = sld.getLayers(); 200 // get the number of themes assigned to the selected layers 201 // notice that there maybe more themes as there are layers because 202 // 1 .. n datasources can be assigned to one layer. 203 int cntTh = countNumberOfThemes( layers, this.scale ); 204 this.themes = new Object[cntTh]; 205 // invokes the data supplyer for each layer in an independent thread 206 int kk = 0; 207 for ( int i = 0; i < layers.length; i++ ) { 208 if ( layers[i] instanceof NamedLayer ) { 209 String styleName = null; 210 if ( i < this.request.getLayers().length ) { 211 styleName = this.request.getLayers()[i].getStyleName(); 212 } 213 kk = invokeNamedLayer( layers[i], kk, styleName ); 214 } else { 215 GetMapServiceInvokerForUL si = new GetMapServiceInvokerForUL( this, (UserLayer) layers[i], kk++ ); 216 217 si.start(); 218 } 219 } 220 waitForFinished(); 221 renderMap(); 222 223 } 224 225 /** 226 * Invoke the named layer 227 * 228 * @param layer 229 * @param kk 230 * @param styleName 231 * @return int 232 * @throws OGCWebServiceException 233 */ 234 private int invokeNamedLayer( AbstractLayer layer, int kk, String styleName ) 235 throws OGCWebServiceException { 236 237 Layer lay = this.configuration.getLayer( layer.getName() ); 238 239 if ( validate( lay, layer.getName() ) ) { 240 UserStyle us = getStyles( (NamedLayer) layer, styleName ); 241 AbstractDataSource[] ds = lay.getDataSource(); 242 243 for ( int j = 0; j < ds.length; j++ ) { 244 245 ScaleHint scaleHint = ds[j].getScaleHint(); 246 if ( this.scale >= scaleHint.getMin() && this.scale < scaleHint.getMax() 247 && isValidArea( ds[j].getValidArea() ) ) { 248 GetMapServiceInvokerForNL si = new GetMapServiceInvokerForNL( this, lay, ds[j], us, kk++ ); 249 si.start(); 250 } 251 } 252 } else { 253 // set theme to null if no data are available for the requested 254 // area and/or scale 255 this.themes[kk++] = null; 256 increaseCounter(); 257 } 258 return kk; 259 } 260 261 /** 262 * returns the number of <code>DataSource</code>s involved in a GetMap request 263 * 264 * @param layers 265 * @param currentscale 266 * @return int 267 */ 268 private int countNumberOfThemes( AbstractLayer[] layers, double currentscale ) { 269 int cnt = 0; 270 for ( int i = 0; i < layers.length; i++ ) { 271 if ( layers[i] instanceof NamedLayer ) { 272 Layer lay = this.configuration.getLayer( layers[i].getName() ); 273 AbstractDataSource[] ds = lay.getDataSource(); 274 for ( int j = 0; j < ds.length; j++ ) { 275 276 ScaleHint scaleHint = ds[j].getScaleHint(); 277 if ( currentscale >= scaleHint.getMin() && currentscale < scaleHint.getMax() 278 && isValidArea( ds[j].getValidArea() ) ) { 279 280 cnt++; 281 } 282 } 283 } else { 284 cnt++; 285 } 286 } 287 return cnt; 288 } 289 290 /** 291 * returns true if the requested boundingbox intersects with the valid area of a datasource 292 * 293 * @param validArea 294 * @return boolean 295 */ 296 private boolean isValidArea( Geometry validArea ) { 297 298 if ( validArea != null ) { 299 try { 300 Envelope env = this.request.getBoundingBox(); 301 Geometry geom = GeometryFactory.createSurface( env, this.reqCRS ); 302 if ( !this.reqCRS.getName().equals( validArea.getCoordinateSystem().getName() ) ) { 303 // if requested CRS is not identical to the CRS of the valid area 304 // a transformation must be performed before intersection can 305 // be checked 306 GeoTransformer gt = new GeoTransformer( validArea.getCoordinateSystem() ); 307 geom = gt.transform( geom ); 308 } 309 return geom.intersects( validArea ); 310 } catch ( Exception e ) { 311 // should never happen 312 LOG.logError( "could not validate WMS datasource area", e ); 313 } 314 } 315 return true; 316 } 317 318 /** 319 * runs a loop until all sub requestes (one for each layer) has been finished or the maximum 320 * time limit has been exceeded. 321 * 322 * @throws OGCWebServiceException 323 */ 324 private void waitForFinished() 325 throws OGCWebServiceException { 326 if ( this.count < this.themes.length ) { 327 // waits until the requested layers are available as <tt>DisplayElements</tt> 328 // or the time limit has been reached. 329 // if count == themes.length then no request must be performed 330 long timeStamp = System.currentTimeMillis(); 331 long lapse = 0; 332 long timeout = 1000 * ( this.configuration.getDeegreeParams().getRequestTimeLimit() - 1 ); 333 do { 334 try { 335 Thread.sleep( 50 ); 336 lapse += 50; 337 } catch ( InterruptedException e ) { 338 throw new OGCWebServiceException( "GetMapHandler", "fatal exception waiting for " 339 + "GetMapHandler results" ); 340 } 341 } while ( this.count < this.themes.length && lapse < timeout ); 342 if ( System.currentTimeMillis() - timeStamp >= timeout ) { 343 throw new OGCWebServiceException( "Processing of the GetMap request " + "exceeds timelimit" ); 344 } 345 } 346 } 347 348 /** 349 * 350 * @param layers 351 * @param inSLD 352 * @return StyledLayerDescriptor 353 * @throws XMLParsingException 354 */ 355 private StyledLayerDescriptor toSLD( GetMap.Layer[] layers, StyledLayerDescriptor inSLD ) 356 throws XMLParsingException { 357 StyledLayerDescriptor sld = null; 358 359 if ( layers != null && layers.length > 0 && inSLD == null ) { 360 // Adds the content from the LAYERS and STYLES attribute to the SLD 361 StringBuffer sb = new StringBuffer( 5000 ); 362 sb.append( "<?xml version=\"1.0\" encoding=\"" + CharsetUtils.getSystemCharset() + "\"?>" ); 363 sb.append( "<StyledLayerDescriptor version=\"1.0.0\" " ); 364 sb.append( "xmlns='http://www.opengis.net/sld'>" ); 365 366 for ( int i = 0; i < layers.length; i++ ) { 367 sb.append( "<NamedLayer>" ); 368 sb.append( "<Name>" + layers[i].getName() + "</Name>" ); 369 sb.append( "<NamedStyle><Name>" + layers[i].getStyleName() + "</Name></NamedStyle></NamedLayer>" ); 370 } 371 sb.append( "</StyledLayerDescriptor>" ); 372 373 try { 374 sld = SLDFactory.createSLD( sb.toString() ); 375 } catch ( XMLParsingException e ) { 376 throw new XMLParsingException( StringTools.stackTraceToString( e ) ); 377 } 378 } else if ( layers != null && layers.length > 0 && inSLD != null ) { 379 // if layers not null and sld is not null then SLD layers just be 380 // considered if present in the layers list 381 List<String> list = new ArrayList<String>(); 382 for ( int i = 0; i < layers.length; i++ ) { 383 list.add( layers[i].getName() ); 384 } 385 386 List<AbstractLayer> newList = new ArrayList<AbstractLayer>( 20 ); 387 AbstractLayer[] al = inSLD.getLayers(); 388 for ( int i = 0; i < al.length; i++ ) { 389 if ( list.contains( al[i].getName() ) ) { 390 newList.add( al[i] ); 391 } 392 } 393 al = new AbstractLayer[newList.size()]; 394 sld = new StyledLayerDescriptor( newList.toArray( al ), inSLD.getVersion() ); 395 } else { 396 // if no layers are defined ... 397 sld = inSLD; 398 } 399 400 return sld; 401 } 402 403 /** 404 * returns the <tt>UserStyle</tt>s assigned to a named layer 405 * 406 * @param sldLayer 407 * layer to get the styles for 408 * @param styleName 409 * requested stylename (from the KVP encoding) 410 * @return UserStyle 411 * @throws OGCWebServiceException 412 */ 413 private UserStyle getStyles( NamedLayer sldLayer, String styleName ) 414 throws OGCWebServiceException { 415 416 AbstractStyle[] styles = sldLayer.getStyles(); 417 UserStyle us = null; 418 419 // to avoid retrieving the layer again for each style 420 Layer layer = null; 421 layer = this.configuration.getLayer( sldLayer.getName() ); 422 int i = 0; 423 while ( us == null && i < styles.length ) { 424 if ( styles[i] instanceof NamedStyle ) { 425 // styles will be taken from the WMS's style repository 426 us = getPredefinedStyle( styles[i].getName(), sldLayer.getName(), layer ); 427 } else { 428 // if the requested style fits the name of the defined style or 429 // if the defined style is marked as default and the requested 430 // style if 'default' the condition is true. This includes that 431 // if more than one style with the same name or more than one 432 // style is marked as default always the first will be choosen 433 if ( styleName == null || ( styles[i].getName() != null && styles[i].getName().equals( styleName ) ) 434 || ( styleName.equalsIgnoreCase( "$DEFAULT" ) && ( (UserStyle) styles[i] ).isDefault() ) ) { 435 us = (UserStyle) styles[i]; 436 } 437 } 438 i++; 439 } 440 if ( us == null ) { 441 // this may happens if the SLD contains a named layer but not 442 // a style! yes this is valid according to SLD spec 1.0.0 443 us = getPredefinedStyle( styleName, sldLayer.getName(), layer ); 444 } 445 return us; 446 } 447 448 /** 449 * Returns a Predifined UserStyle 450 * 451 * @param styleName 452 * @param layerName 453 * @param layer 454 * @return UserStyle 455 * @throws StyleNotDefinedException 456 */ 457 private UserStyle getPredefinedStyle( String styleName, String layerName, Layer layer ) 458 throws StyleNotDefinedException { 459 UserStyle us = null; 460 461 if ( "default".equals( styleName ) ) { 462 us = layer.getStyle( styleName ); 463 } 464 465 if ( us == null ) { 466 if ( styleName == null || styleName.length() == 0 || styleName.equals( "$DEFAULT" ) 467 || styleName.equals( "default" ) ) { 468 styleName = "default:" + layerName; 469 } 470 } 471 472 us = layer.getStyle( styleName ); 473 if ( us == null && !( styleName.startsWith( "default" ) ) && !( styleName.startsWith( "$DEFAULT" ) ) ) { 474 String s = Messages.getMessage( "WMS_STYLENOTDEFINED", styleName, layer ); 475 throw new StyleNotDefinedException( s ); 476 } 477 return us; 478 } 479 480 /** 481 * validates if the requested layer matches the conditions of the request if not a 482 * <tt>WebServiceException</tt> will be thrown. If the layer matches the request, but isn't 483 * able to deviever data for the requested area and/or scale false will be returned. If the 484 * layer matches the request and contains data for the requested area and/or scale true will be 485 * returned. 486 * 487 * @param layer 488 * layer as defined at the capabilities/configuration 489 * @param name 490 * name of the layer (must be submitted seperatly because the layer parameter can be 491 * <tt>null</tt> 492 * @return boolean 493 * @throws OGCWebServiceException 494 */ 495 private boolean validate( Layer layer, String name ) 496 throws OGCWebServiceException { 497 498 // check if layer is available 499 if ( layer == null ) { 500 throw new LayerNotDefinedException( "Layer: " + name + " is not known by the WMS" ); 501 } 502 503 if ( !layer.isSrsSupported( this.request.getSrs() ) ) { 504 throw new InvalidSRSException( "SRS: " + this.request.getSrs() + "is not known by layer: " + name ); 505 } 506 507 // check for valid coordinated reference system 508 String[] srs = layer.getSrs(); 509 boolean tmp = false; 510 for ( int i = 0; i < srs.length; i++ ) { 511 if ( srs[i].equalsIgnoreCase( this.request.getSrs() ) ) { 512 tmp = true; 513 break; 514 } 515 } 516 517 if ( !tmp ) { 518 throw new InvalidSRSException( "layer: " + name + " can't be " + "delievered in SRS: " 519 + this.request.getSrs() ); 520 } 521 522 // check bounding box 523 try { 524 525 Envelope bbox = this.request.getBoundingBox(); 526 Envelope layerBbox = layer.getLatLonBoundingBox(); 527 if ( !this.request.getSrs().equalsIgnoreCase( "EPSG:4326" ) ) { 528 // transform the bounding box of the request to EPSG:4326 529 GeoTransformer gt = new GeoTransformer( CRSFactory.create( "EPSG:4326" ) ); 530 bbox = gt.transform( bbox, this.reqCRS ); 531 } 532 if ( !bbox.intersects( layerBbox ) ) { 533 return false; 534 } 535 536 } catch ( Exception e ) { 537 LOG.logError( e.getMessage(), e ); 538 throw new OGCWebServiceException( "couldn't compare bounding boxes\n" + e.toString() ); 539 } 540 541 return true; 542 } 543 544 /** 545 * put a theme to the passed index of the themes array. The second param passed is a 546 * <tt>Theme</tt> or an exception 547 * 548 * @param index 549 * @param o 550 */ 551 protected synchronized void putTheme( int index, Object o ) { 552 this.themes[index] = o; 553 } 554 555 /** 556 * renders the map from the <tt>DisplayElement</tt>s 557 */ 558 private void renderMap() { 559 560 // GetMapResult response = null; 561 OGCWebServiceException exce = null; 562 563 ArrayList<Object> list = new ArrayList<Object>( 50 ); 564 for ( int i = 0; i < this.themes.length; i++ ) { 565 if ( this.themes[i] instanceof Exception ) { 566 exce = new OGCWebServiceException( "GetMapHandler_Impl: renderMap", this.themes[i].toString() ); 567 } 568 if ( this.themes[i] instanceof OGCWebServiceException ) { 569 exce = (OGCWebServiceException) this.themes[i]; 570 break; 571 } 572 if ( this.themes[i] != null ) { 573 list.add( this.themes[i] ); 574 } 575 } 576 577 if ( exce == null ) { 578 // only if no exception occured 579 try { 580 Theme[] th = list.toArray( new Theme[list.size()] ); 581 MapView map = null; 582 if ( th.length > 0 ) { 583 double tr = configuration.getDeegreeParams().getPrintMapParam().getTargetResolution(); 584 tr = WMPSConfiguration.INCH2M / tr; 585 map = MapFactory.createMapView( "deegree WMS", this.request.getBoundingBox(), this.reqCRS, th, tr ); 586 } 587 this.graph.setClip( 0, 0, this.request.getWidth(), this.request.getHeight() ); 588 if ( !this.request.getTransparency() ) { 589 this.graph.setColor( this.request.getBGColor() ); 590 this.graph.fillRect( 0, 0, this.request.getWidth(), this.request.getHeight() ); 591 } 592 if ( map != null ) { 593 Theme[] allthemes = map.getAllThemes(); 594 map.addOptimizer( new LabelOptimizer( allthemes ) ); 595 // antialiasing must be switched of for gif output format 596 // because the antialiasing may create more than 255 colors 597 // in the map/image, even just a few colors are defined in 598 // the styles 599 if ( !this.configuration.getDeegreeParams().isAntiAliased() ) { 600 ( (Graphics2D) this.graph ).setRenderingHint( RenderingHints.KEY_ANTIALIASING, 601 RenderingHints.VALUE_ANTIALIAS_ON ); 602 ( (Graphics2D) this.graph ).setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, 603 RenderingHints.VALUE_TEXT_ANTIALIAS_ON ); 604 } 605 map.paint( this.graph ); 606 } 607 } catch ( Exception e ) { 608 LOG.logError( e.getMessage(), e ); 609 exce = new OGCWebServiceException( "GetMapHandler_Impl: renderMap", e.toString() ); 610 } 611 } 612 613 // print a copyright note at the left lower corner of the map 614 printCopyright( this.graph, this.request.getHeight() ); 615 616 } 617 618 /** 619 * prints a copyright note at left side of the map bottom. The copyright note will be extracted 620 * from the WMS capabilities/configuration 621 * 622 * @param g 623 * graphic context of the map 624 * @param heigth 625 * height of the map in pixel 626 */ 627 private void printCopyright( Graphics g, int heigth ) { 628 629 WMPSDeegreeParams dp = this.configuration.getDeegreeParams(); 630 String copyright = dp.getCopyright(); 631 if ( this.copyrightImg != null ) { 632 g.drawImage( this.copyrightImg, 8, heigth - this.copyrightImg.getHeight() - 5, null ); 633 } else { 634 if ( copyright != null ) { 635 g.setFont( new Font( "SANSSERIF", Font.PLAIN, 14 ) ); 636 g.setColor( Color.BLACK ); 637 g.drawString( copyright, 8, heigth - 15 ); 638 g.drawString( copyright, 10, heigth - 15 ); 639 g.drawString( copyright, 8, heigth - 13 ); 640 g.drawString( copyright, 10, heigth - 13 ); 641 g.setColor( Color.WHITE ); 642 g.setFont( new Font( "SANSSERIF", Font.PLAIN, 14 ) ); 643 g.drawString( copyright, 9, heigth - 14 ); 644 // g.dispose(); 645 } 646 } 647 } 648 }