2
* Copyright (c) 2008-2010 The Open Source Geospatial Foundation
4
* Published under the BSD license.
5
* See http://svn.geoext.org/core/trunk/geoext/license.txt for the full text
10
* @include GeoExt/widgets/FeatureRenderer.js
11
* @requires GeoExt/widgets/LayerLegend.js
16
* class = VectorLegend
20
* GeoExt/widgets/LayerLegend.js
23
Ext.namespace('GeoExt');
26
* .. class:: VectorLegend(config)
28
* Create a vector legend.
30
GeoExt.VectorLegend = Ext.extend(GeoExt.LayerLegend, {
32
/** api: config[layerRecord]
33
* :class:`GeoExt.data.LayerRecord`
34
* The record containing a vector layer that this legend will be based on.
35
* One of ``layerRecord``, ``layer``, or ``rules`` must be specified in
40
/** api: config[layer]
41
* ``OpenLayers.Layer.Vector``
42
* The layer that this legend will be based on. One of ``layer``,
43
* ``rules``, or ``layerRecord`` must be specified in the config.
47
/** api: config[rules]
48
* ``Array(OpenLayers.Rule)``
49
* List of rules. One of ``rules``, ``layer``, or ``layerRecord`` must be
50
* specified in the config. The ``symbolType`` property must also be
51
* provided if only ``rules`` are given in the config.
55
/** api: config[symbolType]
57
* The symbol type for legend swatches. Must be one of ``"Point"``,
58
* ``"Line"``, or ``"Polygon"``. If not provided, the ``layer`` or
59
* ``layerRecord`` config property must be specified, and the geometry type
60
* of the first feature found on the layer will be used. If a rule does
61
* not have a symbolizer for ``symbolType``, we look at the symbolizers
62
* for the rule, and see if it has a ``"Point"``, ``"Line"`` or
63
* ``"Polygon"`` symbolizer, which we use for rendering a swatch of the
64
* respective geometry type.
68
/** api: config[untitledPrefix]
70
* The prefix to use as a title for rules with no title or
71
* name. Default is ``"Untitled "``. Prefix will be appended with a
74
untitledPrefix: "Untitled ",
76
/** api: config[clickableSymbol]
78
* Set cursor style to "pointer" for symbolizers. Register for
79
* the ``symbolclick`` event to handle clicks. Note that click events
80
* are fired regardless of this value. If ``false``, no cursor style will
81
* be set. Default is ``false``.
83
clickableSymbol: false,
85
/** api: config[clickableTitle]
87
* Set cursor style to "pointer" for rule titles. Register for
88
* the ``titleclick`` event to handle clicks. Note that click events
89
* are fired regardless of this value. If ``false``, no cursor style will
90
* be set. Default is ``false``.
92
clickableTitle: false,
94
/** api: config[selectOnClick]
96
* Set to true if a rule should be selected by clicking on the
97
* symbol or title. Selection will trigger the ruleselected event, and
98
* a click on a selected rule will unselect it and trigger the
99
* ``ruleunselected`` event. Default is ``false``.
101
selectOnClick: false,
103
/** api: config[enableDD]
105
* Allow drag and drop of rules. Default is ``false``.
109
/** api: config[bodyBorder]
111
* Show a border around the legend panel. Default is ``false``.
115
/** private: property[feature]
116
* ``OpenLayers.Feature.Vector``
117
* Cached feature for rendering.
121
/** private: property[selectedRule]
122
* ``OpenLayers.Rule``
123
* The rule that is currently selected.
127
/** private: property[currentScaleDenominator]
129
* The current scale denominator of any map associated with this
130
* legend. Use :meth`setCurrentScaleDenominator` to change this. If not
131
* set an entry for each rule will be rendered. If set, only rules that
132
* apply for the given scale will be rendered.
134
currentScaleDenominator: null,
136
/** private: property[untitledCount]
138
* Last number used for untitled rule.
142
/** private: method[initComponent]
143
* Initializes the Vector legend.
145
initComponent: function() {
146
GeoExt.VectorLegend.superclass.initComponent.call(this);
147
if (this.layerRecord) {
148
this.layer = this.layerRecord.get("layer");
149
if (this.layer.map) {
150
this.currentScaleDenominator = this.layer.map.getScale();
151
this.layer.map.events.on({
152
"zoomend": this.onMapZoom,
158
// determine symbol type
159
if (!this.symbolType) {
161
this.symbolType = this.symbolTypeFromFeature(this.feature);
162
} else if (this.layer) {
163
if (this.layer.features.length > 0) {
164
var feature = this.layer.features[0].clone();
165
feature.attributes = {};
166
this.feature = feature;
167
this.symbolType = this.symbolTypeFromFeature(this.feature);
169
this.layer.events.on({
170
featuresadded: this.onFeaturesAdded,
177
// set rules if not provided
178
if (this.layer && this.feature && !this.rules) {
182
this.rulesContainer = new Ext.Container({
186
this.add(this.rulesContainer);
189
/** api: event[titleclick]
190
* Fires when a rule title is clicked.
192
* Listener arguments:
194
* * comp - :class:`GeoExt.VectorLegend`` This component.
195
* * rule - ``OpenLayers.Rule`` The rule whose title was clicked.
199
/** api: event[symbolclick]
200
* Fires when a rule symbolizer is clicked.
202
* Listener arguments:
204
* * comp - :class:`GeoExt.VectorLegend`` This component.
205
* * rule - ``OpenLayers.Rule`` The rule whose symbol was clicked.
209
/** api: event[ruleclick]
210
* Fires when a rule entry is clicked (fired with symbolizer or
213
* Listener arguments:
215
* * comp - :class:`GeoExt.VectorLegend`` This component.
216
* * rule - ``OpenLayers.Rule`` The rule that was clicked.
220
/** api: event[ruleselected]
221
* Fires when a rule is clicked and ``selectOnClick`` is set to
224
* Listener arguments:
226
* * comp - :class:`GeoExt.VectorLegend`` This component.
227
* * rule - ``OpenLayers.Rule`` The rule that was selected.
231
/** api: event[ruleunselected]
232
* Fires when the selected rule is clicked and ``selectOnClick``
233
* is set to ``true``, or when a rule is unselected by selecting a
236
* Listener arguments:
238
* * comp - :class:`GeoExt.VectorLegend`` This component.
239
* * rule - ``OpenLayers.Rule`` The rule that was unselected.
243
/** api: event[rulemoved]
244
* Fires when a rule is moved.
246
* Listener arguments:
248
* * comp - :class:`GeoExt.VectorLegend`` This component.
249
* * rule - ``OpenLayers.Rule`` The rule that was moved.
257
/** private: method[onMapZoom]
258
* Listener for map zoomend.
260
onMapZoom: function() {
261
this.setCurrentScaleDenominator(
262
this.layer.map.getScale()
266
/** private: method[symbolTypeFromFeature]
267
* :arg feature: ``OpenLayers.Feature.Vector``
269
* Determine the symbol type given a feature.
271
symbolTypeFromFeature: function(feature) {
272
var match = feature.geometry.CLASS_NAME.match(/Point|Line|Polygon/);
273
return (match && match[0]) || "Point";
276
/** private: method[onFeaturesAdded]
277
* Set as a one time listener for the ``featuresadded`` event on the layer
278
* if it was provided with no features originally.
280
onFeaturesAdded: function() {
281
this.layer.events.un({
282
featuresadded: this.onFeaturesAdded,
285
var feature = this.layer.features[0].clone();
286
feature.attributes = {};
287
this.feature = feature;
288
this.symbolType = this.symbolTypeFromFeature(this.feature);
295
/** private: method[setRules]
296
* Sets the ``rules`` property for this. This is called when the component
297
* is constructed without rules. Rules will be derived from the layer's
298
* style map if it has one.
300
setRules: function() {
301
var style = this.layer.styleMap && this.layer.styleMap.styles["default"];
303
style = new OpenLayers.Style();
305
if (style.rules.length === 0) {
307
new OpenLayers.Rule({
308
symbolizer: style.createSymbolizer(this.feature)
312
this.rules = style.rules;
316
/** api: method[setCurrentScaleDenominator]
317
* :arg scale: ``Number`` The scale denominator.
319
* Set the current scale denominator. This will hide entries for any
320
* rules that don't apply at the current scale.
322
setCurrentScaleDenominator: function(scale) {
323
if (scale !== this.currentScaleDenominator) {
324
this.currentScaleDenominator = scale;
329
/** private: method[getRuleEntry]
330
* :arg rule: ``OpenLayers.Rule``
331
* :returns: ``Ext.Container``
333
* Get the item corresponding to the rule.
335
getRuleEntry: function(rule) {
336
return this.rulesContainer.items.get(this.rules.indexOf(rule));
339
/** private: method[addRuleEntry]
340
* :arg rule: ``OpenLayers.Rule``
341
* :arg noDoLayout: ``Boolean`` Don't call doLayout after adding rule.
342
* Default is ``false``.
344
* Add a new rule entry in the rules container. This
345
* method does not add the rule to the rules array.
347
addRuleEntry: function(rule, noDoLayout) {
348
this.rulesContainer.add(this.createRuleEntry(rule));
354
/** private: method[removeRuleEntry]
355
* :arg rule: ``OpenLayers.Rule``
356
* :arg noDoLayout: ``Boolean`` Don't call doLayout after removing rule.
357
* Default is ``false``.
359
* Remove a rule entry from the rules container, this
360
* method assumes the rule is in the rules array, and
361
* it does not remove the rule from the rules array.
363
removeRuleEntry: function(rule, noDoLayout) {
364
var ruleEntry = this.getRuleEntry(rule);
366
this.rulesContainer.remove(ruleEntry);
373
/** private: method[selectRuleEntry]
375
selectRuleEntry: function(rule) {
376
var newSelection = rule != this.selectedRule;
377
if (this.selectedRule) {
381
var ruleEntry = this.getRuleEntry(rule);
382
ruleEntry.body.addClass("x-grid3-row-selected");
383
this.selectedRule = rule;
384
this.fireEvent("ruleselected", this, rule);
388
/** private: method[unselect]
390
unselect: function() {
391
this.rulesContainer.items.each(function(item, i) {
392
if (this.rules[i] == this.selectedRule) {
393
item.body.removeClass("x-grid3-row-selected");
394
this.selectedRule = null;
395
this.fireEvent("ruleunselected", this, this.rules[i]);
400
/** private: method[createRuleEntry]
402
createRuleEntry: function(rule) {
404
if (this.currentScaleDenominator != null) {
405
if (rule.minScaleDenominator) {
406
applies = applies && (this.currentScaleDenominator >= rule.minScaleDenominator);
408
if (rule.maxScaleDenominator) {
409
applies = applies && (this.currentScaleDenominator < rule.maxScaleDenominator);
417
bodyStyle: this.selectOnClick ? {cursor: "pointer"} : undefined,
422
this.createRuleRenderer(rule),
423
this.createRuleTitle(rule)
426
render: function(comp){
427
this.selectOnClick && comp.getEl().on({
428
click: function(comp){
429
this.selectRuleEntry(rule);
433
if (this.enableDD == true) {
442
/** private: method[createRuleRenderer]
443
* :arg rule: ``OpenLayers.Rule``
444
* :returns: ``GeoExt.FeatureRenderer``
446
* Create a renderer for the rule.
448
createRuleRenderer: function(rule) {
449
var symbolizer = rule.symbolizer;
450
var types = [this.symbolType, "Point", "Line", "Polygon"];
452
for(var i=0, len=types.length; i<len; ++i) {
454
if(symbolizer[type]) {
455
symbolizer = symbolizer[type];
461
xtype: "gx_renderer",
462
symbolType: haveType === true ? type : this.symbolType,
463
symbolizers: [symbolizer],
464
style: this.clickableSymbol ? {cursor: "pointer"} : undefined,
467
if (this.clickableSymbol) {
468
this.fireEvent("symbolclick", this, rule);
469
this.fireEvent("ruleclick", this, rule);
477
/** private: method[createRuleTitle]
478
* :arg rule: ``OpenLayers.Rule``
479
* :returns: ``Ext.Component``
481
* Create a title component for the rule.
483
createRuleTitle: function(rule) {
486
style: "padding: 0.2em 0.5em 0;", // TODO: css
487
bodyStyle: Ext.applyIf({background: "transparent"},
488
this.clickableTitle ? {cursor: "pointer"} : undefined),
489
html: this.getRuleTitle(rule),
491
render: function(comp) {
492
this.clickableTitle && comp.getEl().on({
494
this.fireEvent("titleclick", this, rule);
495
this.fireEvent("ruleclick", this, rule);
505
/** private: method[addDD]
506
* :arg component: ``Ext.Component``
508
* Adds drag & drop functionality to a rule entry.
510
addDD: function(component) {
511
var ct = component.ownerCt;
513
new Ext.dd.DragSource(component.getEl(), {
515
onDragOut: function(e, targetId) {
516
var target = Ext.getCmp(targetId);
517
target.removeClass("gx-ruledrag-insert-above");
518
target.removeClass("gx-ruledrag-insert-below");
519
return Ext.dd.DragZone.prototype.onDragOut.apply(this, arguments);
521
onDragEnter: function(e, targetId) {
522
var target = Ext.getCmp(targetId);
524
var sourcePos = ct.items.indexOf(component);
525
var targetPos = ct.items.indexOf(target);
526
if (sourcePos > targetPos) {
527
cls = "gx-ruledrag-insert-above";
528
} else if (sourcePos < targetPos) {
529
cls = "gx-ruledrag-insert-below";
531
cls && target.addClass(cls);
532
return Ext.dd.DragZone.prototype.onDragEnter.apply(this, arguments);
534
onDragDrop: function(e, targetId) {
535
panel.moveRule(ct.items.indexOf(component),
536
ct.items.indexOf(Ext.getCmp(targetId)));
537
return Ext.dd.DragZone.prototype.onDragDrop.apply(this, arguments);
539
getDragData: function(e) {
540
var sourceEl = e.getTarget(".x-column-inner");
542
var d = sourceEl.cloneNode(true);
546
repairXY: Ext.fly(sourceEl).getXY(),
552
new Ext.dd.DropTarget(component.getEl(), {
554
notifyDrop: function() {
560
/** api: method[update]
561
* Update rule titles and symbolizers.
564
GeoExt.VectorLegend.superclass.update.apply(this, arguments);
565
if (this.symbolType && this.rules) {
566
if (this.rulesContainer.items) {
568
for (var i=this.rulesContainer.items.length-1; i>=0; --i) {
569
comp = this.rulesContainer.getComponent(i);
570
this.rulesContainer.remove(comp, true);
573
for (var i=0, ii=this.rules.length; i<ii; ++i) {
574
this.addRuleEntry(this.rules[i], true);
577
if (this.selectedRule) {
578
this.getRuleEntry(this.selectedRule).body.addClass("x-grid3-row-selected");
583
/** private: method[updateRuleEntry]
584
* :arg rule: ``OpenLayers.Rule``
586
* Update the renderer and the title of a rule.
588
updateRuleEntry: function(rule) {
589
var ruleEntry = this.getRuleEntry(rule);
591
ruleEntry.removeAll();
592
ruleEntry.add(this.createRuleRenderer(rule));
593
ruleEntry.add(this.createRuleTitle(rule));
594
ruleEntry.doLayout();
598
/** private: method[moveRule]
600
moveRule: function(sourcePos, targetPos) {
601
var srcRule = this.rules[sourcePos];
602
this.rules.splice(sourcePos, 1);
603
this.rules.splice(targetPos, 0, srcRule);
605
this.fireEvent("rulemoved", this, srcRule);
608
/** private: method[getRuleTitle]
609
* :returns: ``String``
611
* Get a rule title given a rule.
613
getRuleTitle: function(rule) {
614
return rule.title || rule.name || (this.untitledPrefix + (++this.untitledCount));
617
/** private: method[beforeDestroy]
620
beforeDestroy: function() {
622
if (this.layer.events) {
623
this.layer.events.un({
624
featuresadded: this.onFeaturesAdded,
628
if (this.layer.map && this.layer.map.events) {
629
this.layer.map.events.un({
630
"zoomend": this.onMapZoom,
637
GeoExt.VectorLegend.superclass.beforeDestroy.apply(this, arguments);
642
/** private: method[supports]
645
GeoExt.VectorLegend.supports = function(layerRecord) {
646
return layerRecord.get("layer") instanceof OpenLayers.Layer.Vector;
649
/** api: legendtype = gx_vectorlegend */
650
GeoExt.LayerLegend.types["gx_vectorlegend"] = GeoExt.VectorLegend;
652
/** api: xtype = gx_vectorlegend */
653
Ext.reg("gx_vectorlegend", GeoExt.VectorLegend);