001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/tags/2.1/src/org/deegree/ogcwebservices/wmps/PrintMapHandler.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2006 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     Aennchenstraße 19
030     53177 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    
044    package org.deegree.ogcwebservices.wmps;
045    
046    import java.awt.Color;
047    import java.awt.Graphics;
048    import java.awt.Graphics2D;
049    import java.awt.Image;
050    import java.awt.geom.AffineTransform;
051    import java.awt.image.AffineTransformOp;
052    import java.awt.image.BufferedImage;
053    import java.io.ByteArrayOutputStream;
054    import java.io.File;
055    import java.io.IOException;
056    import java.io.InputStream;
057    import java.io.StringReader;
058    import java.net.MalformedURLException;
059    import java.net.URISyntaxException;
060    import java.net.URL;
061    import java.sql.Connection;
062    import java.sql.SQLException;
063    import java.text.MessageFormat;
064    import java.util.ArrayList;
065    import java.util.HashMap;
066    import java.util.List;
067    import java.util.Map;
068    
069    import net.sf.jasperreports.engine.JREmptyDataSource;
070    import net.sf.jasperreports.engine.JRException;
071    import net.sf.jasperreports.engine.JasperExportManager;
072    import net.sf.jasperreports.engine.JasperFillManager;
073    import net.sf.jasperreports.engine.JasperPrint;
074    
075    import org.deegree.datatypes.values.Values;
076    import org.deegree.framework.log.ILogger;
077    import org.deegree.framework.log.LoggerFactory;
078    import org.deegree.framework.util.ImageUtils;
079    import org.deegree.framework.util.MapUtils;
080    import org.deegree.framework.util.StringTools;
081    import org.deegree.framework.xml.XMLFragment;
082    import org.deegree.framework.xml.XMLParsingException;
083    import org.deegree.framework.xml.XMLTools;
084    import org.deegree.graphics.sld.StyledLayerDescriptor;
085    import org.deegree.graphics.transformation.WorldToScreenTransform;
086    import org.deegree.i18n.Messages;
087    import org.deegree.model.crs.CRSFactory;
088    import org.deegree.model.crs.CoordinateSystem;
089    import org.deegree.model.crs.UnknownCRSException;
090    import org.deegree.model.spatialschema.Envelope;
091    import org.deegree.model.spatialschema.GeometryFactory;
092    import org.deegree.model.spatialschema.Point;
093    import org.deegree.ogcwebservices.InconsistentRequestException;
094    import org.deegree.ogcwebservices.OGCWebServiceException;
095    import org.deegree.ogcwebservices.wmps.configuration.PrintMapParam;
096    import org.deegree.ogcwebservices.wmps.configuration.WMPSConfiguration;
097    import org.deegree.ogcwebservices.wmps.operation.PrintMap;
098    import org.deegree.ogcwebservices.wmps.operation.PrintMapResponseDocument;
099    import org.deegree.ogcwebservices.wmps.operation.TextArea;
100    import org.deegree.ogcwebservices.wms.capabilities.Layer;
101    import org.deegree.ogcwebservices.wms.capabilities.LegendURL;
102    import org.deegree.ogcwebservices.wms.capabilities.ScaleHint;
103    import org.deegree.ogcwebservices.wms.capabilities.Style;
104    import org.deegree.ogcwebservices.wms.configuration.AbstractDataSource;
105    import org.deegree.ogcwebservices.wms.configuration.LocalWCSDataSource;
106    import org.deegree.ogcwebservices.wms.configuration.RemoteWCSDataSource;
107    import org.deegree.ogcwebservices.wms.configuration.RemoteWMSDataSource;
108    import org.deegree.ogcwebservices.wms.operation.GetMap;
109    import org.w3c.dom.Document;
110    import org.w3c.dom.Element;
111    import org.w3c.dom.Node;
112    
113    /**
114     * Handles the PrintMap request. Retrieves the request from the DB and creates a pdf file.
115     * 
116     * @author <a href="mailto:deshmukh@lat-lon.de">Anup Deshmukh</a>
117     * @author last edited by: $Author: apoth $
118     * 
119     * @version 2.0, $Revision: 7937 $, $Date: 2007-08-08 12:00:55 +0200 (Mi, 08 Aug 2007) $
120     */
121    public class PrintMapHandler implements Runnable {
122    
123        private static final ILogger LOG = LoggerFactory.getLogger( PrintMapHandler.class );
124    
125        private final double TILE_MAX_SIZE = 800;
126    
127        private final String FORMAT = ".png";
128    
129        private final String MIMETYPE = "image/png";
130    
131        private final String EXCEPTION = "application/vnd.ogc.se_inimage";
132    
133        private WMPSConfiguration configuration;
134    
135        private String message;
136        
137        private static final double PIXELSIZE = 0.028; // has been 0.0254
138    
139        /**
140         * Creates a new instance of the PrintMapHandler for the current configuration.
141         * 
142         * @param configuration
143         */
144        public PrintMapHandler( WMPSConfiguration configuration ) {
145            this.configuration = configuration;
146        }
147    
148        /**
149         * Run a new thread for each PrintMap request. This Thread runs till no more PrintMap requests
150         * are available in the DB.
151         * 
152         * @see java.lang.Runnable#run()
153         */
154        public void run() {
155    
156            RequestManager manager = null;
157            PrintMapResponseDocument response = null;
158            WMPSDatabase wmpsDB = null;
159            Connection connection = null;
160            try {
161    
162                wmpsDB = new WMPSDatabase( this.configuration.getDeegreeParams().getCacheDatabase() );
163    
164                connection = wmpsDB.acquireConnection();
165    
166                while ( true ) {
167                    // request aus db holen
168                    PrintMap printMap = wmpsDB.selectPrintMapRequest( connection );
169                    if ( printMap == null ) {
170                        // abort();
171                        wmpsDB.releaseConnection( connection );
172                        break;
173                    }
174                    // Kartenbild erzeugen
175                    LOG.logDebug( "Performing print map" );
176                    manager = new DefaultRequestManager( this.configuration, printMap );
177                    performPrintMap( printMap, false );
178                    wmpsDB.updateDB( connection, printMap.getId(), printMap.getTimestamp() );
179                    response = manager.createFinalResponse( this.message, null );
180                    manager.sendEmail( response );
181                    wmpsDB.releaseConnection( connection );
182                    LOG.logDebug( "Done performing PrintMap request." );
183                }
184            } catch ( Exception e ) {
185                LOG.logError( e.getMessage(), e );
186                if ( manager != null ) {
187                    try {
188                        response = manager.createFinalResponse( this.message, e.getMessage() );
189                        manager.sendEmail( response );
190                    } catch ( Exception e1 ) {
191                        // should just happen if mail server is not reachable
192                        // in this case the error just can be logged
193                        XMLFragment doc = new XMLFragment( response.getRootElement() );
194                        LOG.logDebug( doc.getAsString() );
195                        LOG.logError( e.getMessage(), e );
196                    }
197                }
198            } finally {
199                try {
200                    if ( !connection.isClosed() ) {
201                        wmpsDB.releaseConnection( connection );
202                    }
203                } catch ( SQLException e ) {
204                    // should never happen
205                    LOG.logError( e.getMessage(), e );
206                }
207            }
208        }
209    
210        /**
211         * performs a sychronous printMap processing
212         * 
213         * @param printMap
214         * @return byte[]
215         * @throws Exception
216         */
217        public byte[] runSynchronous( PrintMap printMap )
218                                throws Exception {
219            return performPrintMap( printMap, true );
220        }
221    
222        /**
223         * From each PrintMap request run the WMS GetMap request.
224         * 
225         * @param printMap
226         * @param synchronous
227         * @return byte[]
228         * @throws PrintMapServiceException
229         * @throws IOException
230         */
231        private byte[] performPrintMap( PrintMap printMap, boolean synchronous )
232                                throws PrintMapServiceException, IOException {
233    
234            Map config_layers = retrieveLayersFromConfig( printMap );
235    
236            int[] mapParams = getMapParamsFromTemplate( printMap.getTemplate() );
237            int scaleDenominator = printMap.getScaleDenominator();
238            Envelope bbox = printMap.getBBOX();
239            if ( bbox == null ) {
240                LOG.logDebug( "BBOX not defined. Using the center and scale to calculate a new BBOX." );
241                Point center = printMap.getCenter();
242                bbox = createBBOX( center, scaleDenominator, mapParams[0], mapParams[1] );
243            } else { // Adjust aspect ratio of bbox to aspect ratio of template map area
244                double bboxAspectRatio = bbox.getWidth() / bbox.getHeight();
245                double printAspectRatio = ( (double) mapParams[0] ) / mapParams[1];
246                if ( bboxAspectRatio > printAspectRatio ) {
247                    double centerY = bbox.getMin().getY() + ( ( bbox.getMax().getY() - bbox.getMin().getY() ) / 2d );
248                    double height = bbox.getWidth() * ( 1.0 / printAspectRatio );
249                    double minY = centerY - ( height / 2d );
250                    double maxY = centerY + ( height / 2d );
251                    bbox = GeometryFactory.createEnvelope( bbox.getMin().getX(), minY, bbox.getMax().getX(), maxY,
252                                                           bbox.getCoordinateSystem() );
253                } else {
254                    double centerX = bbox.getMin().getX() + ( ( bbox.getMax().getX() - bbox.getMin().getX() ) / 2d );
255                    double width = bbox.getHeight() * printAspectRatio;
256                    double minX = centerX - ( width / 2d );
257                    double maxX = centerX + ( width / 2d );
258                    bbox = GeometryFactory.createEnvelope( minX, bbox.getMin().getY(), maxX, bbox.getMax().getY(),
259                                                           bbox.getCoordinateSystem() );
260                }
261            }
262            if ( scaleDenominator == -1 ) {
263                LOG.logDebug( "Scale not defined. Using MapUtil to calculate the scale denominator for the current bbox and map sizes" );
264                double pixelSize = PIXELSIZE / configuration.getDeegreeParams().getPrintMapParam().getTargetResolution();
265                scaleDenominator = (int) MapUtils.calcScale( mapParams[0], mapParams[1], bbox, bbox.getCoordinateSystem(),
266                                                             pixelSize );
267            }
268            BufferedImage mapImage = null;
269            try {
270                mapImage = performBuildMapImage( config_layers, printMap, bbox, mapParams[0], mapParams[1] );
271    
272                saveImageToDisk( printMap, mapImage, "MAP" );
273                String s = StringTools.concat( 100, "Retrieved PrintMap request '", printMap.getId(), "' and saved to disk" );
274                LOG.logDebug( s );
275            } catch ( OGCWebServiceException e ) {
276                LOG.logError( e.getMessage(), e );
277                throw new PrintMapServiceException( Messages.getMessage( "WMPS_ERROR_PERFORMING_PRINTMAP" ) );
278            } catch ( IOException e ) {
279                LOG.logError( e.getMessage(), e );
280                throw new PrintMapServiceException( Messages.getMessage( "WMPS_ERROR_WRITING_MAP_TMP_FILE" ) );
281            }
282            if ( printMap.getLegend() ) {
283                try {
284                    BufferedImage legendImage = performGetLegend( config_layers, printMap, mapParams );
285                    saveImageToDisk( printMap, legendImage, "LEGEND" );
286                    LOG.logDebug( "Saved the legend image file to disk." );
287                } catch ( IOException e ) {
288                    LOG.logError( e.getMessage(), e );
289                    throw new PrintMapServiceException( Messages.getMessage( "WMPS_ERROR_WRITING_LEGEND_TMP_FILE" ) );
290                }
291            }
292            byte[] b = exportOutput( printMap, scaleDenominator, synchronous );
293    
294            return b;
295        }
296    
297        /**
298         * Build the Map image for the current PrintMap request. Vector and Raster layers are handled
299         * seperately.
300         * 
301         * @param config_layers
302         * @param printMap
303         * @param bbox
304         * @param width
305         * @param height
306         * @return BufferedImage
307         * @throws OGCWebServiceException
308         */
309        private BufferedImage performBuildMapImage( Map config_layers, PrintMap printMap, Envelope bbox, double width,
310                                                    double height )
311                                throws OGCWebServiceException {
312    
313            BufferedImage targetImage = new BufferedImage( (int) width, (int) height, BufferedImage.TYPE_INT_ARGB );
314            Graphics2D g = (Graphics2D) targetImage.getGraphics();
315    
316            if ( !printMap.getTransparent() ) {
317                g.setBackground( printMap.getBGColor() );
318            }
319    
320            handleLayerDatasources( config_layers, printMap, bbox, width, height, g );
321            g.dispose();
322    
323            return targetImage;
324        }
325    
326        /**
327         * Determines the datasource for each layer(vector, raster).
328         * 
329         * @param config_layers
330         * @param printMap
331         * @param bbox
332         * @param width
333         * @param height
334         * @param g
335         * @throws OGCWebServiceException
336         * @throws InconsistentRequestException
337         */
338        private void handleLayerDatasources( Map config_layers, PrintMap printMap, Envelope bbox, double width,
339                                             double height, Graphics g )
340                                throws OGCWebServiceException, InconsistentRequestException {
341    
342            double px = PIXELSIZE / configuration.getDeegreeParams().getPrintMapParam().getTargetResolution();
343            CoordinateSystem crs;
344            try {
345                crs = CRSFactory.create( printMap.getSRS() );
346            } catch ( UnknownCRSException e1 ) {
347                throw new InconsistentRequestException( e1.getMessage() );
348            }
349            double scale = MapUtils.calcScale( (int) width, (int) height, bbox, crs, px );
350            GetMap.Layer[] printMapLayers = printMap.getLayers();
351            for ( int i = 0; i < printMapLayers.length; i++ ) {
352                String name = printMapLayers[i].getName();
353    
354                Layer configLayer = (Layer) config_layers.get( name );
355                ScaleHint scaleHint = configLayer.getScaleHint();
356                if ( scale >= scaleHint.getMin() && scale < scaleHint.getMax() ) {
357                    String type = determineDatasourceType( configLayer, scale );
358                    if ( type != null && "vector".equalsIgnoreCase( type ) ) {
359    
360                        GetMap.Layer[] lay = null;
361                        if ( ( printMapLayers.length - i ) > 1 ) {
362                            lay = getContinuousVectorLayer( config_layers, printMapLayers, scale, i );
363                        } else {
364                            lay = new GetMap.Layer[] { printMapLayers[i] };
365                        }
366                        try {
367                            handleVectorDataLayer( printMap, bbox, width, height, g, lay );
368                        } catch ( OGCWebServiceException e ) {
369                            LOG.logError( e.getMessage(), e );
370                            throw new OGCWebServiceException( Messages.getMessage( "WMPS_ERROR_HANDLING_GETMAP", name ) );
371                        }
372                        // Skip the number of layers already handled.
373                        if ( lay.length != 1 ) {
374                            i = i + ( lay.length - 1 );
375                        }
376                    } else {
377                        // must be a raster data layer
378                        GetMap.Layer[] lay = new GetMap.Layer[] { printMapLayers[i] };
379                        try {
380                            handleRasterDataLayer( printMap, bbox, width, height, g, lay );
381                        } catch ( OGCWebServiceException e ) {
382                            LOG.logError( e.getMessage(), e );
383                            throw new OGCWebServiceException( Messages.getMessage( "WMPS_ERROR_HANDLING_GETMAP", name ) );
384                        }
385                    }
386                } else {
387                    String s = StringTools.concat( 100, "No Datasource available for layer: ", name, " at scale: ", scale );
388                    LOG.logInfo( s );
389                }
390            }
391    
392        }
393    
394        /**
395         * returns an array of layers that:
396         * <ul>
397         * <li>a) made of vector data
398         * <li>b) are continous in the requested list of layers
399         * </ul>
400         * 
401         * @param config_layers
402         * @param printMapLayers
403         * @param mapScale
404         *            scale of the entire map
405         * @param i
406         * @return Layer[]
407         */
408        private GetMap.Layer[] getContinuousVectorLayer( Map config_layers, GetMap.Layer[] printMapLayers, double mapScale,
409                                                         int i ) {
410    
411            List<GetMap.Layer> layers = new ArrayList<GetMap.Layer>( printMapLayers.length );
412            int counter = 0;
413            for ( ; i < printMapLayers.length; i++ ) {
414                String name = printMapLayers[i].getName();
415                Layer configLayer = (Layer) config_layers.get( name );
416                String type = determineDatasourceType( configLayer, mapScale );
417                if ( "vector".equals( type ) ) {
418                    layers.add( counter, printMapLayers[i] );
419                    counter++;
420                } else {
421                    break;
422                }
423            }
424    
425            return layers.toArray( new GetMap.Layer[layers.size()] );
426        }
427    
428        /**
429         * Perform the GetMap request for vector layers.
430         * 
431         * @param printMap
432         * @param bbox
433         * @param width
434         * @param height
435         * @param g
436         * @param lay
437         * @throws OGCWebServiceException
438         */
439        private void handleVectorDataLayer( PrintMap printMap, Envelope bbox, double width, double height, Graphics g,
440                                            GetMap.Layer[] lay )
441                                throws OGCWebServiceException {
442    
443            URL sldURL = null;
444            StyledLayerDescriptor sld = null;
445    
446            GetMap getMap = GetMap.create( printMap.getVersion(), printMap.getId(), lay, null, null, this.MIMETYPE,
447                                           (int) width, (int) height, printMap.getSRS(), bbox, printMap.getTransparent(),
448                                           printMap.getBGColor(), this.EXCEPTION, null, sldURL, sld,
449                                           printMap.getVendorSpecificParameters() );
450            DefaultGetMapHandler gmh = new DefaultGetMapHandler( this.configuration, getMap );
451            gmh.performGetMap( g );
452    
453        }
454    
455        /**
456         * Perform the GetMap request for each raster layer. Here the raster layer is divided into tiles
457         * for memory handling efficiency.
458         * 
459         * @param printMap
460         * @param bbox
461         * @param width
462         * @param height
463         * @param g
464         * @param lay
465         * @throws OGCWebServiceException
466         */
467        private void handleRasterDataLayer( PrintMap printMap, Envelope bbox, double width, double height, Graphics g,
468                                            GetMap.Layer[] lay )
469                                throws OGCWebServiceException {
470    
471            // Get Map (missing) parameters.
472            Values elevation = null;
473            Map<String, Values> sampleDimension = null;
474            Values time = null;
475            URL sldURL = null;
476            StyledLayerDescriptor sld = null;
477    
478            boolean xRemainder = false;
479            int wtx = (int) width % (int) this.TILE_MAX_SIZE;
480            int nkx = (int) width / (int) this.TILE_MAX_SIZE;
481            if ( wtx > 0 ) {
482                xRemainder = true;
483                nkx++;
484            }
485    
486            boolean yRemainder = false;
487            int wty = (int) height % (int) this.TILE_MAX_SIZE;
488            int nky = (int) height / (int) this.TILE_MAX_SIZE;
489            if ( wty > 0 ) {
490                yRemainder = true;
491                nky++;
492            }
493    
494            WorldToScreenTransform trans = new WorldToScreenTransform( bbox.getMin().getX(), bbox.getMin().getY(),
495                                                                       bbox.getMax().getX(), bbox.getMax().getY(), 0d, 0d,
496                                                                       width - 1, height - 1 );
497    
498            for ( int x = 0; x < nkx; x++ ) {
499                double tileWidth = this.TILE_MAX_SIZE;
500                if ( xRemainder ) {
501                    if ( x == nkx - 1 ) {
502                        tileWidth = wtx;
503                    }
504                }
505                for ( int y = 0; y < nky; y++ ) {
506                    double tileHeight = this.TILE_MAX_SIZE;
507                    if ( yRemainder ) {
508                        if ( y == nky - 1 ) {
509                            tileHeight = wty;
510                        }
511                    }
512                    BufferedImage bi = new BufferedImage( (int) tileWidth, (int) tileHeight, BufferedImage.TYPE_INT_ARGB );
513                    Graphics tileg = bi.getGraphics();
514                    // calc bbox
515                    Envelope bb = calculateTileBBOX( trans, x, y, tileWidth, tileHeight, bbox.getCoordinateSystem() );
516                    // create GetMap
517                    GetMap getMap = GetMap.create( printMap.getVersion(), printMap.getId(), lay, elevation,
518                                                   sampleDimension, this.MIMETYPE, (int) tileWidth, (int) tileHeight,
519                                                   printMap.getSRS(), bb, printMap.getTransparent(), printMap.getBGColor(),
520                                                   this.EXCEPTION, time, sldURL, sld,
521                                                   printMap.getVendorSpecificParameters() );
522    
523                    // performGetMap( tileg );
524                    DefaultGetMapHandler gmh = new DefaultGetMapHandler( this.configuration, getMap );
525                    gmh.performGetMap( tileg );
526                    tileg.dispose();
527                    g.drawImage( bi, (int) Math.round( x * this.TILE_MAX_SIZE ),
528                                 (int) Math.round( y * this.TILE_MAX_SIZE ), (int) Math.round( tileWidth ),
529                                 (int) Math.round( tileHeight + 1 ), null );
530                }
531            }
532    
533        }
534    
535        /**
536         * Calculate the tile BBOX for the raster datalayer.
537         * 
538         * @param trans
539         * @param x
540         * @param y
541         * @param tileWidth
542         * @param tileHeight
543         * @param crs
544         * @return Envelope
545         */
546        private Envelope calculateTileBBOX( WorldToScreenTransform trans, int x, int y, double tileWidth,
547                                            double tileHeight, CoordinateSystem crs ) {
548    
549            double x1 = x * this.TILE_MAX_SIZE;
550            double y1 = y * this.TILE_MAX_SIZE;
551            double x2 = x1 + tileWidth;
552            double y2 = y1 + tileHeight;
553    
554            double minX = trans.getSourceX( x1 );
555            double maxX = trans.getSourceX( x2 );
556            double minY = trans.getSourceY( y2 );
557            double maxY = trans.getSourceY( y1 );
558    
559            return GeometryFactory.createEnvelope( minX, minY, maxX, maxY, crs );
560        }
561    
562        /**
563         * Parses the Layer datastores to determine the type of the layer. Layers having a vector
564         * datasource as well as a raster datasource for the passed mapScale will be treated as raster
565         * layers
566         * 
567         * @param layer
568         * @param mapScale
569         *            scale of the entire map
570         * @return String either raster, vector or nodatasource
571         */
572        private String determineDatasourceType( Layer layer, double mapScale ) {
573    
574            AbstractDataSource[] ads = layer.getDataSource();
575            String type = null;
576    
577            boolean[] mixed = new boolean[] { false, false };
578            for ( int i = 0; i < ads.length; i++ ) {
579                ScaleHint scaleHint = ads[i].getScaleHint();
580                if ( mapScale >= scaleHint.getMin() && mapScale < scaleHint.getMax() ) {
581                    if ( ( ads[i] instanceof RemoteWMSDataSource ) || ( ads[i] instanceof RemoteWCSDataSource )
582                         || ( ads[i] instanceof LocalWCSDataSource ) ) {
583                        type = "raster";
584                        mixed[0] = true;
585                    } else {
586                        type = "vector";
587                        mixed[1] = true;
588                    }
589                }
590            }
591            if ( mixed[0] && mixed[1] ) {
592                // Layers having a vector datasource as well as a raster datasource
593                // for the passed mapScale will be treated as raster layers
594                type = "raster";
595            }
596    
597            return type;
598        }
599    
600        /**
601         * Retrieve the legend images from the URL given in the configuration layers.
602         * 
603         * 
604         * @param layerDefs
605         * @param printMap
606         * @param mapParams
607         * @return BufferedImage
608         * @throws OGCWebServiceException
609         * @throws InconsistentRequestException
610         */
611        private BufferedImage performGetLegend( Map layerDefs, PrintMap printMap, int[] mapParams ) {
612    
613            GetMap.Layer[] layers = printMap.getLayers();
614    
615            Map<String, Image> legendImg = new HashMap<String, Image>( layers.length );
616            int height = 0;
617            int maxWidth = 0;
618            for ( int i = 0; i < layers.length; i++ ) {
619                String name = layers[i].getName();
620                String styleName = layers[i].getStyleName();
621    
622                Layer configLayer = (Layer) layerDefs.get( name );
623                Style style = configLayer.getStyleResource( styleName );
624                LegendURL[] lu = style.getLegendURL();
625                if ( lu != null && lu.length > 0 ) {
626                    int k = 0;
627                    boolean drawn = false;
628                    while ( k < lu.length && !drawn ) {
629                        URL url = lu[k++].getOnlineResource();                    
630                        try {
631                            Image img = ImageUtils.loadImage( url );
632                            legendImg.put( name, img );
633                            drawn = true;
634                        } catch ( IOException e ) {
635                            // we do not throw the exception bacause there are maybe
636                            // further URLs we can try and even if not the user will
637                            // be informed by a special legend symbol that no correct
638                            // symbol can be accessed
639                            LOG.logError( "can not access LegendURL: " + url, e );
640                        } catch (Exception e) {
641                            LOG.logError( "can not read image from LegendURL: " + url, e );
642                        }
643                    }
644                    if ( !drawn ) {
645                        // if legend URL(s) are defined but none of them can
646                        // be accessed
647                        String s = StringTools.concat( 100, "no legend URL accessable for layer: ", name, "; style: ",
648                                                       styleName, " using dummy legend image" );
649                        LOG.logError( s );
650                        BufferedImage img = drawMissingLegendURLImage( s );
651                        legendImg.put( name, img );
652                    }
653                } else {
654                    // if no legend URL has been defined which probably is the case
655                    // for WMS no supporting GetLegendGraphic operation
656                    String s = StringTools.concat( 100, "no legend URL available for layer: ", name, "; style: ",
657                                                   styleName, " using dummy legend image" );
658                    LOG.logError( s );
659                    BufferedImage img = drawMissingLegendURLImage( s );
660                    legendImg.put( name, img );
661                }
662                // update all over legend height and width
663                BufferedImage img = (BufferedImage) legendImg.get( name );
664                if ( img.getWidth() > maxWidth ) {
665                    maxWidth = img.getWidth();
666                }
667                height += img.getHeight();
668            }
669    
670            // depending on the size of the legend all legend symbols must scaled by
671            // the same factor to fit the legend size defined in the current template
672            double dh = calcDeltaLegend( mapParams[2], mapParams[3], height, maxWidth );
673    
674            // create an empty basic image as target for painting all legend symbols
675            BufferedImage actualLegendImage = null;
676            if ( mapParams[2] > 0 && mapParams[3] > 0 ) {
677                actualLegendImage = new BufferedImage( mapParams[2], mapParams[3], BufferedImage.TYPE_INT_ARGB );
678        
679                Graphics2D g = (Graphics2D) actualLegendImage.getGraphics();
680        
681                int y = 0;
682                for ( int i = layers.length; i > 0; i-- ) {
683                    // draw all legend symbols in correct order
684                    String name = layers[i - 1].getName();
685                    BufferedImage img = scaleImage( (BufferedImage) legendImg.get( name ), dh );
686                    g.drawImage( img, 0, y, null );
687                    y += img.getHeight();
688                }
689        
690                g.dispose();
691            } else {
692                // create empty legend image if size is not valid
693                actualLegendImage = new BufferedImage( 10, 10, BufferedImage.TYPE_INT_ARGB );
694            }
695    
696            return actualLegendImage;
697        }
698    
699        private BufferedImage drawMissingLegendURLImage( String text ) {
700            BufferedImage img = new BufferedImage( 550, 50, BufferedImage.TYPE_INT_ARGB );
701            Graphics g = img.getGraphics();
702            g.setColor( Color.YELLOW );
703            g.fillRect( 0, 0, img.getWidth(), img.getHeight() );
704            g.setColor( Color.RED );
705            g.drawString( text, 10, 20 );
706            g.dispose();
707            return img;
708        }
709    
710        /**
711         * calculates factor for resizing legend images
712         * 
713         * @param legendWidth
714         *            The width of the legend area
715         * @param legendHeight
716         *            The height of the legend area
717         * @param height
718         *            The height of all legends put together
719         * @param maxWidth
720         *            The width of the wides legend
721         * @return Returns the factor for resizing legend images
722         */
723        private double calcDeltaLegend( int legendWidth, int legendHeight, int height, int maxWidth ) {
724            double dh = legendHeight / (double) height;
725            double dw = legendWidth / (double) maxWidth;
726            if ( dw < dh ) {
727                return dw;
728            }
729            return dh;
730        }
731    
732        /**
733         * Scale Image.
734         * 
735         * @param image
736         * @param ratio
737         * @return BufferedImage
738         */
739        private BufferedImage scaleImage( BufferedImage image, double ratio ) {
740    
741            AffineTransform tx = new AffineTransform();
742            tx.scale( ratio, ratio );
743            AffineTransformOp op = new AffineTransformOp( tx, AffineTransformOp.TYPE_BILINEAR );
744    
745            return op.filter( image, null );
746        }
747    
748        /**
749         * Save the GetMap image to the disk.
750         * 
751         * @param printMap
752         * @param image
753         * @param type
754         * @throws IOException
755         */
756        private void saveImageToDisk( PrintMap printMap, BufferedImage image, String type )
757                                throws IOException {
758    
759            String fileName = null;
760            String templateName = printMap.getTemplate();
761            if ( type.equalsIgnoreCase( "MAP" ) ) {
762                fileName = StringTools.concat( 200, "Map_", templateName, '_', printMap.getId(), this.FORMAT );
763            } else if ( type.equalsIgnoreCase( "LEGEND" ) ) {
764                fileName = StringTools.concat( 200, "Legend_", templateName, '_', printMap.getId(), this.FORMAT );
765            }
766    
767            PrintMapParam printMapParam = this.configuration.getDeegreeParams().getPrintMapParam();
768            String path = printMapParam.getPlotImageDir() + '/' + fileName;
769            URL downloadDirectory = new URL( path );
770    
771            try {
772                ImageUtils.saveImage( image, new File( downloadDirectory.toURI() ), 1 );
773            } catch ( URISyntaxException e ) {
774                // should never happen because each valid URL is a valid URI too
775                LOG.logError( e.getMessage(), e );
776            }
777    
778        }
779    
780        /**
781         * Use JasperReports to create a pdf file. The Jasper Template will be loaded and a dynamic link
782         * will be created to the image on the disk.
783         * 
784         * @param printMap
785         * @param scaleDenominator
786         * @param synchronous
787         * @return byte[]
788         * @throws PrintMapServiceException
789         */
790        private byte[] exportOutput( PrintMap printMap, int scaleDenominator, boolean synchronous )
791                                throws PrintMapServiceException {
792    
793            // generate a file using JasperReports.
794            byte[] b = null;
795            try {
796                JasperPrint print = fillJasperTemplate( printMap, scaleDenominator );
797                b = doJasperPrintExport( printMap, print, synchronous );
798            } catch ( Exception e ) {
799                LOG.logError( e.getMessage(), e );
800                throw new PrintMapServiceException( Messages.getMessage( "WMPS_ERROR_SAVING_PDF" ) );
801            }
802    
803            return b;
804        }
805    
806        /**
807         * Open the JasperAPI to process the PrintMap request.
808         * 
809         * @param printMap
810         * @param scaleDenominator
811         * @return JasperPrint
812         * @throws InconsistentRequestException
813         * @throws JRException
814         * @throws MalformedURLException
815         */
816        private JasperPrint fillJasperTemplate( PrintMap printMap, int scaleDenominator )
817                                throws JRException, MalformedURLException {
818    
819            URL templatePath = null;
820            try {
821                String templateName = printMap.getTemplate();
822                templatePath = getTemplatePath( templateName, true );
823            } catch ( IOException e ) {
824                LOG.logError( e.getMessage(), e );
825                throw new MalformedURLException( Messages.getMessage( "WMPS_ERROR_CREATING_TEMPLATEPATH",
826                                                                      printMap.getTemplate() ) );
827            }
828    
829            Map<String, Object> parameters = new HashMap<String, Object>();
830            URL mapImagePath = getResultImagePath( printMap, "Map" );
831            parameters.put( "MAP", mapImagePath.getFile() );
832            if ( printMap.getLegend() ) {
833                URL legendImagePath = getResultImagePath( printMap, "Legend" );
834                parameters.put( "LEGEND", legendImagePath.getFile() );
835            }
836    
837            String scale = "1:" + scaleDenominator;
838    
839            if ( printMap.getScaleBar() == true ) {
840                parameters.put( "SCALE", scale );
841            }
842    
843            TextArea[] textAreas = printMap.getTextAreas();
844            if ( textAreas != null && textAreas.length > 0 ) {
845                for ( int i = 0; i < textAreas.length; i++ ) {
846                    TextArea textArea = textAreas[i];
847                    LOG.logDebug( "Names and text fields entered, extracted." );
848                    String name = textArea.getName();
849                    String text = textArea.getText();
850                    if ( name != null ) {
851                        LOG.logDebug( "If name is not null, allocate it to the hashmap 'parameters' in uppercase." );
852                        parameters.put( name.toUpperCase(), text );
853                    }
854                }
855            } else {
856                String title = printMap.getTitle();
857                if ( title != null ) {
858                    parameters.put( "TITLE", title );
859                }
860                String copyright = printMap.getCopyright();
861                if ( copyright != null ) {
862                    parameters.put( "COPYRIGHT", copyright );
863                }
864                String note = printMap.getNote();
865                if ( note != null ) {
866                    parameters.put( "NOTE", note );
867                }
868            }
869    
870            JasperPrint print = null;
871            try {
872                print = JasperFillManager.fillReport( templatePath.getFile(), parameters, new JREmptyDataSource() );
873            } catch ( JRException e ) {
874                LOG.logError( e.getMessage(), e );
875                throw new JRException( Messages.getMessage( "WMPS_ERROR_BUILDING_TEMPLATE", templatePath ) );
876            }
877    
878            return print;
879        }
880    
881        /**
882         * Retrieve the result map image file path for the current request.
883         * 
884         * @param printMap
885         * @param type
886         * @return URL
887         * @throws MalformedURLException
888         */
889        private URL getResultImagePath( PrintMap printMap, String type )
890                                throws MalformedURLException {
891    
892            String templateName = printMap.getTemplate();
893            String fileName = type + "_" + templateName + "_" + printMap.getId() + this.FORMAT;
894            PrintMapParam printMapParam = this.configuration.getDeegreeParams().getPrintMapParam();
895            String path = printMapParam.getPlotImageDir() + '/' + fileName;
896            URL imagePath = new URL( path );
897    
898            return imagePath;
899    
900        }
901    
902        /**
903         * Print the layer to a the specified format.
904         * 
905         * @param printMap
906         * @param print
907         * @param synchronous
908         * @return byte[]
909         * @throws JRException
910         * @throws IOException
911         */
912        private byte[] doJasperPrintExport( PrintMap printMap, JasperPrint print, boolean synchronous )
913                                throws JRException, IOException {
914    
915            String format = this.configuration.getDeegreeParams().getPrintMapParam().getFormat();
916            String templateName = printMap.getTemplate();
917            String filename = StringTools.concat( 200, format, '_', templateName, '_', printMap.getId(), '.', format );
918            String directory = this.configuration.getDeegreeParams().getPrintMapParam().getPlotDirectory();
919    
920            URL downloadFile = new URL( directory + '/' + filename );
921    
922            byte[] b = null;
923            try {
924                if ( synchronous ) {
925                    b = doSynchronousProcessing( print, format );
926                } else {
927                    doSaveResultDocument( print, format, downloadFile );
928                    createMailLink( filename );
929                }
930            } catch ( JRException e ) {
931                LOG.logError( e.getMessage(), e );
932                throw new JRException( Messages.getMessage( "WMPS_ERROR_PRINTING_REPORT", format, downloadFile.getFile() ) );
933            }
934    
935            return b;
936        }
937    
938        /**
939         * Create a mail link to be sent to the user email address. The mail link allows the user to
940         * open the pdf document for viewing and downloading purposes. Here 2 cases are taken into
941         * consideration
942         * <ul>
943         * <li> An authentification servlet link will be sent to the client.
944         * <li> A direct access to the clients data file.
945         * </ul>
946         * 
947         * @param printMap
948         * @param filename
949         */
950        private void createMailLink( String filename ) {
951    
952            PrintMapParam pmp = this.configuration.getDeegreeParams().getPrintMapParam();
953            String onlineResource = pmp.getOnlineResource();
954    
955            String template = pmp.getMailTextTemplate();
956            this.message = MessageFormat.format( template, new Object[] { onlineResource, filename.trim() } );
957    
958        }
959    
960        /**
961         * Save the result document using the JasperExportManager to the file specified.
962         * 
963         * @param print
964         * @param format
965         * @param downloadDirectory
966         * @throws JRException
967         */
968        private void doSaveResultDocument( JasperPrint print, String format, URL downloadDirectory )
969                                throws JRException {
970    
971            if ( format.equalsIgnoreCase( "pdf" ) ) {
972                LOG.logDebug( "Exporting as pdf to file " + downloadDirectory.getFile() );
973                JasperExportManager.exportReportToPdfFile( print, downloadDirectory.getFile() );
974            } else if ( format.equalsIgnoreCase( "html" ) ) {
975                LOG.logDebug( "Exporting as html to file " + downloadDirectory.getFile() );
976                JasperExportManager.exportReportToHtmlFile( print, downloadDirectory.getFile() );
977            } else if ( format.equalsIgnoreCase( "xml" ) ) {
978                LOG.logDebug( "Exporting as xml to file " + downloadDirectory.getFile() );
979                JasperExportManager.exportReportToXmlFile( print, downloadDirectory.getFile(), false );
980            }
981    
982        }
983    
984        /**
985         * Export the result document to a stream to be returend to the waiting client.
986         * 
987         * @param print
988         * @param format
989         * @return byte[]
990         * @throws JRException
991         */
992        private byte[] doSynchronousProcessing( JasperPrint print, String format )
993                                throws JRException {
994    
995            byte[] b;
996            ByteArrayOutputStream bos = new ByteArrayOutputStream( 100000 );
997            if ( format.equalsIgnoreCase( "pdf" ) ) {
998                JasperExportManager.exportReportToPdfStream( print, bos );
999            } else if ( format.equalsIgnoreCase( "xml" ) ) {
1000                JasperExportManager.exportReportToXmlStream( print, bos );
1001            }
1002            b = bos.toByteArray();
1003    
1004            return b;
1005        }
1006    
1007        /**
1008         * Retrieve PrintMap request layers from the WMPSConfiguration file. Counter check if the layer
1009         * has been defined and also get the type of datasource used by the layer.
1010         * 
1011         * @param printMap
1012         * @return Map key-> layer name, value -> configLayer
1013         * @throws PrintMapServiceException
1014         */
1015        private Map retrieveLayersFromConfig( PrintMap printMap )
1016                                throws PrintMapServiceException {
1017    
1018            GetMap.Layer[] requestedLayers = printMap.getLayers();
1019            Map<String, Layer> layers = new HashMap<String, Layer>();
1020            for ( int i = 0; i < requestedLayers.length; i++ ) {
1021                GetMap.Layer layer = requestedLayers[i];
1022    
1023                Layer configLayer = this.configuration.getLayer( layer.getName() );
1024                if ( configLayer != null ) {
1025                    layers.put( layer.getName(), configLayer );
1026                } else {
1027                    throw new PrintMapServiceException( Messages.getMessage( "WMPS_UNKNOWN_LAYER", layer.getName() ) );
1028                }
1029            }
1030    
1031            return layers;
1032        }
1033    
1034        /**
1035         * Create a bounding box if no bounding box has been passed along with the PrintMap request.
1036         * 
1037         * @param center
1038         * @param scaleDenominator
1039         * @param mapWidth
1040         * @param mapHeight
1041         * @return Envelope
1042         */
1043        private Envelope createBBOX( Point center, int scaleDenominator, double mapWidth, double mapHeight ) {
1044    
1045            // screen -> world projection
1046            double pixelSize = PIXELSIZE / configuration.getDeegreeParams().getPrintMapParam().getTargetResolution();
1047            double w2 = ( scaleDenominator * pixelSize * mapWidth ) / 2d;
1048            double x1 = center.getX() - w2;
1049            double x2 = center.getX() + w2;
1050            w2 = ( scaleDenominator * pixelSize * mapHeight ) / 2d;
1051            double y1 = center.getY() - w2;
1052            double y2 = center.getY() + w2;
1053    
1054            Envelope bbox = GeometryFactory.createEnvelope( x1, y1, x2, y2, center.getCoordinateSystem() );
1055    
1056            return bbox;
1057    
1058        }
1059    
1060        /**
1061         * Returns the current configuration used to initialise the PrintMapHandler.
1062         * 
1063         * @return WMPSConfiguration
1064         */
1065        public WMPSConfiguration getConfiguration() {
1066            return this.configuration;
1067        }
1068    
1069        /**
1070         * Parse the Template and retrieve the page width, page height information.
1071         * 
1072         * @param templateName
1073         * @return int[]
1074         * @throws PrintMapServiceException
1075         * @throws IOException
1076         */
1077        private int[] getMapParamsFromTemplate( String templateName )
1078                                throws PrintMapServiceException, IOException {
1079    
1080            int[] mapParams = null;
1081            URL file = null;
1082            // try {
1083            boolean isCompiled = false;
1084            file = getTemplatePath( templateName, isCompiled );
1085            if ( file != null ) {
1086                Document dom = null;
1087                try {
1088                    InputStream is = file.openStream();
1089                    int c = 0;
1090                    StringBuffer sb = new StringBuffer( 10000 );
1091                    while ( ( c = is.read() ) > -1 ) {
1092                        sb.append( (char) c );
1093                    }
1094                    is.close();
1095                    // hack to ensure reporting engine is working even if the
1096                    // jasper-report server is not available
1097                    String s = StringTools.replace(
1098                                                    sb.toString(),
1099                                                    "<!DOCTYPE jasperReport PUBLIC \"//JasperReports//DTD Report Design//EN\" \"http://jasperreports.sourceforge.net/dtds/jasperreport.dtd\">",
1100                                                    "", false );
1101    
1102                    dom = XMLTools.parse( new StringReader( s ) );
1103                    // XMLFragment xml = new XMLFragment( file );
1104                    mapParams = parseImageNodes( dom.getDocumentElement() );
1105                } catch ( Exception e ) {
1106                    LOG.logError( e.getMessage(), e );
1107                    throw new PrintMapServiceException( Messages.getMessage( "WMPS_ERROR_PARSING_TEMPLATE", file ) );
1108                }
1109            }
1110    
1111            return mapParams;
1112        }
1113    
1114        /**
1115         * Return the url for the current (jasper reports) template.
1116         * 
1117         * @param templateName
1118         * @param isCompiled
1119         * @return URL
1120         * @throws PrintMapServiceException
1121         */
1122        private URL getTemplatePath( String templateName, boolean isCompiled )
1123                                throws IOException {
1124    
1125            if ( isCompiled ) {
1126                templateName = templateName + ".jasper";
1127            } else {
1128                templateName = templateName + ".jrxml";
1129            }
1130    
1131            PrintMapParam printMapParam = this.configuration.getDeegreeParams().getPrintMapParam();
1132            URL fileURL = new URL( printMapParam.getTemplateDirectory() + templateName );
1133    
1134            LOG.logDebug( "Retrieved the template file url. " + fileURL );
1135    
1136            return fileURL;
1137        }
1138    
1139        /**
1140         * Gets the Image node defined to hold the 'Map' and the node defined to hold the 'Legend'.
1141         * 
1142         * @param root
1143         * @return List
1144         * @throws PrintMapServiceException
1145         */
1146        private int[] parseImageNodes( Element root )
1147                                throws PrintMapServiceException {
1148    
1149            int[] mapParams = new int[4];
1150            int mapCount = 0;
1151            try {
1152    
1153                List<Node> images = XMLTools.getRequiredNodes( root, "detail/band/image", null );
1154                for ( int i = 0; i < images.size(); i++ ) {
1155                    Node image = images.get( i );
1156                    // e.g. $P{MAP}
1157                    String value = XMLTools.getRequiredNodeAsString( image, "imageExpression", null );
1158                    int idx = value.indexOf( "{" );
1159                    if ( idx != -1 ) {
1160    
1161                        String tmp = value.substring( idx + 1, value.length() - 1 );
1162                        Element reportElement = (Element) XMLTools.getRequiredNode( image, "reportElement", null );
1163                        String width = reportElement.getAttribute( "width" );
1164                        String height = reportElement.getAttribute( "height" );
1165    
1166                        double res = configuration.getDeegreeParams().getPrintMapParam().getTargetResolution();
1167                        // Templates created by iReport assumes a resolution of 72 dpi
1168                        if ( tmp.startsWith( "MAP" ) ) {
1169                            mapParams[0] = (int) ( Integer.parseInt( width ) / 72d * res );
1170                            mapParams[1] = (int) ( Integer.parseInt( height ) / 72d * res );
1171                            mapCount = mapCount + 1;
1172                        } else if ( tmp.startsWith( "LEGEND" ) ) {
1173                            mapParams[2] = (int) ( Integer.parseInt( width ) / 72d * res );
1174                            mapParams[3] = (int) ( Integer.parseInt( height ) / 72d * res );
1175                        }
1176                    }
1177                }
1178                if ( ( mapCount == 0 ) || ( mapCount > 1 ) ) {
1179                    throw new PrintMapServiceException( Messages.getMessage( "WMPS_TOO_MANY_MAPAREAS", mapCount ) );
1180                }
1181            } catch ( XMLParsingException e ) {
1182                LOG.logError( e.getMessage(), e );
1183                throw new PrintMapServiceException( Messages.getMessage( "WMPS_INVALID_JASPER_TEMPLATE" ) );
1184            }
1185    
1186            return mapParams;
1187        }
1188    
1189    }