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