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