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    }