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    }