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    }