001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.3_testing/src/org/deegree/portal/standard/wms/control/SimplePrintListener.java $
002    /*----------------------------------------------------------------------------
003     This file is part of deegree, http://deegree.org/
004     Copyright (C) 2001-2009 by:
005       Department of Geography, University of Bonn
006     and
007       lat/lon GmbH
008    
009     This library is free software; you can redistribute it and/or modify it under
010     the terms of the GNU Lesser General Public License as published by the Free
011     Software Foundation; either version 2.1 of the License, or (at your option)
012     any later version.
013     This library is distributed in the hope that it will be useful, but WITHOUT
014     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
015     FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
016     details.
017     You should have received a copy of the GNU Lesser General Public License
018     along with this library; if not, write to the Free Software Foundation, Inc.,
019     59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020    
021     Contact information:
022    
023     lat/lon GmbH
024     Aennchenstr. 19, 53177 Bonn
025     Germany
026     http://lat-lon.de/
027    
028     Department of Geography, University of Bonn
029     Prof. Dr. Klaus Greve
030     Postfach 1147, 53001 Bonn
031     Germany
032     http://www.geographie.uni-bonn.de/deegree/
033    
034     e-mail: info@deegree.org
035    ----------------------------------------------------------------------------*/
036    
037    package org.deegree.portal.standard.wms.control;
038    
039    import java.awt.Graphics;
040    import java.awt.geom.Rectangle2D;
041    import java.awt.image.BufferedImage;
042    import java.io.File;
043    import java.net.URL;
044    import java.util.ArrayList;
045    
046    import javax.servlet.http.HttpServletRequest;
047    import javax.servlet.http.HttpSession;
048    
049    import org.deegree.datatypes.QualifiedName;
050    import org.deegree.enterprise.control.RPCMember;
051    import org.deegree.enterprise.control.RPCMethodCall;
052    import org.deegree.enterprise.control.RPCParameter;
053    import org.deegree.enterprise.control.RPCStruct;
054    import org.deegree.enterprise.control.RPCWebEvent;
055    import org.deegree.framework.log.ILogger;
056    import org.deegree.framework.log.LoggerFactory;
057    import org.deegree.framework.util.ImageUtils;
058    import org.deegree.framework.util.StringTools;
059    import org.deegree.i18n.Messages;
060    import org.deegree.model.crs.CoordinateSystem;
061    import org.deegree.model.spatialschema.Envelope;
062    import org.deegree.model.spatialschema.GeometryFactory;
063    import org.deegree.model.spatialschema.Point;
064    import org.deegree.ogcbase.ImageURL;
065    import org.deegree.ogcwebservices.OWSUtils;
066    import org.deegree.ogcwebservices.getcapabilities.OGCCapabilities;
067    import org.deegree.ogcwebservices.wms.capabilities.LegendURL;
068    import org.deegree.ogcwebservices.wms.capabilities.WMSCapabilities;
069    import org.deegree.ogcwebservices.wms.capabilities.WMSCapabilitiesDocument;
070    import org.deegree.ogcwebservices.wms.capabilities.WMSCapabilitiesDocumentFactory;
071    import org.deegree.owscommon_new.HTTP;
072    import org.deegree.owscommon_new.Operation;
073    import org.deegree.portal.Constants;
074    import org.deegree.portal.PortalException;
075    import org.deegree.portal.common.control.AbstractSimplePrintListener;
076    import org.deegree.portal.context.ContextException;
077    import org.deegree.portal.context.Format;
078    import org.deegree.portal.context.FormatList;
079    import org.deegree.portal.context.General;
080    import org.deegree.portal.context.Layer;
081    import org.deegree.portal.context.LayerList;
082    import org.deegree.portal.context.Server;
083    import org.deegree.portal.context.Style;
084    import org.deegree.portal.context.StyleList;
085    import org.deegree.portal.context.ViewContext;
086    
087    /**
088     * This class prints the View context. It inherits from AbstractSimplePrintListner and implement the abstract method
089     * getViewContext
090     *
091     * TODO The methods changeBBox(), changeLayerList(), extractBBox() are already implemented in AbstractContextListner.
092     * The question now is wether to inherit from AbstractContextListner instead of AbstractListner?
093     *
094     * @author <a href="mailto:mays@lat-lon.de">Judit Mays</a>
095     * @author last edited by: $Author: mschneider $
096     *
097     * @version $Revision: 18195 $, $Date: 2009-06-18 17:55:39 +0200 (Do, 18. Jun 2009) $
098     */
099    public class SimplePrintListener extends AbstractSimplePrintListener {
100    
101        private static final ILogger LOG = LoggerFactory.getLogger( SimplePrintListener.class );
102    
103        private String missingImg = null;
104    
105        @Override
106        protected ViewContext getViewContext( RPCWebEvent rpc ) {
107    
108            init();
109            RPCMethodCall mc = rpc.getRPCMethodCall();
110            RPCParameter[] params = mc.getParameters();
111            RPCStruct struct = (RPCStruct) params[2].getValue();
112    
113            HttpSession session = ( (HttpServletRequest) getRequest() ).getSession();
114            ViewContext vc = (ViewContext) session.getAttribute( Constants.CURRENTMAPCONTEXT );
115            // change values: BBOX and Layer List
116            Envelope bbox = extractBBox( (RPCStruct) struct.getMember( "boundingBox" ).getValue(), null );
117            RPCMember[] layerList = ( (RPCStruct) struct.getMember( "layerList" ).getValue() ).getMembers();
118            try {
119                changeBBox( vc, bbox );
120                changeLayerList( vc, layerList );
121            } catch ( PortalException e ) {
122                LOG.logError( "An Error occured while trying to get the Viewcontext" );
123                return null;
124                // TODO This method implements its abstract method in AbstractSimplePrintListner
125                // Preferably if the abstract header could be changed to throw a adequate excpetion
126                // since this header is already implemented by another class
127            }
128            return vc;
129        }
130    
131        private void init() {
132    
133            String tmp = getInitParameter( "MISSING_IMAGE" );
134            if ( tmp != null ) {
135                missingImg = StringTools.concat( 200, getHomePath(), tmp );
136                LOG.logDebug( "MissingLegend PATH = ", missingImg );
137            }
138        }
139    
140        /**
141         * Convenience method to extract the boundig box from an rpc fragment.
142         *
143         * @param bboxStruct
144         *            the <code>RPCStruct</code> containing the bounding box. For example,
145         *            <code>&lt;member&gt;&lt;name&gt;boundingBox&lt;/name&gt;etc...</code>.
146         * @param crs
147         *            a coordinate system value, may be null.
148         * @return an envelope with the boundaries defined in the rpc structure
149         */
150        protected Envelope extractBBox( RPCStruct bboxStruct, CoordinateSystem crs ) {
151    
152            Double minx = (Double) bboxStruct.getMember( Constants.RPC_BBOXMINX ).getValue();
153            Double miny = (Double) bboxStruct.getMember( Constants.RPC_BBOXMINY ).getValue();
154            Double maxx = (Double) bboxStruct.getMember( Constants.RPC_BBOXMAXX ).getValue();
155            Double maxy = (Double) bboxStruct.getMember( Constants.RPC_BBOXMAXY ).getValue();
156    
157            Envelope bbox = GeometryFactory.createEnvelope( minx.doubleValue(), miny.doubleValue(), maxx.doubleValue(),
158                                                            maxy.doubleValue(), crs );
159            return bbox;
160        }
161    
162        /**
163         * changes the layer list of the ViewContext vc according to the information contained in the rpcLayerList
164         *
165         * @param vc
166         *            The original ViewContext where the changes will be applied to
167         * @param rpcLayerList
168         *            the current layerlist
169         * @throws PortalException
170         */
171        protected void changeLayerList( ViewContext vc, RPCMember[] rpcLayerList )
172                                throws PortalException {
173            LayerList layerList = vc.getLayerList();
174            ArrayList<Layer> nLayers = new ArrayList<Layer>( rpcLayerList.length );
175    
176            // this is needed to keep layer order
177            // order is correct in rpc call JavaScript, but get lost in translation...
178            LOG.logDebug( "Layerlist length: " + rpcLayerList.length );
179            for ( int i = 0; i < rpcLayerList.length; i++ ) {
180                String[] v = StringTools.toArray( (String) rpcLayerList[i].getValue(), "|", false );
181                String layerName = rpcLayerList[i].getName();
182    
183                String title = layerName;
184                if ( v.length > 5 ) {
185                    // See if there is a title field and use it if exists
186                    title = v[5];
187                }
188                boolean isQueryable = false;
189                if ( v.length > 6 ) {
190                    // see if there's a isQuerzable field and use it if exists
191                    isQueryable = v[6].equalsIgnoreCase( "true" );
192                }
193    
194                boolean isVisible = Boolean.valueOf( v[0] ).booleanValue();
195                LOG.logDebug( "Service url: " + v[4] );
196                // server address must be set!! otherwise an outdated layer might be taken.
197                Layer l = layerList.getLayer( layerName, v[4] );
198                if ( l != null ) {
199                    // needed to reconstruct new layer order
200                    // otherwise layer order is still from original context
201                    LOG.logDebug( StringTools.concat( 100, "Layer ", layerName, " is defined in WMC" ) );
202                    l.setHidden( !isVisible );
203                } else {
204                    LOG.logDebug( StringTools.concat( 100, "Layer ", layerName, " is undefined by the WMC" ) );
205                    if ( layerList.getLayers().length == 0 ) {
206                        // FIXME is this Exception Correct?
207                        throw new PortalException( Messages.getMessage( "IGEO_STD_CNTXT_ERROR_EMPTY_LAYERLIST" ) );
208                    }
209    
210                    Layer p = layerList.getLayers()[0];
211                    // a new layer must be created because it is not prsent in the current context.
212                    // This is the case if the client has loaded an additional WMS
213                    String[] tmp = StringTools.toArray( v[2], " ", false );
214                    try {
215                        v[4] = OWSUtils.validateHTTPGetBaseURL( v[4] );
216                        WMSCapabilitiesDocument doc = WMSCapabilitiesDocumentFactory.getWMSCapabilitiesDocument( new URL(
217                                                                                                                          v[4]
218                                                                                                                                                  + "request=GetCapabilities&service=WMS" ) );
219                        LOG.logDebug( "Service base url: " + v[4] );
220                        OGCCapabilities capa = doc.parseCapabilities();
221    
222                        URL url = getLegendURL( capa, layerName, null );
223                        LOG.logDebug( "Final obtained legend url: " + url.toExternalForm() );
224                        ImageURL tmpImg = p.getStyleList().getCurrentStyle().getLegendURL();
225                        LegendURL legendUrl = new LegendURL( tmpImg.getWidth(), tmpImg.getHeight(), tmpImg.getFormat(), url );
226                        Style style = new Style( "default", "", null, legendUrl, true );
227                        StyleList styleList = new StyleList( new org.deegree.portal.context.Style[] { style } );
228                        // end of changes
229                        Server server = new Server( v[3], tmp[1], tmp[0], new URL( v[4] ), capa );
230    
231                        Format[] formats = new Format[] { new Format( "image/png", true ) };
232                        FormatList fl = new FormatList( formats );
233                        l = new Layer( server, layerName, title, "", p.getSrs(), null, null, fl, styleList, isQueryable,
234                                       !isVisible, null );
235                    } catch ( Exception e ) {
236                        throw new PortalException( StringTools.stackTraceToString( e ) );
237                    }
238                }
239                nLayers.add( l );
240            }
241            try {
242                nLayers.trimToSize();
243                Layer[] ls = new Layer[nLayers.size()];
244                ls = nLayers.toArray( ls );
245                vc.setLayerList( new LayerList( ls ) );
246            } catch ( ContextException e ) {
247                throw new PortalException( Messages.getMessage( "IGEO_STD_CNTXT_ERROR_SET_LAYERLIST",
248                                                                StringTools.stackTraceToString( e.getStackTrace() ) ) );
249            }
250        }
251    
252        /**
253         * This method is called, when no legend image could be retreived from the information given in the WebMapContext
254         * document. The layer was probably added during runtime dynamically.
255         *
256         * It is attempted to get the legend url from style info in the WMS Capabilities, first with the passed style info,
257         * second for the default style, third for the only available style. (If the WMS capabilities holds more than one
258         * style, but no style was passed with the layer info, then one cannot know "the right style". Therefore no style is
259         * taken from WMS capabilities, in this case.)
260         *
261         * Then, it is attempted to get the legend image from a GetLegendGraphics request, if the WMS capabilities state,
262         * that this request is supported by the WMS.
263         *
264         * If all this fails, the image is taken from init param "missingImage".
265         *
266         * @param capa
267         * @param layerName
268         * @param styleName
269         * @return URL of the Legend
270         * @throws Exception
271         */
272        private URL getLegendURL( OGCCapabilities capa, String layerName, String styleName )
273                                throws Exception {
274    
275            // changes added to enable fetching the legend graphic through multiple methods
276            WMSCapabilities wmsCapa = (WMSCapabilities) capa;
277            org.deegree.ogcwebservices.wms.capabilities.Layer ogcLayer = wmsCapa.getLayer( layerName );
278            URL url = null;
279            if ( styleName == null ) {
280                styleName = "default";
281            }
282            org.deegree.ogcwebservices.wms.capabilities.Style ogcStyle = ogcLayer.getStyleResource( styleName );// stylename
283    
284            // 1. get style for layer
285            // first try with "default" and then with "" (empty string) as style name
286            LOG.logDebug( "Trying first method" );
287            if ( ogcStyle == null ) {
288                LOG.logDebug( "Layer has no valid default style definition. Obtaining default style from OGCLayer" );
289                ogcStyle = ogcLayer.getStyleResource( "" );
290            }
291    
292            // try to get another style for layer (only, if there is just one style definition)
293            if ( ogcStyle == null ) {
294                LOG.logDebug( "Layer has no valid default style definition." );
295                org.deegree.ogcwebservices.wms.capabilities.Style[] styles = ogcLayer.getStyles();
296                if ( styles.length == 1 ) {
297                    // using the only available style definition as default style definition
298                    LOG.logDebug( "Layer has only one style definition. Assuming this as default style." );
299                    ogcStyle = styles[0];
300                } else {
301                    // more than one style definition available, but non is the default style.
302                    // Therefore no style definition can be chosen. Nothing happens here.
303                }
304            }
305    
306            // 2. try to get legend url from style definition
307            if ( ogcStyle != null ) {
308                LOG.logDebug( "Trying second method" );
309                LegendURL[] legendUrls = ogcStyle.getLegendURL();
310                if ( legendUrls != null && legendUrls.length > 0 ) {
311                    // First field of the array contains a url to the legend
312                    url = legendUrls[0].getOnlineResource();
313                    LOG.logDebug( "Legend url from the ogcStyle is: " + url.toExternalForm() );
314                }
315            }
316    
317            // 3. try to get legend url from getLegendGraphic request
318            if ( ogcStyle == null || url == null ) {
319                LOG.logDebug( "Trying third method" );
320                // either layer does not have a style info at all
321                // or layer has a style info, but this style does not contain a legend url
322    
323                QualifiedName name = new QualifiedName( "GetLegendGraphic" );
324                Operation op = wmsCapa.getOperationMetadata().getOperation( name );
325                if ( op != null ) {
326                    HTTP http = (HTTP) op.getDCP().get( 0 );
327                    url = http.getGetOnlineResources().get( 0 );
328                } else {
329                    LOG.logDebug( "GetLegendGrpahic not served by the service." );
330                }
331                LOG.logDebug( "LegendURLs can not be extracted from ogcStyle." );
332            }
333            if ( url != null ) {
334                try {
335                    // If no exception is thrown here, then everythis is ok, otherwise we load the missing legend image
336                    ImageUtils.loadImage( url );
337                } catch ( Exception e ) {
338                    // The path is invalid. Loading the missing image from controller.xml
339                    LOG.logDebug( StringTools.concat( 100, "The url : " + url,
340                                                      " could not be loaded. Loading missing image" ) );
341                    url = createMissingLegend( ogcLayer.getTitle() );
342                }
343            }
344            return url;
345        }
346    
347        /**
348         * Tries to create the missing image from the given path in the controller.xml If this image is invalid we try to
349         * create our own empty legend with/without layer title, depending on the configurations
350         *
351         * @param layerTitle
352         * @return Missing legend URL
353         */
354        private URL createMissingLegend( String layerTitle ) {
355    
356            URL url = null;
357            try {
358                // using default length. width and format
359                BufferedImage img = createEmptyLegend( layerTitle );
360                // create the folder print in webapp if we don't have it
361                File printDir = new File( getHomePath() + "print" );
362                if ( !printDir.exists() ) {
363                    printDir.mkdir();
364                }
365                File imgFile = findavailableName( printDir, "missingLegend", ".png" );
366                ImageUtils.saveImage( img, imgFile, 1.0f );
367                url = new URL( "file:///" + imgFile.getAbsolutePath() );
368    
369            } catch ( Exception e ) {
370                LOG.logError( e.getLocalizedMessage() );
371            }
372            return url;
373        }
374    
375        /**
376         * Creates an empty legend in case the path to the MISSING_IMAGE parameter in the controller is invalid
377         *
378         * @param layerTitle
379         * @return Empty legend BufferedImage
380         */
381        private BufferedImage createEmptyLegend( String layerTitle ) {
382    
383            BufferedImage legend = new BufferedImage( 1, 1, BufferedImage.TYPE_INT_ARGB );
384            if ( missingImg != null ) {
385                try {
386                    legend = ImageUtils.loadImage( missingImg );
387                    // if MISSING_IMAGE path is valid and useLayerTitle is false, just return the missing image
388                    return legend;
389                } catch ( Exception e ) {
390                    // its ok, we already have a valid legend BufferedImage
391                }
392            }
393    
394            // if useLayerTitle == true,add the title to the image
395            LOG.logDebug( "Adding title to the missing image" );
396            BufferedImage missingLegend = new BufferedImage( 80, 15, BufferedImage.TYPE_INT_RGB );
397            Graphics g = missingLegend.getGraphics();
398            Rectangle2D rect = g.getFontMetrics().getStringBounds( layerTitle, g );
399    
400            missingLegend = new BufferedImage( rect.getBounds().width + 80, legend.getHeight() + 15,
401                                               BufferedImage.TYPE_INT_ARGB );
402            g.dispose();
403            g = missingLegend.getGraphics();
404            g.drawImage( legend, 0, 0, null );
405            g.dispose();
406            return missingLegend;
407        }
408    
409        /**
410         * Finds the first non used name in the folder name that starts with the given prefix. Ex. if prefix is print, then
411         * it looks if print1 does not exist to use, if not then print2, if not then print3 etc..
412         *
413         * @param folderName
414         * @param prefix
415         * @param suffix
416         *            (extension)of the generated path, ex .png or .jpg
417         * @return Available file name
418         */
419        private File findavailableName( File folderName, String prefix, String suffix ) {
420            String filename = folderName.getAbsolutePath();
421            if ( !filename.endsWith( "/" ) && !filename.endsWith( "\\" ) ) {
422                filename += "/";
423            }
424            filename += prefix;
425            int counter = 1;
426            while ( true ) {
427                File file = new File( StringTools.concat( 100, filename, counter++, suffix ) );
428                if ( !file.exists() ) {
429                    return file;
430                }
431            }
432        }
433    
434        /**
435         * changes the bounding box of a given view context
436         *
437         * @param vc
438         *            the view context to be changed
439         * @param bbox
440         *            the new bounding box
441         * @throws PortalException
442         */
443        public static final void changeBBox( ViewContext vc, Envelope bbox )
444                                throws PortalException {
445            General gen = vc.getGeneral();
446    
447            CoordinateSystem cs = gen.getBoundingBox()[0].getCoordinateSystem();
448            Point[] p = new Point[] { GeometryFactory.createPoint( bbox.getMin(), cs ),
449                                     GeometryFactory.createPoint( bbox.getMax(), cs ) };
450            try {
451                gen.setBoundingBox( p );
452            } catch ( ContextException e ) {
453                LOG.logError( e.getMessage(), e );
454                throw new PortalException( Messages.getMessage( "IGEO_STD_CNTXT_ERROR_SET_BBOX" ) );
455            }
456        }
457    
458    }