Saturday, November 20, 2010

Hack OpenGeo Styler to work with generic WMS servers

As part of the OpenGeo Suite, Styler is a web based WMS viewer plus SLD editor built upon OpenLayers and GeoExt. Although it uses WMS and SLD protocols, current release of Styler must be coupled with a running GeoServer instance. So it is impossible for Styler to work with a generic WMS server due to the following issues:
  • WMS layers must come from GeoServer instance on the same machine as “/geoserver/wms?”
  • Every WMS layer must have has a WFS feature type at back end which must come from “/geoserver/wfs?”
  • There is no where to configure proxy so it doesn’t work with a remote server
  • Instead of WMS GetStyle, Styler is using GeoServer REST API (not a standard) to load SLD from WMS
  • Instead of WMS PutStyle, Styler is using GeoServer REST API (not a standard) to push and persist styles back to WMS
  • Styler is not using either standard WMS parameter “SLD” or “SLD_BODY” to make stateless change of map styles
  • Styler requires the WMS to support DescribeLayer interface
By briefly looking through its source code, I found it’s not too hard to overcome those issues and make Styler talk to a generic WMS server (e.g. ArcGIS Server) as long as the target WMS meets following prerequisites:
  • WMS must support EPSG:900913 (EPSG:3857 or Esri’s EPSG:102110 is ok but requires a little tweak in OpenLayers)
  • WMS must support GetStyle interface
  • Each layer in WMS must have a WFS feature type at back end in order to get attributes list
  • Each style of a WMS layer must have a SLD definition which can be retrieved through GetStyle
  • Optionally WMS needs to support PutStyle interface if one wants to persist SLD styles at server side
  • Optionally WMS needs to support SLD_BODY to make stateless change of rendering and symbology 
So in this article, I’m going to go through the basic steps to tweak the OpenGeo Styler to communicate with a non-GeoServer WMS:

1. Checkout Styler source code from http://svn.opengeo.org/suite/trunk/styler

2. Build Styler
sudo easy_install jstools
mkdir script
jsbuild build.cfg -o script
After a successful build there should be 4 js files in script folder.
Note: I only found out how I can build it on Linux, and I didn’t try on Windows

3. Setup debug environment for Styler
To launch Styler application simply follow the instructions here. By default, the index.html references merged and compressed js file in “script” folder, in order to modify and debug Styler you will need to reference those uncompressed js files in index.html as below:
1: ...
2: <script src="../styler/externals/openlayers/lib/OpenLayers.js"></script>
3: <script src="../styler/externals/ext/adapter/ext/ext-base.js"></script>
4: <script src="../styler/externals/ext/ext-all.js"></script>
5: <script src="../styler/externals/geoext/lib/GeoExt.js"></script>
6: <script src="../styler/script/gxp.js"></script>
7: <script src="../styler/lib/Styler.js"></script>
8: <script src="../styler/lib/Styler/dispatch.js"></script>
9: <script src="../styler/lib/Styler/ColorManager.js"></script>        
10: <script src="../styler/lib/Styler/SchemaManager.js"></script>
11: <script src="../styler/lib/Styler/SLDManager.js"></script>      
12: <script src="../styler/externals/ux/colorpicker/color-picker.ux.js"></script>
13: ...
Now you should be able to debug into OpenLayers and Styler in browser.

4. Hack the code
You don’t actually need to touch any code in OpenLayers or GeoExt, all changes are within three .js files in Styler itself, which are:
\lib\Styler.js
\lib\Styler\SLDManager.js
\lib\Styler\SchemaManager.js
Code changes in those files are explained in details below.

Code Changes in Styler.js

a. Add three properties for class Styler
1: // right before the “constructor”, and after other properties
2: ...
3: wmsUrl: "/geoserver/wms?",
4: wfsUrl: "/geoserver/wfs?",  
5: proxyUrl: "",
6: ...

b. Within Styler.js replace “/geoserver/wms?” or “/geoserver/ows?” with “this.proxyUrl + this.wmsUrl”, and replace “/geoserver/wfs?” with “this.proxyUrl + this.wfsUrl”. This is to allow user to point Styler to any WMS, either remote or local.

Note: there is one exception though, in method createLayers() you just need to replace “/geoserver/wms?” with this.wmsUrl because WMS layer doesn’t need a proxy to request map image.

c. Avoid using WMS DescribeLayer
1: //locate method:
2: describeLayers: function(callback) {
3:   ...
4: }
5: //Comment out original code and add following chunk of code
6: describeLayers: function(callback) {
7: for (var i=0, ii=this.wmsLayerList.length; i<ii; ++i) {
8:                   config = this.wmsLayerList[i];
9:                   // assume each WMS layer has a backend WFS layer
10:                   this.layerList.push(config);
11:             }
12:             callback();
13:   }
14: //here assumption is that every WMS layer has a WFS feature type at back end

d. If your WMS supports Google Mercator projection as something other than “EPSG:900913”, In method createLayers() you will need to add an extra parameter (e.g. “srs: ‘EPSG:3857’”) when creating each WMS layer
1: createLayers: function() {
2: ...
3:     layers.push(
4:     new OpenLayers.Layer.WMS(
5:         config.title, this.wmsUrl, {
6:         layers: config.name,
7:         styles: config.styles[0].name,
8:         transparent: true,
9:         format: "image/png",
10:         srs: "EPSG:3857"  // EPSG:3857, EPSG:102110 or EPSG:102113 etc.
11:         }, {
12:     }));
13: ...
14: }

e. Pass proxyUrl, wmsUrl and wfsUrl into SLDManager and SchemaManager
1: getSchemas: function(callback) {
2:             this.schemaManager = new Styler.SchemaManager(
3:             this.map,
4:             {
5:               proxyUrl: this.proxyUrl,
6:               wfsUrl: this.wfsUrl
7:             }
8:           );
9: 
10:   getStyles: function(callback) {
11:         this.sldManager = new Styler.SLDManager(this.map, {'proxyUrl': this.proxyUrl});
12:             this.sldManager.loadAll(callback);
13:       }
14: 
15:   initEditor: function() { {
16:   …
17:  this.sldManager = new Styler.SLDManager(this.map, {'proxyUrl': this.proxyUrl});
18:   ...
19: }
20: ...
21: 
22: // Note: the original constructors for SLDManager and SchemaManager takes a single input parameter
23: // but they will be modified to take 2nd input parameters in SLDManager.js and SchemaManager.js
24: 


Changes in SLDManager.js

a. Add an extra parameter called “proxyUrl” right before the constructor
1: // right before the “constructor”, and after other properties
2: ...
3: proxyUrl: "",
4: ...

b. Modify SLDManager constructor to take 2nd input parameter
1: initialize: function(map, options) {
2:     this.map = map;
3:     var layer;
4:     this.layers = [];
5:     this.layerData = {};
6:     this.format = new OpenLayers.Format.SLD({multipleSymbolizers: true});
7:     for (var i=0; i<this.map.layers.length; ++i) {
8:         layer = this.map.layers[i];
9:         if(layer instanceof OpenLayers.Layer.WMS) {
10:             this.layers.push(layer);
11:         }
12:     }
13:     // this is important so user can pass in ‘proxyUrl’
14:     OpenLayers.Util.extend(this, options);  
15: },

c. Overwrite getUrl() so it sends GetStyle request to retrieve WMS SLD defintion, here is an example
1: getUrl: function(layer, styleName) {
2:     var url;
3:     if(layer instanceof OpenLayers.Layer.WMS) {
4:         url = layer.url.split("?")[0] + "?" + "version=1.3.0&request=GetStyles&layers=" + layer.params["LAYERS"];       
5:         OpenLayers.Console.log(url);
6:     }
7:     //TODO handle other layer types
8:     return url;
9: }
10: 

d. In loadSld() method replace  this.getUrl(layer, styleName) with this.proxyUrl + this.getUrl(layer, styleName)
1: ...
2: loadSld: function(layer, styleName, callback) {
3:         Ext.Ajax.request({
4:             url: this.proxyUrl + this.getUrl(layer, styleName),
5:             method: "GET",
6:             …              
7: ...
8: // obviously there may be more than one styles for each WMS layer, 
9: // in this case the first style is always used.
10: 

e. In saveSld() method, replace original code with following to use “SLD_BODY” parameter to make stateless change of rendering and symbology
1: saveSld: function(layer, callback, scope) {
2:     this.layerData[layer.id].style.name = "__internalstyle__";
3:         var sldBody = this.format.write(this.layerData[layer.id].sld, {});
4:     layer.mergeNewParams({
5:       styles: "__internalstyle__", 
6:       sld_body: sldBody
7:     });
8:     callback.call(scope || this);  
9:   }
10: // By using “SLD_BODY” any change to the style is stateless which will not affect other users. 
11: // But if you want to persist modified styles on server side, 
12: // you must use WMS PutStyle operation and also make sure server supports it
13: 
14: 

Changes in SchemaManager.js

a. Add two extra parameters called “wfsUrl” and “proxyUrl” right before the constructor
1: // right before the “constructor”, and after other properties
2: ...
3: proxyUrl: "",
4: wfsUrl: “”,
5: ...

b. Modify SchemaManager constructor to take 2nd input parameter
1: initialize: function(map, options) {
2:             // other code
3: ...
4:             OpenLayers.Util.extend(this, options);
5:        ...     
6:             for(var i=0; i<this.map.layers.length; ++i) {
7:                   layer = this.map.layers[i];
8:                   if(layer instanceof OpenLayers.Layer.WMS) {
9:                         this.attributeStores[layer.id] = new GeoExt.data.AttributeStore({
10:                               //url: layer.url.split("?")[0].replace("/wms", "/wfs"),
11:                           // TODO: need better logic to handle case where this.wfsUrl is empty or must be derived from this.wmsUrl
12:                           url: this.proxyUrl + this.wfsUrl, 
13:                               baseParams: {
14:                                     //version: "1.1.1",    // is there a WFS 1.1.1?
15:                             version: "1.1.0",
16:                                     request: "DescribeFeatureType",
17:                                     typename: layer.params["LAYERS"]
18:                               }
19:                         });
20:                   }
21:             }
22:       }

5. Rebuild Styler and test against a generic WMS server.

So far the OpenGeo Styler has been hacked to communicate with a generic WMS server. To test it you just need to pass in the urls for WMS, WFS and proxy into Styler constructor in the original index.html like below.
1: Ext.BLANK_IMAGE_URL = "theme/img/blank.gif";
2: Ext.onReady(function() {
3:     window.styler = new Styler({
4:         baseLayers: [                              
5:             new OpenLayers.Layer.OSM(
6:                 "Open Street Map",
7:                 "http://tile.openstreetmap.org/${z}/${x}/${y}.png",
8:                 {numZoomLevels: 19}
9:             )                                                             
10:         ],                        
11:         wmsUrl:"http://localhost:8399/<WMSServer_endpoint>?",
12:         wfsUrl:"http://localhost:8399/<WFSServer_endpoint>?",
13:         proxyUrl: "http://locahost:8080/openlayers-trunk/ApacheProxyServlet?url="
14:     });
15: });

Some screenshots:

image
Styler load SLD style definition through WMS GetStyle operation

image
Styler changes map symbology through SLD_BODY parameter


6. Things to improve
  • If a layer has more than one styles, provide GUI for users to select 
  • Make OpenGeo Styler to persist SLD styles on server through WMS PutStyle operation