1 /**
  2 * @fileOverview  
  3 * Copyright (c) 2013 Regione Autonoma della Sardegna
  4 * Published under the GPL license.<br>
  5 * See <a href="https://sardegnageoportale.it/webgis/license.txt">https://sardegnageoportale.it/webgis/license.txt</a>
  6 * for the full text of the license.
  7 * @version 0.1
  8 */
  9 
 10 /**
 11  * @requires plugins/WMSSource.js
 12  * @requires OpenLayers/Format/WMSCapabilities/v1_1_1_WMSC.js
 13  */
 14 
 15 /**
 16  * @namespace framework.plugins
 17  */
 18 Ext.namespace("framework.plugins");
 19 
 20 /** @example
 21  *  Configuration in the  :class:`gxp.Viewer`:
 22  *
 23  *  .. code-block:: javascript
 24  *
 25  *    defaultSourceType: "framework_gwcsource",
 26  *    sources: {
 27  *        "opengeo": {
 28  *            url: "http://suite.opengeo.org/geoserver/wms"
 29  *        }
 30  *    }
 31  *
 32  *  A typical configuration for a layer from this source (in the ``layers``
 33  *  array of the viewer's ``map`` config option would look like this:
 34  *
 35  *  .. code-block:: javascript
 36  *
 37  *    {
 38  *        source: "opengeo",
 39  *        name: "world",
 40  *        group: "background"
 41  *    }
 42  *
 43  */
 44 
 45 /** 
 46  *  Plugin for using Geowebcache layers with :class:`gxp.Viewer` instances. The
 47  *  plugin issues a GetCapabilities request to create a store of the WMS's
 48  *  layers. If tilesets are available, it will use them. 
 49  *  @name_ GWCSource
 50  *  @class Plugin for using Geowebcache service
 51  *  @constructor
 52  *  @extends <a target="_blank" href="http://gxp.opengeo.org/master/doc/lib/plugins/WMSSource.html">gxp.plugins.WMSSource</a>
 53  */
 54 framework.plugins.GWCSource = Ext.extend(gxp.plugins.WMSSource, 
 55 /** 
 56 * @lends framework.plugins.GWCSource.prototype 
 57 */
 58 {
 59     
 60     /**
 61     * framework_gwcsource plugin type.
 62     * @public
 63     * @type String
 64     */
 65     ptype: "framework_gwcsource",
 66     
 67     /** api: config[version]
 68     * Only WMS 1.1.1 is supported at the moment.
 69     * @public
 70     * @type String
 71     */
 72     version: "1.1.1",
 73 
 74      
 75     /** 
 76      * Only WFS 1.1.0 is supported at the moment.
 77      * @public
 78      * @type String
 79      */
 80     url_wfs: "/geoserver/ows",
 81 
 82     /** 
 83     * @constructor
 84     * @private
 85     * @param {Array(String)} List of config properties that are required for each
 86     * layer from this source to allow lazy loading. Default is
 87     * ``["title", "bbox"]``. When the source loads layers from a WMS-C that
 88     * does not use subsets of the default Web Mercator grid, not provide
 89     * tiles for all default Web Mercator resolutions, and not use a tileSize
 90     * of 256x256 pixels, ``tileOrigin``, ``resolutions`` and ``tileSize``
 91     * should be included in this list.
 92     */ 
 93     
 94     constructor: function(config) {
 95         config.baseParams = {
 96             SERVICE: "WMS",
 97             REQUEST: "GetCapabilities",
 98             TILED: true
 99         };
100         if (!config.format) {
101             this.format = new OpenLayers.Format.WMSCapabilities({
102                 keepData: true,
103                 profile: "WMSC",
104                 allowFallback: true
105             });
106         }
107         framework.plugins.GWCSource.superclass.constructor.apply(this, arguments); 
108     },
109     
110     /** 
111      * Creates a store of layer records.  Fires "ready" when store is loaded.
112      * @public
113      */
114     createStore: function() {
115         var baseParams = this.baseParams || {
116             SERVICE: "WMS",
117             REQUEST: "GetCapabilities"
118         };
119         if (this.version) {
120             baseParams.VERSION = this.version;
121         }
122 
123         var lazy = this.isLazy();
124 
125         this.store = new GeoExt.data.WMSCapabilitiesStore({
126             // Since we want our parameters (e.g. VERSION) to override any in the 
127             // given URL, we need to remove corresponding paramters from the 
128             // provided URL.  Simply setting baseParams on the store is also not
129             // enough because Ext just tacks these parameters on to the URL - so
130             // we get requests like ?Request=GetCapabilities&REQUEST=GetCapabilities
131             // (assuming the user provides a URL with a Request parameter in it).
132             url: this.trimUrl(this.url, baseParams),
133             baseParams: baseParams,
134             format: this.format,
135             autoLoad: !lazy,
136             layerParams: {exceptions: null},
137             listeners: {
138                 load: function() {
139                     // The load event is fired even if a bogus capabilities doc 
140                     // is read (http://trac.geoext.org/ticket/295).
141                     // Until this changes, we duck type a bad capabilities 
142                     // object and fire failure if found.
143                     if (!this.store.reader.raw || !this.store.reader.raw.service) {
144                         this.fireEvent("failure", this, "Invalid capabilities document.");
145                     } else {
146                         if (!this.title) {
147                             this.title = this.store.reader.raw.service.title;                        
148                         }
149                         if (!this.ready) {
150                             this.ready = true;
151                             this.fireEvent("ready", this);
152                         } else {
153                             this.lazy = false;
154                             //TODO Here we could update all records from this
155                             // source on the map that were added when the
156                             // source was lazy.
157                         }
158                     }
159                     // clean up data stored on format after parsing is complete
160                     delete this.format.data;
161                 },
162                 exception: function(proxy, type, action, options, response, error) {
163                     delete this.store;
164                     var msg, details = "";
165                     if (type === "response") {
166                         if (typeof error == "string") {
167                             msg = error;
168                         } else {
169                             msg = "Invalid response from server.";
170                             // special error handling in IE
171                             var data = this.format && this.format.data;
172                             if (data && data.parseError) {
173                                 msg += "  " + data.parseError.reason + " - line: " + data.parseError.line;
174                             }
175                             var status = response.status;
176                             if (status >= 200 && status < 300) {
177                                 // TODO: consider pushing this into GeoExt
178                                 var report = error && error.arg && error.arg.exceptionReport;
179                                 details = gxp.util.getOGCExceptionText(report);
180                             } else {
181                                 details = "Status: " + status;
182                             }
183                         }                      
184                     } else {
185                         msg = "Trouble creating layer store from response.";
186                         details = "Unable to handle response.";
187                     }
188                     // TODO: decide on signature for failure listeners
189                     this.fireEvent("failure", this, msg, details);
190                     // clean up data stored on format after parsing is complete
191                     delete this.format.data;
192                 },
193                 scope: this
194             }
195         });
196         if (lazy) {
197             this.lazy = true;
198             // ping server of lazy source with an incomplete request, to see if
199             // it is available
200             Ext.Ajax.request({
201                 method: "GET",
202                 url: this.url,
203                 params: {SERVICE: "WMS"},
204                 callback: function(options, success, response) {
205                     var status = response.status;
206                     // responseText should not be empty (OGCException)
207                     if (status >= 200 && status < 403 && response.responseText) {
208                         this.ready = true;
209                         this.fireEvent("ready", this);
210                     } else {
211                         this.fireEvent("failure", this,
212                             "Layer source not available.",
213                             "Unable to contact WMS service."
214                         );
215                     }
216                 },
217                 scope: this
218             });
219         }
220     },
221 
222 
223     /**
224      * createLayerRecord
225      * @private
226      * @param {object} config configuration object
227      */
228     createLayerRecord: function(config) {
229         var record = framework.plugins.GWCSource.superclass.createLayerRecord.apply(this, arguments);
230         if (!record) {
231             return;
232         }
233         var caps, srs;
234         if (this.store.reader.raw) {
235             caps = this.store.reader.raw.capability;
236         }
237         var tileSets = (caps && caps.vendorSpecific) ? 
238             caps.vendorSpecific.tileSets : (config.capability && config.capability.tileSets);
239         var layer = record.get("layer");
240         if (tileSets) {
241             var mapProjection = this.getProjection(record) || this.getMapProjection();
242             // look for tileset with same name and equivalent projection
243             for (var i=0, len=tileSets.length; i<len; i++) {
244                 var tileSet = tileSets[i];
245                 if (tileSet.layers === layer.params.LAYERS) {
246                     var tileProjection;
247                     for (srs in tileSet.srs) {
248                         tileProjection = new OpenLayers.Projection(srs);
249                         break;
250                     }
251                     if (mapProjection.equals(tileProjection)) {
252                         var bbox = tileSet.bbox[srs].bbox;
253                         layer.projection = tileProjection;
254                         layer.addOptions({
255                             resolutions: tileSet.resolutions,
256                             tileSize: new OpenLayers.Size(tileSet.width, tileSet.height),
257                             tileOrigin: new OpenLayers.LonLat(bbox[0], bbox[1])
258                         });
259                         break;
260                     }
261                 }
262             }
263         } else if (this.lazy) {
264             // lazy loading
265             var tileSize = config.tileSize,
266                 tileOrigin = config.tileOrigin;
267             layer.addOptions({
268                 resolutions: config.resolutions,
269                 tileSize: tileSize ? new OpenLayers.Size(tileSize[0], tileSize[1]) : undefined,
270                 tileOrigin: tileOrigin ? OpenLayers.LonLat.fromArray(tileOrigin) : undefined
271             });
272             if (!tileOrigin) {
273                 // If tileOrigin was not set, our best bet is to use the map's
274                 // maxExtent, because GWC's tiling scheme always aligns to the
275                 // default Web Mercator grid. We don't do this with addOptions
276                 // because we persist the config from layer.options in
277                 // getConfigForRecord, and we don't want to persist a guessed
278                 // configuration.
279                 var maxExtent;
280                 if (this.target.map.maxExtent) {
281                     maxExtent = this.target.map.maxExtent;
282                 } else {
283                     srs = config.srs || this.target.map.projection;
284                     maxExtent = OpenLayers.Projection.defaults[srs].maxExtent;
285                 }
286                 if (maxExtent) {
287                     layer.tileOrigin = OpenLayers.LonLat.fromArray(maxExtent);
288                 }
289             }
290         }
291         // unless explicitly configured otherwise, use cached version
292         layer.params.TILED = (config.cached !== false) && true;
293         return record;
294     },
295 
296     /** 
297      * Create a config object that can be used to recreate the given record
298      * @public
299      * @param {GeoExt.data.LayerRecord} record
300      * @returns {Object}
301      */    
302     getConfigForRecord: function(record) {
303         var config = framework.plugins.GWCSource.superclass.getConfigForRecord.apply(this, arguments),
304             name = config.name,
305             tileSetsCap,
306             layer = record.getLayer();
307         if (config.capability && this.store.reader.raw) {
308             var capability = this.store.reader.raw.capability;
309             var tileSets = capability.vendorSpecific && capability.vendorSpecific.tileSets;
310             if (tileSets) {
311                 for (var i=tileSets.length-1; i>=0; --i) {
312                     tileSetsCap = tileSets[i];
313                     if (tileSetsCap.layers === name && tileSetsCap.srs[layer.projection]) {
314                         config.capability.tileSets = [tileSetsCap];
315                         break;
316                     }
317                 }
318             }
319         }
320         if (!(config.capability && config.capability.tileSets)) {
321             var tileSize = layer.options.tileSize;
322             if (tileSize) {
323                 config.tileSize = [tileSize.w, tileSize.h];
324             }
325             config.tileOrigin = layer.options.tileOrigin;
326             config.resolutions = layer.options.resolutions;
327         }
328         return Ext.applyIf(config, {
329             // the "tiled" property is already used to indicate singleTile
330             // the "cached" property will indicate whether to send the TILED param
331             cached: !!layer.params.TILED
332         });
333     },
334 
335    /** 
336     * Helper function to fetch the schema for a layer of this source
337     * @private
338     * @param {String} url. The url to the WFS endpoint
339     * @param {String} typeName. The typeName to use
340     * @param {function} callback Callback function. Will be called with
341     * a ``GeoExt.data.AttributeStore`` containing the schema as first
342     * argument, or false if the WMS does not support DescribeLayer or the
343     * layer is not associated with a WFS feature type.
344     * @param {scope} Object. Optional scope for the callback
345     */
346     fetchSchema: function(url, typeName, callback, scope) {
347         var schema = this.schemaCache[typeName];
348         if (schema) {
349             if (schema.getCount() == 0) {
350                 schema.on("load", function() {
351                     callback.call(scope, schema);
352                 }, this, {single: true});
353             } else {
354                 callback.call(scope, schema);
355             }
356         } else {
357             schema = new GeoExt.data.AttributeStore({
358                 url: url,
359                 baseParams: {
360                     SERVICE: "WFS",
361                     //TODO should get version from WFS GetCapabilities
362                     VERSION: "1.1.0",
363                     REQUEST: "DescribeFeatureType",
364                     TYPENAME: typeName
365                 },
366                 autoLoad: true,
367                 listeners: {
368                     "load": function() {
369                         callback.call(scope, schema);
370                     },
371                     scope: this
372                 }
373             });
374           this.schemaCache[typeName] = schema;
375         }
376     },
377 
378     /** 
379      * Gets the schema for a layer of this source, if the layer is a feature
380      * layer.
381      * @public
382      * @param {GeoExt.data.LayerRecord} rec the WMS layer to issue a WFS DescribeFeatureType request for
383      * @param {Function} callback Will be called with
384      *      a ``GeoExt.data.AttributeStore`` containing the schema as first
385      *      argument, or false if the WMS does not support DescribeLayer or the
386      *      layer is not associated with a WFS feature type.
387      * @param {scope} scope Optional scope for the callback
388      */
389     getSchema: function(rec, callback, scope) {
390         if (!this.schemaCache) {
391             this.schemaCache = {};
392         }
393         this.describeLayer(rec, function(r) {
394             if (r && r.get("owsType") == "WFS") {
395                 var typeName = r.get("typeName");
396                 var url = r.get("owsURL");
397                 this.fetchSchema(url, typeName, callback, scope);
398             } else if (!r) {
399                 // When DescribeLayer is not supported, we make the following
400                 // assumptions:
401                 // 1. URL of the WFS is the same as the URL of the WMS
402                 // 2. typeName is the same as the WMS Layer name
403                 //this.fetchSchema(this.url, rec.get('name'), callback, scope);
404                 this.fetchSchema(this.url_wfs, rec.get('name'), callback, scope);
405             } else {
406                 callback.call(scope, false);
407             }
408         }, this);
409     },
410     
411     /** Creates a WFS protocol for the given WMS layer record.
412      * @public
413      * @param {GeoExt.data.LayerRecord} record
414      * @param {function} callback
415      * @param {type} scope 
416      * @returns {OpenLayers.Protocol.WFS}
417      */
418     getWFSProtocol: function(record, callback, scope) {
419         this.getSchema(record, function(schema) {
420             var protocol = false;
421             if (schema) {
422                 var geometryName;
423                 var geomRegex = /gml:((Multi)?(Point|Line|Polygon|Curve|Surface|Geometry)).*/;
424                 schema.each(function(r) {
425                     var match = geomRegex.exec(r.get("type"));
426                     if (match) {
427                         geometryName = r.get("name");
428                     }
429                 }, this);
430                 protocol = new OpenLayers.Protocol.WFS({
431                     version: "1.1.0",
432                     srsName: record.getLayer().projection.getCode(),
433                     url: schema.url,
434                     featureType: schema.reader.raw.featureTypes[0].typeName,
435                     featureNS: schema.reader.raw.targetNamespace,
436                     geometryName: geometryName
437                 });
438             }
439             callback.call(scope, protocol, schema, record);
440         }, this);
441     }
442 
443 
444     
445 });
446 
447 Ext.preg(framework.plugins.GWCSource.prototype.ptype, framework.plugins.GWCSource);
448