SLDSelect.js 21.2 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572
/* Copyright (c) 2006-2011 by OpenLayers Contributors (see authors.txt for 
 * full list of contributors). Published under the Clear BSD license.  
 * See http://svn.openlayers.org/trunk/openlayers/license.txt for the
 * full text of the license. */

/**
 * @requires OpenLayers/Control.js
 * @requires OpenLayers/Layer/WMS.js
 * @requires OpenLayers/Handler/RegularPolygon.js
 * @requires OpenLayers/Handler/Polygon.js
 * @requires OpenLayers/Handler/Path.js
 * @requires OpenLayers/Handler/Click.js
 * @requires OpenLayers/Filter/Spatial.js
 * @requires OpenLayers/Format/SLD/v1_0_0.js
 */

/**
 * Class: OpenLayers.Control.SLDSelect
 * Perform selections on WMS layers using Styled Layer Descriptor (SLD)
 *
 * Inherits from:
 *  - <OpenLayers.Control>
 */
OpenLayers.Control.SLDSelect = OpenLayers.Class(OpenLayers.Control, {

    /**
     * Constant: EVENT_TYPES
     * {Array(String)} Supported application event types.  Register a listener
     *     for a particular event with the following syntax:
     * (code)
     * control.events.register(type, obj, listener);
     * (end)
     *
     * Listeners will be called with a reference to an event object.  The
     *     properties of this event depends on exactly what happened.
     *
     * Supported control event types (in addition to those from 
     * <OpenLayers.Control>):
     * selected - Triggered when a selection occurs.  Listeners receive an 
     *     event with *filters* and *layer* properties.  Filters will be an 
     *     array of OpenLayers.Filter objects created in order to perform 
     *     the particular selection.
     */
    EVENT_TYPES: ["selected"],

    /**
     * APIProperty: clearOnDeactivate
     * {Boolean} Should the selection be cleared when the control is 
     *     deactivated. Default value is false.
     */
    clearOnDeactivate: false,

    /**
     * APIProperty: layers
     * {Array(<OpenLayers.Layer.WMS>)} The WMS layers this control will work 
     *     on.
     */
    layers: null,

    /**
     * Property: callbacks
     * {Object} The functions that are sent to the handler for callback
     */
    callbacks: null,

    /**
     * APIProperty: selectionSymbolizer
     * {Object} Determines the styling of the selected objects. Default is
     *     a selection in red.
     */
    selectionSymbolizer: {
        'Polygon': {fillColor: '#FF0000', stroke: false},
        'Line': {strokeColor: '#FF0000', strokeWidth: 2},
        'Point': {graphicName: 'square', fillColor: '#FF0000', pointRadius: 5}
    },

    /**
     * APIProperty: layerOptions
     * {Object} The options to apply to the selection layer, by default the
     *     selection layer will be kept out of the layer switcher.
     */
    layerOptions: null,

    /**
     * APIProperty: handlerOptions
     * {Object} Used to set non-default properties on the control's handler
     */
    handlerOptions: null,

    /**
     * APIProperty: sketchStyle
     * {<OpenLayers.Style>|Object} Style or symbolizer to use for the sketch
     * handler. The recommended way of styling the sketch layer, however, is
     * to configure an <OpenLayers.StyleMap> in the layerOptions of the
     * <handlerOptions>:
     * 
     * (code)
     * new OpenLayers.Control.SLDSelect(OpenLayers.Handler.Path, {
     *     handlerOptions: {
     *         layerOptions: {
     *             styleMap: new OpenLayers.StyleMap({
     *                 "default": {strokeColor: "yellow"}
     *             });
     *         }
     *     }
     * });
     * (end)
     */
    sketchStyle: null,

    /**
     * APIProperty: wfsCache
     * {Object} Cache to use for storing parsed results from
     *     <OpenLayers.Format.WFSDescribeFeatureType.read>. If not provided,
     *     these will be cached on the prototype.
     */
    wfsCache: {},

    /**
     * APIProperty: layerCache
     * {Object} Cache to use for storing references to the selection layers.
     *     Normally each source layer will have exactly 1 selection layer of
     *     type OpenLayers.Layer.WMS. If not provided, layers will
     *     be cached on the prototype. Note that if <clearOnDeactivate> is
     *     true, the layer will no longer be cached after deactivating the
     *     control.
     */
    layerCache: {},

    /**
     * Constructor: OpenLayers.Control.SLDSelect
     * Create a new control for selecting features in WMS layers using
     *     Styled Layer Descriptor (SLD).
     *
     * Parameters:
     * handler - {<OpenLayers.Class>} A sketch handler class. This determines
     *     the type of selection, e.g. box (<OpenLayers.Handler.Box>), point
     *     (<OpenLayers.Handler.Point>), path (<OpenLayers.Handler.Path>) or
     *     polygon (<OpenLayers.Handler.Polygon>) selection. To use circle
     *     type selection, use <OpenLayers.Handler.RegularPolygon> and pass
     *     the number of desired sides (e.g. 40) as "sides" property to the
     *     <handlerOptions>.
     * options - {Object} An object containing all configuration properties for
     *     the control.
     *
     * Valid options:
     * layers - Array({<OpenLayers.Layer.WMS>}) The layers to perform the
     *     selection on.
     */
    initialize: function(handler, options) {
        // concatenate events specific to this control with those from the base
        this.EVENT_TYPES =
            OpenLayers.Control.SLDSelect.prototype.EVENT_TYPES.concat(
            OpenLayers.Control.prototype.EVENT_TYPES
        );
        OpenLayers.Control.prototype.initialize.apply(this, [options]);

        this.callbacks = OpenLayers.Util.extend({done: this.select, 
            click: this.select}, this.callbacks);
        this.handlerOptions = this.handlerOptions || {};
        this.layerOptions = OpenLayers.Util.applyDefaults(this.layerOptions, {
            displayInLayerSwitcher: false,
            tileOptions: {maxGetUrlLength: 2048}
        });
        if (this.sketchStyle) {
            this.handlerOptions.layerOptions = OpenLayers.Util.applyDefaults(
                this.handlerOptions.layerOptions,
                {styleMap: new OpenLayers.StyleMap({"default": this.sketchStyle})}
            );
        }
        this.handler = new handler(this, this.callbacks, this.handlerOptions);
    },

    /**
     * APIMethod: destroy
     * Take care of things that are not handled in superclass.
     */
    destroy: function() {
        for (var key in this.layerCache) {
            delete this.layerCache[key];
        }
        for (var key in this.wfsCache) {
            delete this.wfsCache[key];
        }
        OpenLayers.Control.prototype.destroy.apply(this, arguments);
    },

    /**
     * Method: coupleLayerVisiblity
     * Couple the selection layer and the source layer with respect to
     *     layer visibility. So if the source layer is turned off, the
     *     selection layer is also turned off.
     *
     * Parameters:
     * evt - {Object}
     */
    coupleLayerVisiblity: function(evt) {
        this.setVisibility(evt.object.getVisibility());
    },

    /**
     * Method: createSelectionLayer
     * Creates a "clone" from the source layer in which the selection can
     * be drawn. This ensures both the source layer and the selection are 
     * visible and not only the selection.
     *
     * Parameters:
     * source - {<OpenLayers.Layer.WMS>} The source layer on which the selection
     *     is performed.
     *
     * Returns:
     * {<OpenLayers.Layer.WMS>} A WMS layer with maxGetUrlLength configured to 2048
     *     since SLD selections can easily get quite long.
     */
    createSelectionLayer: function(source) {
        // check if we already have a selection layer for the source layer
        var selectionLayer;
        if (!this.layerCache[source.id]) {
            selectionLayer = new OpenLayers.Layer.WMS(source.name, 
                source.url, source.params, 
                OpenLayers.Util.applyDefaults(
                    this.layerOptions,
                    source.getOptions())
            );
            this.layerCache[source.id] = selectionLayer;
            // make sure the layers are coupled wrt visibility, but only
            // if they are not displayed in the layer switcher, because in
            // that case the user cannot control visibility.
            if (this.layerOptions.displayInLayerSwitcher === false) {
                source.events.on({
                    "visibilitychanged": this.coupleLayerVisiblity,
                    scope: selectionLayer});
            }
            this.map.addLayer(selectionLayer);
        } else {
            selectionLayer = this.layerCache[source.id];
        }
        return selectionLayer;
    },

    /**
     * Method: createSLD
     * Create the SLD document for the layer using the supplied filters.
     *
     * Parameters:
     * layer - {<OpenLayers.Layer.WMS>}
     * filters - Array({<OpenLayers.Filter>}) The filters to be applied.
     * geometryAttributes - Array({Object}) The geometry attributes of the 
     *     layer.
     *
     * Returns:
     * {String} The SLD document generated as a string.
     */
    createSLD: function(layer, filters, geometryAttributes) {
        var sld = {version: "1.0.0", namedLayers: {}};
        var layerNames = [layer.params.LAYERS].join(",").split(",");
        for (var i=0, len=layerNames.length; i<len; i++) { 
            var name = layerNames[i];
            sld.namedLayers[name] = {name: name, userStyles: []};
            var symbolizer = this.selectionSymbolizer;
            var geometryAttribute = geometryAttributes[i];
            if (geometryAttribute.type.indexOf('Polygon') >= 0) {
                symbolizer = {Polygon: this.selectionSymbolizer['Polygon']};
            } else if (geometryAttribute.type.indexOf('LineString') >= 0) {
                symbolizer = {Line: this.selectionSymbolizer['Line']};
            } else if (geometryAttribute.type.indexOf('Point') >= 0) {
                symbolizer = {Point: this.selectionSymbolizer['Point']};
            }
            var filter = filters[i];
            sld.namedLayers[name].userStyles.push({name: 'default', rules: [
                new OpenLayers.Rule({symbolizer: symbolizer, 
                    filter: filter, 
                    maxScaleDenominator: layer.options.minScale})
            ]});
        }
        return new OpenLayers.Format.SLD({srsName: this.map.getProjection()}).write(sld);
    },

    /**
     * Method: parseDescribeLayer
     * Parse the SLD WMS DescribeLayer response and issue the corresponding
     *     WFS DescribeFeatureType request
     *
     * request - {XMLHttpRequest} The request object.
     */
    parseDescribeLayer: function(request) {
        var format = new OpenLayers.Format.WMSDescribeLayer();
        var doc = request.responseXML;
        if(!doc || !doc.documentElement) {
            doc = request.responseText;
        }
        var describeLayer = format.read(doc);
        var typeNames = [];
        var url = null;
        for (var i=0, len=describeLayer.length; i<len; i++) {
            // perform a WFS DescribeFeatureType request
            if (describeLayer[i].owsType == "WFS") {
                typeNames.push(describeLayer[i].typeName);
                url = describeLayer[i].owsURL;
            }
        }
        var options = {
            url: url,
            params: {
                SERVICE: "WFS",
                TYPENAME: typeNames.toString(),
                REQUEST: "DescribeFeatureType",
                VERSION: "1.0.0"
            },
            callback: function(request) {
                var format = new OpenLayers.Format.WFSDescribeFeatureType();
                var doc = request.responseXML;
                if(!doc || !doc.documentElement) {
                    doc = request.responseText;
                }
                var describeFeatureType = format.read(doc);
                this.control.wfsCache[this.layer.id] = describeFeatureType;
                this.control._queue && this.control.applySelection();
            },
            scope: this
        };
        OpenLayers.Request.GET(options);
    },

   /**
    * Method: getGeometryAttributes
    * Look up the geometry attributes from the WFS DescribeFeatureType response
    *
    * Parameters:
    * layer - {<OpenLayers.Layer.WMS>} The layer for which to look up the 
    *     geometry attributes.
    *
    * Returns:
    * Array({Object}) Array of geometry attributes
    */ 
   getGeometryAttributes: function(layer) {
        var result = [];
        var cache = this.wfsCache[layer.id];
        for (var i=0, len=cache.featureTypes.length; i<len; i++) {
            var typeName = cache.featureTypes[i];
            var properties = typeName.properties;
            for (var j=0, lenj=properties.length; j < lenj; j++) {
                var property = properties[j];
                var type = property.type;
                if ((type.indexOf('LineString') >= 0) ||
                    (type.indexOf('GeometryAssociationType') >=0) ||
                    (type.indexOf('GeometryPropertyType') >= 0) ||
                    (type.indexOf('Point') >= 0) ||
                    (type.indexOf('Polygon') >= 0) ) {
                        result.push(property);
                }
            }
        }
        return result;
    },

    /**
     * APIMethod: activate
     * Activate the control. Activating the control will perform a SLD WMS
     *     DescribeLayer request followed by a WFS DescribeFeatureType request
     *     so that the proper symbolizers can be chosen based on the geometry
     *     type.
     */
    activate: function() {
        var activated = OpenLayers.Control.prototype.activate.call(this);
        if(activated) {
            for (var i=0, len=this.layers.length; i<len; i++) {
                var layer = this.layers[i];
                if (layer && !this.wfsCache[layer.id]) {
                    var options = {
                        url: layer.url,
                        params: {
                            SERVICE: "WMS",
                            VERSION: layer.params.VERSION,
                            LAYERS: layer.params.LAYERS,
                            REQUEST: "DescribeLayer"
                        },
                        callback: this.parseDescribeLayer,
                        scope: {layer: layer, control: this}
                    };
                    OpenLayers.Request.GET(options);
                }
            }
        }
        return activated;
    },

    /**
     * APIMethod: deactivate
     * Deactivate the control. If clearOnDeactivate is true, remove the
     *     selection layer(s).
     */
    deactivate: function() {
        var deactivated = OpenLayers.Control.prototype.deactivate.call(this);
        if(deactivated) {
            for (var i=0, len=this.layers.length; i<len; i++) {
                var layer = this.layers[i];
                if (layer && this.clearOnDeactivate === true) {
                    var layerCache = this.layerCache;
                    var selectionLayer = layerCache[layer.id];
                    if (selectionLayer) {
                        layer.events.un({
                            "visibilitychanged": this.coupleLayerVisiblity,
                            scope: selectionLayer});
                        selectionLayer.destroy();
                        delete layerCache[layer.id];
                    }
                }
            }
        }
        return deactivated;
    },

    /**
     * APIMethod: setLayers
     * Set the layers on which the selection should be performed.  Call the 
     *     setLayers method if the layer(s) to be used change and the same 
     *     control should be used on a new set of layers.
     *     If the control is already active, it will be active after the new
     *     set of layers is set.
     *
     * Parameters:
     * layers - {Array(<OpenLayers.Layer.WMS>)}  The new set of layers on which 
     *     the selection should be performed.
     */
    setLayers: function(layers) {
        if(this.active) {
            this.deactivate();
            this.layers = layers;
            this.activate();
        } else {
            this.layers = layers;
        }
    },

    /**
     * Function: createFilter
     * Create the filter to be used in the SLD.
     *
     * Parameters:
     * geometryAttribute - {Object} Used to get the name of the geometry 
     *     attribute which is needed for constructing the spatial filter.
     * geometry - {<OpenLayers.Geometry>} The geometry to use.
     *
     * Returns:
     * {<OpenLayers.Filter.Spatial>} The spatial filter created.
     */
    createFilter: function(geometryAttribute, geometry) {
        var filter = null;
        if (this.handler instanceof OpenLayers.Handler.RegularPolygon) {
            // box
            if (this.handler.irregular === true) {
                filter = new OpenLayers.Filter.Spatial({
                    type: OpenLayers.Filter.Spatial.BBOX,
                    property: geometryAttribute.name,
                    value: geometry.getBounds()}
                );
            } else {
                filter = new OpenLayers.Filter.Spatial({
                    type: OpenLayers.Filter.Spatial.INTERSECTS,
                    property: geometryAttribute.name,
                    value: geometry}
                );
            }
        } else if (this.handler instanceof OpenLayers.Handler.Polygon) {
            filter = new OpenLayers.Filter.Spatial({
                type: OpenLayers.Filter.Spatial.INTERSECTS,
                property: geometryAttribute.name,
                value: geometry}
            );
        } else if (this.handler instanceof OpenLayers.Handler.Path) {
            // if source layer is point based, use DWITHIN instead
            if (geometryAttribute.type.indexOf('Point') >= 0) {
                filter = new OpenLayers.Filter.Spatial({
                    type: OpenLayers.Filter.Spatial.DWITHIN,
                    property: geometryAttribute.name,
                    distance: this.map.getExtent().getWidth()*0.01 ,
                    distanceUnits: this.map.getUnits(),
                    value: geometry}
                );
            } else {
                filter = new OpenLayers.Filter.Spatial({
                    type: OpenLayers.Filter.Spatial.INTERSECTS,
                    property: geometryAttribute.name,
                    value: geometry}
                );
            }
        } else if (this.handler instanceof OpenLayers.Handler.Click) {
            if (geometryAttribute.type.indexOf('Polygon') >= 0) {
                filter = new OpenLayers.Filter.Spatial({
                    type: OpenLayers.Filter.Spatial.INTERSECTS,
                    property: geometryAttribute.name,
                    value: geometry}
                );
            } else {
                filter = new OpenLayers.Filter.Spatial({
                    type: OpenLayers.Filter.Spatial.DWITHIN,
                    property: geometryAttribute.name,
                    distance: this.map.getExtent().getWidth()*0.01 ,
                    distanceUnits: this.map.getUnits(),
                    value: geometry}
                );
            }
        }
        return filter;
    },

    /**
     * Method: select
     * When the handler is done, use SLD_BODY on the selection layer to
     *     display the selection in the map.
     *
     * Parameters:
     * geometry - {Object} or {<OpenLayers.Geometry>}
     */
    select: function(geometry) {
        this._queue = function() {
            for (var i=0, len=this.layers.length; i<len; i++) {
                var layer = this.layers[i];
                var geometryAttributes = this.getGeometryAttributes(layer);
                var filters = [];
                for (var j=0, lenj=geometryAttributes.length; j<lenj; j++) {
                    var geometryAttribute = geometryAttributes[j];
                    if (geometryAttribute !== null) {
                        // from the click handler we will not get an actual 
                        // geometry so transform
                        if (!(geometry instanceof OpenLayers.Geometry)) {
                            var point = this.map.getLonLatFromPixel(
                                geometry.xy);
                            geometry = new OpenLayers.Geometry.Point(
                                point.lon, point.lat);
                        }
                        var filter = this.createFilter(geometryAttribute,
                        geometry);
                        if (filter !== null) {
                            filters.push(filter);
                        }
                    }
                }
    
                var selectionLayer = this.createSelectionLayer(layer);
                var sld = this.createSLD(layer, filters, geometryAttributes);
    
                this.events.triggerEvent("selected", {
                    layer: layer,
                    filters: filters
                });
    
                selectionLayer.mergeNewParams({SLD_BODY: sld});
                delete this._queue;
            }
        };
        this.applySelection();
    },
    
    /**
     * Method: applySelection
     * Checks if all required wfs data is cached, and applies the selection
     */
    applySelection: function() {
        var canApply = true;
        for (var i=0, len=this.layers.length; i<len; i++) {
            if(!this.wfsCache[this.layers[i].id]) {
                canApply = false;
                break;
            }
        }
        canApply && this._queue.call(this);
    },

    CLASS_NAME: "OpenLayers.Control.SLDSelect"
});