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