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