[OpenLayers-Users] HOWTO: Using the script protocol and modifying the filter. A solution for usb/disk-based map distribution

Phil Scadden p.scadden at gns.cri.nz
Wed Jun 8 20:07:51 EDT 2011


WFS queries are usually (and best) done through ajax mechanisms. 
However, if you have a map application on standalone disk or similar 
accessing remote map servers, you have a problem with browser security 
which will block you.
The solution is use the script protocol instead, which uses script 
insertion, GET and JSONP to achieve similar thing. And the emphasis is 
similar. I have more or less got this working but it was quite a slog. 
Here is how which might somebody some time and if anyone can come up 
with better solution, i'd be all ears. Note that I have modded some bits 
of OL2.11 to achieve this as well.

The protocol setup looks something like this:
                      wfsProtocol = new OpenLayers.Protocol.Script({
                          url: mywfsURL,
                          callbackKey: "format_options",
                          callbackPrefix: "callback:",
                          params: {
                              service: "WFS",
                              version: "1.1.0",
                              srsName: mySRS,
                              request: "GetFeature",
                              featurePrefix: myFeaturePrefix,
                              typeName: myfeatureType,
                              outputFormat: "json"
                          },
                          filterToParams: function(filter, params) {
                           // filter to params code
                        }
                      });
                  }

You use the protocol with same code that you would use for wfs protocol eg:
                          wfsProtocol.read({
                             filter: new OpenLayers.Filter.Spatial({
                                     type: 
OpenLayers.Filter.Spatial.INTERSECTS,
                                     value: e.feature.geometry
                             }),
                             callback: mycallback,
                             scope: strategy
                          });

The fun comes with the filtertoParams function in the protocol setup 
(which I left as blank in the above code. This has to take the filter 
and convert into set of parameters to use for a HTTP GET. By default (if 
you dont specify anything, it uses a QueryStringFilter. As far as I can 
see, this only works properly for a LOGICAL AND filter. Not at all for 
OR and spatial has too many complications for its simple approach.

For everything except AND filters, the filter needs to be converted to a 
cql Filter with all of cql filters limitations. The biggies are:
1/ (E)CQL Filters work exclusively in the native SRS. Returned GML 
however will be converted into the requested SRS.
2/  Lat/long native projections expect the geometry string to be in 
lat/long order, not x,y. This is big problem if you code is used where 
users can add own layers because you have to figure out what the kind of 
SRS it is.
3/ Like is supported but not ilike. Implicit AND clauses can be put in 
as separate parameters however. (Have form propertyname__ilike=value).

Here is my filterToParams function. Its not comprehensive but covers 
filters I use (mostly).


                          filterToParams: function(filter, params) {
                              // example to demonstrate BBOX serialization
                              delete params.cql_filter;
                              if (filter.type === 
OpenLayers.Filter.Spatial.BBOX) {
                                  params.bbox = filter.value.toArray();
                                  if (filter.projection) {
                                      
params.bbox.push(filter.projection.getCode());
                                  }
                              } else if (filter.type === 
OpenLayers.Filter.Spatial.DWITHIN) {
                                       var geom = filter.value;
                                      transformGeometry(geom);
                                       if (geom.x>=-180 && geom.x<=360 
&& geom.y>=-90 && geom.y<=90) { // is this a lat long?
                                           var xx = geom.x;
                                                geom.x = geom.y;
                                                geom.y = xx;
                                       }
                                      params.cql_filter = 
'DWITHIN(SHAPE,' + geom.toString() + ',' + filter.distance + ',meters)';
                              } else if (filter.type === 
OpenLayers.Filter.Spatial.INTERSECTS) {
                                   var geom = filter.value;
                                  transformGeometry(geom);
                                  geom.components[0].swapXY();
                                  params.cql_filter = 
'INTERSECTS(SHAPE,' + geom.toString() + ')';
                              } else if 
(OpenLayers.Format.QueryStringFilter) {
                                  var format = new 
OpenLayers.Format.QueryStringFilter({
                                      srsInBBOX: this.srsInBBOX
                                  });
                                  params = format.writecql(filter, params);
                              }
                              return params;
                          }

Notes about it.
transformGeometry is application-specific procedure to transform the 
geometry into the native SRS of the layer. There is quite a lot to this  
Firstly, when the layer is added to map, then the SRS has to be looked 
at. If not already supported then you need use script injection to bring 
in the appropriate projection from proj4. The layer native SRS has to be 
tracked so it can be compared with current map projection. If there are 
different, then transformGeometry needs to do a geom.transform to get it 
into native.

The next step is to determine whether this is lat/long SRS. If it is, 
then need to swapXY. I have extended the linearRing with a swapXY method.
     swapXY: function() {
         for (var i=0, len=this.components.length; i<len - 1; i++) {
             var component = this.components[i];
             var xx = component.x;
             component.x = component.y;
             component.y = xx;
         }
         this.bounds = null;
         return this;
     },

Note on DWITHIN and Geoserver. Geoserver just completely ignores the 
distanceUnits parameter. You need to make sure the distance units are in 
the native SRS as well.

For none spatial filters, I fell back on QueryStringFilter. It does not 
support OR however, so I extended with a writeccql method.

It looks like:
         writecql: function(filter, params) {
             params = params || {};
             params.cql_filter = params.cql_filter || [];
             var className = filter.CLASS_NAME;
             var filterType = 
className.substring(className.lastIndexOf(".") + 1);
             switch (filterType) {
                 case "Comparison":
                     var op = cmpToStr[filter.type];
                     if (op !== undefined) {
                         var value = filter.value;
                         if (filter.type == 
OpenLayers.Filter.Comparison.LIKE) {
                             op = 'like';
                             value =  "'" + 
value.toLowerCase().replace(/\*/g, "\%") + "'";
                             filter.property = 'strToLowerCase(' + 
filter.property + ')';
                       }
                         params.cql_filter = params.cql_filter + 
filter.property + " " + op + " " + value;
                     } else {
                         OpenLayers.Console.warn(
                             "Unknown comparison filter type " + 
filter.type);
                     }
                     break;
                 case "Logical":
                     if (filter.type === OpenLayers.Filter.Logical.AND) {
                         for (var i=0,len=filter.filters.length; i<len; 
i++) {
                             if (params.cql_filter !="") 
params.cql_filter = params.cql_filter + ' AND ';
                             params = this.writecql(filter.filters[i], 
params);                                                      ;
                         }
                     } else if (filter.type === 
OpenLayers.Filter.Logical.OR) {
                         for (var i=0,len=filter.filters.length; i<len; 
i++) {
                             if (params.cql_filter !="") 
params.cql_filter = params.cql_filter + ' OR ';
                             params = this.writecql(filter.filters[i], 
params);
                         }
                     } else {
                         OpenLayers.Console.warn(
                             "Unsupported logical filter type " + 
filter.type);
                     }
                     break;
                 default:
                     OpenLayers.Console.warn("Unknown filter type " + 
filterType);
             }
             return params;
         },

Now it should be possible to combine this into ordinary write and 
support ilike AND parameters but I didnt figure that out. Note also that 
this version actually creates an ECQL filter for geoserver for Like 
comparison operators. This will only work with geoserver2.1 and others 
that support ECQL. For more general use, remove the strToLowerCase() 
function on the like. Unfortunately, this make LIKE case-sensitive.

I would love someone to tidy this up, but if not, I hope this saves 
someone a lot of work.
-- 
Phil Scadden, Senior Scientist GNS Science Ltd 764 Cumberland St, 
Private Bag 1930, Dunedin, New Zealand Ph +64 3 4799663, fax +64 3 477 5232


Notice: This email and any attachments are confidential. If received in error please destroy and immediately notify us. Do not copy or disclose the contents.



More information about the Users mailing list