001    //$HeadURL: http://svn.wald.intevation.org/svn/deegree/base/trunk/src/org/deegree/ogcwebservices/wms/operation/GetFeatureInfo.java $
002    /*----------------------------------------------------------------------------
003     This file is part of deegree, http://deegree.org/
004     Copyright (C) 2001-2009 by:
005     Department of Geography, University of Bonn
006     and
007     lat/lon GmbH
008    
009     This library is free software; you can redistribute it and/or modify it under
010     the terms of the GNU Lesser General Public License as published by the Free
011     Software Foundation; either version 2.1 of the License, or (at your option)
012     any later version.
013     This library is distributed in the hope that it will be useful, but WITHOUT
014     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
015     FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
016     details.
017     You should have received a copy of the GNU Lesser General Public License
018     along with this library; if not, write to the Free Software Foundation, Inc.,
019     59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020    
021     Contact information:
022    
023     lat/lon GmbH
024     Aennchenstr. 19, 53177 Bonn
025     Germany
026     http://lat-lon.de/
027    
028     Department of Geography, University of Bonn
029     Prof. Dr. Klaus Greve
030     Postfach 1147, 53001 Bonn
031     Germany
032     http://www.geographie.uni-bonn.de/deegree/
033    
034     e-mail: info@deegree.org
035     ----------------------------------------------------------------------------*/
036    package org.deegree.ogcwebservices.wms.operation;
037    
038    import java.awt.Point;
039    import java.io.UnsupportedEncodingException;
040    import java.net.URLEncoder;
041    import java.util.ArrayList;
042    import java.util.Iterator;
043    import java.util.List;
044    import java.util.Map;
045    import java.util.StringTokenizer;
046    
047    import org.deegree.framework.log.ILogger;
048    import org.deegree.framework.log.LoggerFactory;
049    import org.deegree.framework.util.CharsetUtils;
050    import org.deegree.framework.util.ColorUtils;
051    import org.deegree.framework.util.StringTools;
052    import org.deegree.graphics.sld.StyledLayerDescriptor;
053    import org.deegree.ogcbase.ExceptionCode;
054    import org.deegree.ogcwebservices.InconsistentRequestException;
055    import org.deegree.ogcwebservices.OGCWebServiceException;
056    import org.deegree.ogcwebservices.wms.InvalidPointException;
057    
058    /**
059     * @author Katharina Lupp <a href="mailto:k.lupp@web.de">Katharina Lupp </a>
060     * @version $Revision: 31591 $ $Date: 2011-08-18 16:08:43 +0200 (Do, 18 Aug 2011) $
061     */
062    public class GetFeatureInfo extends WMSRequestBase {
063    
064        private static final long serialVersionUID = 1197866346790857492L;
065    
066        private static final ILogger LOGGER = LoggerFactory.getLogger( GetFeatureInfo.class );
067    
068        private List<String> queryLayers = null;
069    
070        private Point clickPoint = null;
071    
072        private String exceptions = null;
073    
074        private String infoFormat = null;
075    
076        private StyledLayerDescriptor sld = null;
077    
078        private GetMap getMapRequestCopy = null;
079    
080        private int featureCount = 1;
081    
082        private boolean infoFormatIsDefault = false;
083    
084        /**
085         * creates a <tt>WMSFeatureInfoRequest</tt> from the request parameters.
086         * 
087         * @return an instance of <tt>WMSFeatureInfoRequest</tt>
088         * @param version
089         *            VERSION=version (R): Request version.
090         * @param id
091         *            the request id
092         * @param queryLayers
093         *            QUERY_LAYERS=layer_list (R): Comma-separated list of one or more layers to be queried.
094         * @param getMapRequestCopy
095         *            &lt;map_request_copy&gt; (R): Partial copy of the Map request parameters that generated the map for
096         *            which information is desired.
097         * @param infoFormat
098         *            INFO_FORMAT=output_format (O): Return format of feature information (MIME type).
099         * @param featureCount
100         *            FEATURE_COUNT=number (O): Number of features about which to return information (default=1).
101         * @param clickPoint
102         *            X=pixel_column (R): X coordinate in pixels of feature (measured from upper left corner=0) Y=pixel_row
103         *            (R): Y coordinate in pixels of feature (measured from upper left corner=0)
104         * @param exceptions
105         *            EXCEPTIONS=exception_format (O): The format in which exceptions are to be reported by the WMS
106         *            (default=application/vnd.ogc.se_xml).
107         * @param sld
108         *            StyledLayerDescriptor
109         * @param vendorSpecificParameter
110         *            Vendor-specific parameters (O): Optional experimental parameters.
111         */
112        public static GetFeatureInfo create( String version, String id, String[] queryLayers, GetMap getMapRequestCopy,
113                                             String infoFormat, int featureCount, java.awt.Point clickPoint,
114                                             String exceptions, StyledLayerDescriptor sld,
115                                             Map<String, String> vendorSpecificParameter ) {
116    
117            return new GetFeatureInfo( version, id, queryLayers, getMapRequestCopy, infoFormat, featureCount, clickPoint,
118                                       exceptions, sld, vendorSpecificParameter );
119    
120        }
121    
122        /**
123         * creates a <tt>WMSFeatureInfoRequest</tt> from a <tt>HashMap</tt> that contains the request parameters as
124         * key-value-pairs. Keys are expected to be in upper case notation.
125         * 
126         * @param model
127         *            <tt>HashMap</tt> containing the request parameters
128         * @return an instance of <tt>WMSFeatureInfoRequest</tt>
129         * @throws OGCWebServiceException
130         */
131        public static GetFeatureInfo create( Map<String, String> model )
132                                throws OGCWebServiceException {
133    
134            // VERSION
135            String version = model.get( "VERSION" );
136            if ( version == null ) {
137                version = model.get( "WMTVER" );
138            }
139            if ( version == null ) {
140                throw new InconsistentRequestException( "VERSION-value must be set in the GetFeatureInfo request" );
141            }
142    
143            boolean is130 = ( "1.3.0".compareTo( version ) <= 0 );
144    
145            // ID
146            String id = model.get( "ID" );
147            if ( id == null ) {
148                throw new InconsistentRequestException( "ID-value must be set in the GetFeatureInfo request" );
149            }
150    
151            // QUERY_LAYERS
152            String layerlist = model.remove( "QUERY_LAYERS" );
153            String[] queryLayers = null;
154    
155            if ( layerlist != null ) {
156                StringTokenizer st = new StringTokenizer( layerlist, "," );
157                queryLayers = new String[st.countTokens()];
158                int i = 0;
159                while ( st.hasMoreTokens() ) {
160                    queryLayers[i++] = st.nextToken();
161                }
162            } else {
163                throw new InconsistentRequestException( "QUERY_LAYERS-value must be set in the GetFeatureInfo request" );
164            }
165    
166            // INFO_FORMAT (mime-type)
167            String infoFormat = model.remove( "INFO_FORMAT" );
168            boolean infoFormatDefault = false;
169            if ( infoFormat == null ) {
170                infoFormat = "application/vnd.ogc.gml";
171                infoFormatDefault = true;
172            }
173    
174            // FEATURE_COUNT (default=1)
175            String feco = model.remove( "FEATURE_COUNT" );
176            int featureCount = 1;
177            if ( feco != null ) {
178                featureCount = Integer.parseInt( feco.trim() );
179            }
180            if ( featureCount < 0 ) {
181                featureCount = 1;
182            }
183    
184            // X, Y (measured from upper left corner=0)
185            String X;
186            String Y;
187    
188            if ( is130 ) {
189                X = "I";
190                Y = "J";
191            } else {
192                X = "X";
193                Y = "Y";
194            }
195    
196            String xstring = model.remove( X );
197            String ystring = model.remove( Y );
198    
199            java.awt.Point clickPoint = null;
200            if ( ( xstring != null ) && ( ystring != null ) ) {
201                try {
202                    int x = Integer.parseInt( xstring.trim() );
203                    int y = Integer.parseInt( ystring.trim() );
204                    clickPoint = new java.awt.Point( x, y );
205                } catch ( NumberFormatException nfe ) {
206                    LOGGER.logError( nfe.getLocalizedMessage(), nfe );
207                    throw new OGCWebServiceException( "GetFeatureInfo", "Invalid point parameter",
208                                                      ExceptionCode.INVALID_POINT );
209                }
210            } else {
211                throw new InconsistentRequestException( X + "- and/or " + Y
212                                                        + "-value must be set in the GetFeatureInfo request" );
213            }
214    
215            // EXCEPTIONS (default=application/vnd.ogc.se_xml)
216            String exceptions = model.get( "EXCEPTIONS" );
217            if ( exceptions == null ) {
218                if ( is130 ) {
219                    exceptions = "XML";
220                } else {
221                    exceptions = "application/vnd.ogc.se_xml";
222                }
223            }
224    
225            // <map_request_copy>
226            GetMap getMapRequestCopy = null;
227    
228            try {
229                getMapRequestCopy = GetMap.create( model );
230            } catch ( Exception ex ) {
231                throw new InconsistentRequestException(
232                                                        "\nAn Exception "
233                                                                                + "occured in creating the GetMap request-copy included in the "
234                                                                                + "GetFeatureInfo-Operations:\n"
235                                                                                + "--> Location: WMSProtocolFactory, createGetFeatureInfoRequest(int, HashMap)\n"
236                                                                                + ex.getMessage() );
237    
238            }
239    
240            // check for consistency
241            if ( clickPoint.x > getMapRequestCopy.getWidth() || clickPoint.y > getMapRequestCopy.getHeight() ) {
242                throw new InvalidPointException( "The requested point is not valid." );
243            }
244    
245            // VendorSpecificParameter; because all defined parameters has been
246            // removed
247            // from the model the vendorSpecificParameters are what left
248            Map<String, String> vendorSpecificParameter = model;
249    
250            // StyledLayerDescriptor
251            StyledLayerDescriptor sld = getMapRequestCopy.getStyledLayerDescriptor();
252    
253            GetFeatureInfo res = create( version, id, queryLayers, getMapRequestCopy, infoFormat, featureCount, clickPoint,
254                                         exceptions, sld, vendorSpecificParameter );
255            res.infoFormatIsDefault = infoFormatDefault;
256    
257            return res;
258        }
259    
260        /**
261         * Creates a new WMSFeatureInfoRequest_Impl object.
262         * 
263         * @param version
264         * @param id
265         * @param queryLayers
266         * @param getMapRequestCopy
267         * @param infoFormat
268         * @param featureCount
269         * @param clickPoint
270         * @param exceptions
271         * @param sld
272         * @param vendorSpecificParameter
273         */
274        private GetFeatureInfo( String version, String id, String[] queryLayers, GetMap getMapRequestCopy,
275                                String infoFormat, int featureCount, Point clickPoint, String exceptions,
276                                StyledLayerDescriptor sld, Map<String, String> vendorSpecificParameter ) {
277            super( version, id, vendorSpecificParameter );
278            this.queryLayers = new ArrayList<String>();
279            setQueryLayers( queryLayers );
280            setGetMapRequestCopy( getMapRequestCopy );
281            setGetMapRequestCopy( getMapRequestCopy );
282            setFeatureCount( featureCount );
283            setClickPoint( clickPoint );
284            setExceptions( exceptions );
285            setStyledLayerDescriptor( sld );
286            setInfoFormat( infoFormat );
287        }
288    
289        /**
290         * <map request copy> is not a name/value pair like the other parameters. Instead, most of the GetMap request
291         * parameters that generated the original map are repeated. Two are omitted because GetFeatureInfo provides its own
292         * values: VERSION and REQUEST. The remainder of the GetMap request shall be embedded contiguously in the
293         * GetFeatureInfo request.
294         * 
295         * @return a copy of the original request
296         */
297        public GetMap getGetMapRequestCopy() {
298            return getMapRequestCopy;
299        }
300    
301        /**
302         * sets the <GetMapRequestCopy>
303         * 
304         * @param getMapRequestCopy
305         */
306        public void setGetMapRequestCopy( GetMap getMapRequestCopy ) {
307            this.getMapRequestCopy = getMapRequestCopy;
308        }
309    
310        /**
311         * The required QUERY_LAYERS parameter states the map layer(s) from which feature information is desired to be
312         * retrieved. Its value is a comma- separated list of one or more map layers that are returned as an array. This
313         * parameter shall contain at least one layer name, but may contain fewer layers than the original GetMap request.
314         * <p>
315         * </p>
316         * If any layer in this list is not contained in the Capabilities XML of the WMS, the results are undefined and the
317         * WMS shall produce an exception response.
318         * 
319         * @return the layer names
320         */
321        public String[] getQueryLayers() {
322            return queryLayers.toArray( new String[queryLayers.size()] );
323        }
324    
325        /**
326         * adds the <QueryLayers>
327         * 
328         * @param queryLayers
329         */
330        public void addQueryLayers( String queryLayers ) {
331            this.queryLayers.add( queryLayers );
332        }
333    
334        /**
335         * sets the <QueryLayers>
336         * 
337         * @param queryLayers
338         */
339        public void setQueryLayers( String[] queryLayers ) {
340            this.queryLayers.clear();
341    
342            if ( queryLayers != null ) {
343                for ( int i = 0; i < queryLayers.length; i++ ) {
344                    this.queryLayers.add( queryLayers[i] );
345                }
346            }
347        }
348    
349        /**
350         * The optional INFO_FORMAT indicates what format to use when returning the feature information. Supported values
351         * for a GetFeatureInfo request on a WMS instance are listed as MIME types in one or more <Format>elements inside
352         * the <Request><FeatureInfo>element of its Capabilities XML. The entire MIME type string in <Format>is used as the
353         * value of the INFO_FORMAT parameter. In an HTTP environment, the MIME type shall be set on the returned object
354         * using the Content-type entity header.
355         * <p>
356         * </p>
357         * <b>EXAMPLE: </b> <tt> The parameter INFO_FORMAT=application/vnd.ogc.gml
358         * requests that the feature information be formatted in Geography Markup
359         * Language (GML).</tt>
360         * 
361         * @return the format
362         */
363        public String getInfoFormat() {
364            return infoFormat;
365        }
366    
367        /**
368         * sets the <InfoFormat>
369         * 
370         * @param infoFormat
371         */
372        public void setInfoFormat( String infoFormat ) {
373            this.infoFormat = infoFormat;
374        }
375    
376        /**
377         * The optional FEATURE_COUNT parameter states the maximum number of features for which feature information should
378         * be returned. Its value is a positive integer greater than zero. The default value is 1 if this parameter is
379         * omitted.
380         * 
381         * @return the count
382         */
383        public int getFeatureCount() {
384            return featureCount;
385        }
386    
387        /**
388         * sets the <FeatureCount>
389         * 
390         * @param featureCount
391         */
392        public void setFeatureCount( int featureCount ) {
393            this.featureCount = featureCount;
394        }
395    
396        /**
397         * The required X and Y parameters indicate a point of interest on the map. X and Y identify a single point within
398         * the borders of the WIDTH and HEIGHT parameters of the embedded GetMap request. The origin is set to (0,0)
399         * centered in the pixel at the upper left corner; X increases to the right and Y increases downward. X and Y are
400         * retruned as java.awt.Point class/datastructure.
401         * 
402         * @return the point of interest
403         */
404        public Point getClickPoint() {
405            return clickPoint;
406        }
407    
408        /**
409         * sets the <ClickPoint>
410         * 
411         * @param clickPoint
412         */
413        public void setClickPoint( Point clickPoint ) {
414            this.clickPoint = clickPoint;
415        }
416    
417        /**
418         * The optional EXCEPTIONS parameter states the manner in which errors are to be reported to the client. The default
419         * value is application/vnd.ogc.se_xml if this parameter is absent from the request. At present, not other values
420         * are defined for the WMS GetFeatureInfo request.
421         * 
422         * @return the exception format
423         */
424        public String getExceptions() {
425            return exceptions;
426        }
427    
428        /**
429         * sets the <Exception>
430         * 
431         * @param exceptions
432         */
433        public void setExceptions( String exceptions ) {
434            this.exceptions = exceptions;
435        }
436    
437        /**
438         * returns the SLD the request is made of. This implies that a 'simple' HTTP GET-Request will be transformed into a
439         * valid SLD. This is mandatory within a JaGo WMS.
440         * <p>
441         * </p>
442         * This mean even if a GetMap request is send using the HTTP GET method, an implementing class has to map the
443         * request to a SLD data sructure.
444         * 
445         * @return the sld
446         */
447        public StyledLayerDescriptor getStyledLayerDescriptor() {
448            return sld;
449        }
450    
451        /**
452         * sets the SLD the request is made of. This implies that a 'simple' HTTP GET-Request or a part of it will be
453         * transformed into a valid SLD. For convenience it is asumed that the SLD names just a single layer to generate
454         * display elements of.
455         * 
456         * @param sld
457         */
458        public void setStyledLayerDescriptor( StyledLayerDescriptor sld ) {
459            this.sld = sld;
460        }
461    
462        @Override
463        public String toString() {
464            try {
465                return getRequestParameter();
466            } catch ( OGCWebServiceException e ) {
467                e.printStackTrace();
468            }
469            return super.toString();
470        }
471    
472        /**
473         * returns the parameter of a HTTP GET request.
474         * 
475         */
476        @Override
477        public String getRequestParameter()
478                                throws OGCWebServiceException {
479            // indicates if the request parameters are decoded as SLD. deegree won't
480            // perform SLD requests through HTTP GET
481            if ( ( getMapRequestCopy.getBoundingBox() == null ) || ( queryLayers.size() == 0 ) ) {
482                throw new OGCWebServiceException( "Operations can't be expressed as HTTP GET request " );
483            }
484    
485            StringBuffer sb = new StringBuffer( "service=WMS" );
486    
487            if ( getVersion().compareTo( "1.0.0" ) <= 0 ) {
488                sb.append( "&VERSION=" + getVersion() + "&REQUEST=feature_info" );
489                sb.append( "&TRANSPARENT=" + getMapRequestCopy.getTransparency() );
490            } else {
491                sb.append( "&VERSION=" + getVersion() + "&REQUEST=GetFeatureInfo" );
492                sb.append( "&TRANSPARENCY=" + getMapRequestCopy.getTransparency() );
493            }
494    
495            sb.append( "&WIDTH=" + getMapRequestCopy.getWidth() );
496            sb.append( "&HEIGHT=" + getMapRequestCopy.getHeight() );
497            sb.append( "&FORMAT=" + getMapRequestCopy.getFormat() );
498            sb.append( "&EXCEPTIONS=" + getExceptions() );
499            sb.append( "&BGCOLOR=" );
500            sb.append( ColorUtils.toHexCode( "0x", getMapRequestCopy.getBGColor() ) );
501            if ( "1.3.0".compareTo( getVersion() ) <= 0 ) {
502                sb.append( "&CRS=" + getMapRequestCopy.getSrs() );
503                sb.append( "&BBOX=" ).append( getMapRequestCopy.getBoundingBox().getMin().getY() );
504                sb.append( ',' ).append( getMapRequestCopy.getBoundingBox().getMin().getX() );
505                sb.append( ',' ).append( getMapRequestCopy.getBoundingBox().getMax().getY() );
506                sb.append( ',' ).append( getMapRequestCopy.getBoundingBox().getMax().getX() );
507            } else {
508                sb.append( "&SRS=" + getMapRequestCopy.getSrs() );
509                sb.append( "&BBOX=" ).append( getMapRequestCopy.getBoundingBox().getMin().getX() );
510                sb.append( ',' ).append( getMapRequestCopy.getBoundingBox().getMin().getY() );
511                sb.append( ',' ).append( getMapRequestCopy.getBoundingBox().getMax().getX() );
512                sb.append( ',' ).append( getMapRequestCopy.getBoundingBox().getMax().getY() );
513            }
514    
515            GetMap.Layer[] layers = getMapRequestCopy.getLayers();
516            String l = "";
517            String s = "";
518    
519            for ( int i = 0; i < layers.length; i++ ) {
520                l += ( layers[i].getName() + "," );
521                s += ( layers[i].getStyleName() + "," );
522            }
523    
524            l = l.substring( 0, l.length() - 1 );
525            s = s.substring( 0, s.length() - 1 );
526            sb.append( "&LAYERS=" + l );
527    
528            // replace $DEFAULT with "", which is what WMSses expect
529            s = StringTools.replace( s, "$DEFAULT", "", true );
530    
531            sb.append( "&STYLES=" + s );
532    
533            // TODO
534            // append time, elevation and sample dimension
535    
536            String[] qlayers = getQueryLayers();
537            String ql = "";
538    
539            for ( int i = 0; i < qlayers.length; i++ ) {
540                ql += ( qlayers[i] + "," );
541            }
542    
543            ql = ql.substring( 0, ql.length() - 1 );
544            sb.append( "&QUERY_LAYERS=" + ql );
545            sb.append( "&FEATURE_COUNT=" + getFeatureCount() );
546            sb.append( "&INFO_FORMAT=" + getInfoFormat() );
547            if ( "1.3.0".compareTo( getVersion() ) <= 0 ) {
548                sb.append( "&I=" + clickPoint.x );
549                sb.append( "&J=" + clickPoint.y );
550            } else {
551                sb.append( "&X=" + clickPoint.x );
552                sb.append( "&Y=" + clickPoint.y );
553            }
554            
555            DimensionValues values = getMapRequestCopy.getDimTime();
556            if ( values != null ) {
557                sb.append( "&time=" + values.getOriginalValue() );
558            }
559            
560            if ( getVendorSpecificParameters() != null ) {
561                Iterator<String> iterator = getVendorSpecificParameters().keySet().iterator();
562                while ( iterator.hasNext() ) {
563                    String key = iterator.next();
564                    String value = getVendorSpecificParameters().get( key );
565                    try {
566                        value = URLEncoder.encode( value, CharsetUtils.getSystemCharset() );
567                    } catch ( UnsupportedEncodingException e ) {
568                        // system encoding should be supported...
569                    }
570                    sb.append( '&' ).append( key ).append( '=' ).append( value );
571                }
572            }
573    
574    
575            return sb.toString();
576        }
577    
578        /**
579         * @return whether the info format is the default setting
580         */
581        public boolean isInfoFormatDefault() {
582            return infoFormatIsDefault;
583        }
584    
585    }