Free cookie consent management tool by TermsFeed Policy Generator

source: trunk/sources/HeuristicLab.Operators.Views.GraphVisualization/3.3/Model/GraphVisualizationInfo.cs @ 3344

Last change on this file since 3344 was 3344, checked in by mkommend, 14 years ago

added reaction to operator image changes and changed collapse and expand icons (ticket #867)

File size: 20.4 KB
RevLine 
[2853]1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2008 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22using System;
23using System.Collections.Generic;
24using System.Linq;
25using System.Text;
26using HeuristicLab.Core;
27using Netron.Diagramming.Core;
28using System.Drawing;
29using HeuristicLab.Collections;
[2934]30using HeuristicLab.Persistence.Default.CompositeSerializers.Storable;
[2853]31
32namespace HeuristicLab.Operators.Views.GraphVisualization {
[3017]33  [StorableClass]
[2893]34  public sealed class GraphVisualizationInfo : DeepCloneable {
[2934]35    private BidirectionalLookup<IOperator, IOperatorShapeInfo> operatorShapeInfoMapping;
36    [Storable]
37    private BidirectionalLookup<IOperator, IOperatorShapeInfo> OperatorShapeInfoMappingStore {
38      get { return this.operatorShapeInfoMapping; }
39      set {
[2942]40        IOperator op;
41        IOperatorShapeInfo shapeInfo;
42        foreach (KeyValuePair<IOperator, IOperatorShapeInfo> pair in value.FirstEnumerable) {
43          op = pair.Key;
44          shapeInfo = pair.Value;
[2966]45          shapeInfo.Icon = new Bitmap(op.ItemImage);
[2942]46          this.RegisterOperatorEvents(op);
47          this.operatorParameterCollectionMapping.Add(op, pair.Key.Parameters);
48          this.operatorShapeInfoMapping.Add(op, shapeInfo);
49          this.shapeInfos.Add(shapeInfo);
50        }
51
[2968]52        foreach (IOperator oper in value.FirstValues) {
[2942]53          foreach (IParameter param in oper.Parameters) {
54            this.parameterOperatorMapping.Add(param, oper);
55            IValueParameter<IOperator> opParam = param as IValueParameter<IOperator>;
56            if (opParam != null) {
57              this.RegisterOperatorParameterEvents(opParam);
58              shapeInfo = this.operatorShapeInfoMapping.GetByFirst(oper);
59              if (opParam.Value != null) {
60                this.connections.Add(new KeyValuePair<IOperatorShapeInfo, string>(shapeInfo, param.Name), this.operatorShapeInfoMapping.GetByFirst(opParam.Value));
61              }
62            } else
63              this.RegisterParameterEvents(param);
64          }
65        }
[2934]66      }
67    }
68
[2861]69    private BidirectionalLookup<IOperator, IObservableKeyedCollection<string, IParameter>> operatorParameterCollectionMapping;
[2934]70    private Dictionary<IParameter, IOperator> parameterOperatorMapping;
[2853]71
[2893]72    private GraphVisualizationInfo() {
[2934]73      this.operatorShapeInfoMapping = new BidirectionalLookup<IOperator, IOperatorShapeInfo>();
[2861]74      this.operatorParameterCollectionMapping = new BidirectionalLookup<IOperator, IObservableKeyedCollection<string, IParameter>>();
[2934]75      this.parameterOperatorMapping = new Dictionary<IParameter, IOperator>();
[2861]76
[2934]77      this.shapeInfos = new ObservableSet<IOperatorShapeInfo>();
78      this.connections = new ObservableDictionary<KeyValuePair<IOperatorShapeInfo, string>, IOperatorShapeInfo>();
[2861]79    }
[2853]80
[2893]81    public GraphVisualizationInfo(OperatorGraph operatorGraph)
[2861]82      : this() {
[2942]83      this.OperatorGraph = operatorGraph;
[2875]84
[2853]85      foreach (IOperator op in operatorGraph.Operators)
[2934]86        if (!this.operatorShapeInfoMapping.ContainsFirst(op))
[2868]87          this.AddOperator(op);
[2853]88
[2875]89      this.UpdateInitialShape();
[2853]90    }
91
[2968]92    public override IDeepCloneable Clone(Cloner cloner) {
93      GraphVisualizationInfo clone = new GraphVisualizationInfo();
94      cloner.RegisterClonedObject(this, clone);
95      clone.operatorGraph = (OperatorGraph)cloner.Clone(this.operatorGraph);
[2970]96      clone.oldInitialShape = (IOperatorShapeInfo)cloner.Clone(this.oldInitialShape);
[2968]97      clone.oldInitialShapeColor = this.oldInitialShapeColor;
98
99      IOperator op;
100      IOperatorShapeInfo shapeInfo;
101      foreach (KeyValuePair<IOperator, IOperatorShapeInfo> pair in this.operatorShapeInfoMapping.FirstEnumerable) {
102        op = (IOperator)cloner.Clone(pair.Key);
103        shapeInfo = (IOperatorShapeInfo)cloner.Clone(pair.Value);
104        clone.RegisterOperatorEvents(op);
105        clone.operatorParameterCollectionMapping.Add(op, pair.Key.Parameters);
106        clone.operatorShapeInfoMapping.Add(op, shapeInfo);
107        clone.shapeInfos.Add(shapeInfo);
108      }
109
110      foreach (IOperator oper in clone.operatorShapeInfoMapping.FirstValues) {
111        foreach (IParameter param in oper.Parameters) {
112          clone.parameterOperatorMapping.Add(param, oper);
113          IValueParameter<IOperator> opParam = param as IValueParameter<IOperator>;
114          if (opParam != null) {
115            clone.RegisterOperatorParameterEvents(opParam);
116            shapeInfo = clone.operatorShapeInfoMapping.GetByFirst(oper);
117            if (opParam.Value != null) {
118              clone.connections.Add(new KeyValuePair<IOperatorShapeInfo, string>(shapeInfo, param.Name), clone.operatorShapeInfoMapping.GetByFirst(opParam.Value));
119            }
120          } else
121            clone.RegisterParameterEvents(param);
122        }
123      }
124
125      return clone;
126    }
127
[2861]128    public event EventHandler InitialShapeChanged;
129    private void operatorGraph_InitialOperatorChanged(object sender, EventArgs e) {
[2875]130      this.UpdateInitialShape();
131    }
132
133    private void UpdateInitialShape() {
[2934]134      IOperatorShapeInfo old = this.oldInitialShape as OperatorShapeInfo;
[2875]135      if (old != null)
[2934]136        old.Color = oldInitialShapeColor;
[2875]137
138      OperatorShapeInfo newInitialShapeInfo = this.InitialShape as OperatorShapeInfo;
139      if (newInitialShapeInfo != null) {
[2934]140        oldInitialShapeColor = newInitialShapeInfo.Color;
141        newInitialShapeInfo.Color = Color.LightGreen;
[2875]142      }
143
144      oldInitialShape = this.InitialShape;
[2861]145      if (this.InitialShapeChanged != null)
146        this.InitialShapeChanged(this, new EventArgs());
147    }
148
[2934]149    [Storable]
150    private IOperatorShapeInfo oldInitialShape;
151    [Storable]
[2875]152    private Color oldInitialShapeColor;
[2934]153    public IOperatorShapeInfo InitialShape {
[2861]154      get {
155        IOperator op = this.operatorGraph.InitialOperator;
156        if (op == null)
157          return null;
[2934]158        return this.operatorShapeInfoMapping.GetByFirst(op);
[2861]159      }
[2893]160      set {
161        if (value == null)
162          this.OperatorGraph.InitialOperator = null;
163        else
[2934]164          this.OperatorGraph.InitialOperator = this.operatorShapeInfoMapping.GetBySecond(value);
[2893]165      }
[2861]166    }
167
[2968]168
[2942]169    private OperatorGraph operatorGraph;
[2934]170    [Storable]
[2853]171    public OperatorGraph OperatorGraph {
172      get { return this.operatorGraph; }
[2942]173      private set {
174        if (this.operatorGraph != null || value == null)
175          throw new InvalidOperationException("Could not set OperatorGraph");
176
177        this.operatorGraph = value;
178        this.operatorGraph.InitialOperatorChanged += new EventHandler(operatorGraph_InitialOperatorChanged);
179        this.operatorGraph.Operators.ItemsAdded += new HeuristicLab.Collections.CollectionItemsChangedEventHandler<IOperator>(Operators_ItemsAdded);
180        this.operatorGraph.Operators.ItemsRemoved += new HeuristicLab.Collections.CollectionItemsChangedEventHandler<IOperator>(Operators_ItemsRemoved);
181        this.operatorGraph.Operators.CollectionReset += new HeuristicLab.Collections.CollectionItemsChangedEventHandler<IOperator>(Operators_CollectionReset);
182      }
[2853]183    }
184
[2934]185    private ObservableSet<IOperatorShapeInfo> shapeInfos;
186    public INotifyObservableCollectionItemsChanged<IOperatorShapeInfo> ObserveableShapeInfos {
[2853]187      get { return this.shapeInfos; }
188    }
[2934]189    public IEnumerable<IOperatorShapeInfo> OperatorShapeInfos {
[2853]190      get { return this.shapeInfos; }
191    }
[2934]192    public IOperator GetOperatorForShapeInfo(IOperatorShapeInfo shapeInfo) {
193      return this.operatorShapeInfoMapping.GetBySecond(shapeInfo);
[2893]194    }
[2853]195
[2934]196    private ObservableDictionary<KeyValuePair<IOperatorShapeInfo, string>, IOperatorShapeInfo> connections;
197    public INotifyObservableDictionaryItemsChanged<KeyValuePair<IOperatorShapeInfo, string>, IOperatorShapeInfo> ObservableConnections {
[2868]198      get { return this.connections; }
199    }
[2934]200    public IEnumerable<KeyValuePair<KeyValuePair<IOperatorShapeInfo, string>, IOperatorShapeInfo>> Connections {
[2868]201      get { return this.connections; }
202    }
203
204    #region methods to manipulate operatorgraph by the shape info
[2934]205    internal void AddShapeInfo(IOperator op, IOperatorShapeInfo shapeInfo) {
[2853]206      this.RegisterOperatorEvents(op);
[2861]207      this.operatorParameterCollectionMapping.Add(op, op.Parameters);
[2934]208      this.operatorShapeInfoMapping.Add(op, shapeInfo);
[2853]209      this.shapeInfos.Add(shapeInfo);
210
[2868]211      foreach (IParameter param in op.Parameters)
212        this.AddParameter(op, param);
213
[2942]214      this.operatorGraph.Operators.Add(op);
[2853]215    }
216
[2934]217    internal void RemoveShapeInfo(IOperatorShapeInfo shapeInfo) {
218      IOperator op = this.operatorShapeInfoMapping.GetBySecond(shapeInfo);
[2853]219      this.operatorGraph.Operators.Remove(op);
220    }
221
[2934]222    internal void AddConnection(IOperatorShapeInfo shapeInfoFrom, string connectorName, IOperatorShapeInfo shapeInfoTo) {
223      IOperator opFrom = this.operatorShapeInfoMapping.GetBySecond(shapeInfoFrom);
224      IOperator opTo = this.operatorShapeInfoMapping.GetBySecond(shapeInfoTo);
[2868]225
226      IValueParameter<IOperator> param = (IValueParameter<IOperator>)opFrom.Parameters[connectorName];
227      param.Value = opTo;
228    }
229
[2934]230    internal void ChangeConnection(IOperatorShapeInfo shapeInfoFrom, string connectorName, IOperatorShapeInfo shapeInfoTo) {
231      IOperator opFrom = this.operatorShapeInfoMapping.GetBySecond(shapeInfoFrom);
232      IOperator opTo = this.operatorShapeInfoMapping.GetBySecond(shapeInfoTo);
[2868]233
234      IValueParameter<IOperator> param = (IValueParameter<IOperator>)opFrom.Parameters[connectorName];
235      param.Value = opTo;
236    }
237
[2934]238    internal void RemoveConnection(IOperatorShapeInfo shapeInfoFrom, string connectorName) {
239      IOperator opFrom = this.operatorShapeInfoMapping.GetBySecond(shapeInfoFrom);
[2868]240      IValueParameter<IOperator> param = (IValueParameter<IOperator>)opFrom.Parameters[connectorName];
241      param.Value = null;
242    }
243    #endregion
244
[2853]245    #region operator events
246    private void AddOperator(IOperator op) {
[2934]247      if (!this.operatorShapeInfoMapping.ContainsFirst(op)) {
[2868]248        this.RegisterOperatorEvents(op);
[2934]249        IOperatorShapeInfo shapeInfo = Factory.CreateOperatorShapeInfo(op);
[2868]250        this.operatorParameterCollectionMapping.Add(op, op.Parameters);
[2934]251        this.operatorShapeInfoMapping.Add(op, shapeInfo);
[2942]252        this.shapeInfos.Add(shapeInfo);
[2868]253        foreach (IParameter param in op.Parameters)
254          this.AddParameter(op, param);
255      }
[2853]256    }
257
258    private void RemoveOperator(IOperator op) {
259      this.DeregisterOperatorEvents(op);
[2861]260      foreach (IParameter param in op.Parameters)
261        this.RemoveParameter(op, param);
[2853]262
[2934]263      IOperatorShapeInfo shapeInfo = this.operatorShapeInfoMapping.GetByFirst(op);
[2861]264      this.operatorParameterCollectionMapping.RemoveByFirst(op);
[2934]265      this.operatorShapeInfoMapping.RemoveByFirst(op);
[2853]266      this.shapeInfos.Remove(shapeInfo);
267    }
268
[2935]269    private void OperatorBreakpointChanged(object sender, EventArgs e) {
270      IOperator op = (IOperator)sender;
271      IOperatorShapeInfo operatorShapeInfo = this.operatorShapeInfoMapping.GetByFirst(op);
272      if (op.Breakpoint) {
273        operatorShapeInfo.LineColor = Color.Red;
274        operatorShapeInfo.LineWidth = 2;
275      } else {
276        operatorShapeInfo.LineColor = Color.Black;
277        operatorShapeInfo.LineWidth = 1;
278      }
279    }
280
[3344]281    private void OperatorItemImageChanged(object sender, EventArgs e) {
282      IOperator op = (IOperator)sender;
283      IOperatorShapeInfo operatorShapeInfo = this.operatorShapeInfoMapping.GetByFirst(op);
284      operatorShapeInfo.Icon = new Bitmap(op.ItemImage);
285    }
286
[2896]287    private void OperatorNameChanged(object sender, EventArgs e) {
288      IOperator op = (IOperator)sender;
[2934]289      IOperatorShapeInfo operatorShapeInfo = this.operatorShapeInfoMapping.GetByFirst(op);
290      operatorShapeInfo.Title = op.Name;
[2896]291    }
292
[2853]293    private void Operators_ItemsAdded(object sender, HeuristicLab.Collections.CollectionItemsChangedEventArgs<IOperator> e) {
294      foreach (IOperator op in e.Items)
295        this.AddOperator(op);
296    }
297    private void Operators_ItemsRemoved(object sender, HeuristicLab.Collections.CollectionItemsChangedEventArgs<IOperator> e) {
298      foreach (IOperator op in e.Items)
299        this.RemoveOperator(op);
300    }
301    private void Operators_CollectionReset(object sender, HeuristicLab.Collections.CollectionItemsChangedEventArgs<IOperator> e) {
302      foreach (IOperator op in e.OldItems)
303        this.RemoveOperator(op);
304      foreach (IOperator op in e.Items)
305        this.AddOperator(op);
306    }
307
308    private void RegisterOperatorEvents(IOperator op) {
309      op.Parameters.ItemsAdded += new CollectionItemsChangedEventHandler<IParameter>(Parameters_ItemsAdded);
310      op.Parameters.ItemsRemoved += new CollectionItemsChangedEventHandler<IParameter>(Parameters_ItemsRemoved);
311      op.Parameters.ItemsReplaced += new CollectionItemsChangedEventHandler<IParameter>(Parameters_ItemsReplaced);
312      op.Parameters.CollectionReset += new CollectionItemsChangedEventHandler<IParameter>(Parameters_CollectionReset);
[2896]313      op.NameChanged += new EventHandler(OperatorNameChanged);
[3344]314      op.ItemImageChanged += new EventHandler(OperatorItemImageChanged);
[2935]315      op.BreakpointChanged += new EventHandler(OperatorBreakpointChanged);
[2853]316    }
[2896]317
[2853]318    private void DeregisterOperatorEvents(IOperator op) {
319      op.Parameters.ItemsAdded -= new CollectionItemsChangedEventHandler<IParameter>(Parameters_ItemsAdded);
320      op.Parameters.ItemsRemoved -= new CollectionItemsChangedEventHandler<IParameter>(Parameters_ItemsRemoved);
321      op.Parameters.ItemsReplaced -= new CollectionItemsChangedEventHandler<IParameter>(Parameters_ItemsReplaced);
322      op.Parameters.CollectionReset -= new CollectionItemsChangedEventHandler<IParameter>(Parameters_CollectionReset);
[2896]323      op.NameChanged -= new EventHandler(OperatorNameChanged);
[3344]324      op.ItemImageChanged -= new EventHandler(OperatorItemImageChanged);
[2935]325      op.BreakpointChanged -= new EventHandler(OperatorBreakpointChanged);
[2853]326    }
327    #endregion
328
329    #region parameter events
[2861]330    private void AddParameter(IOperator op, IParameter param) {
[2934]331      this.parameterOperatorMapping.Add(param, op);
[2853]332      IValueParameter<IOperator> opParam = param as IValueParameter<IOperator>;
333      if (opParam != null) {
[2861]334        this.RegisterOperatorParameterEvents(opParam);
[2934]335        IOperatorShapeInfo shapeInfo = this.operatorShapeInfoMapping.GetByFirst(op);
[2861]336        shapeInfo.AddConnector(param.Name);
337
338        if (opParam.Value != null) {
[2934]339          if (!this.operatorShapeInfoMapping.ContainsFirst(opParam.Value))
[2861]340            this.AddOperator(opParam.Value);
[2934]341          this.connections.Add(new KeyValuePair<IOperatorShapeInfo, string>(shapeInfo, param.Name), this.operatorShapeInfoMapping.GetByFirst(opParam.Value));
[2861]342        }
[2934]343      } else
344        this.RegisterParameterEvents(param);
[2853]345    }
[2868]346
[2861]347    private void RemoveParameter(IOperator op, IParameter param) {
[2853]348      IValueParameter<IOperator> opParam = param as IValueParameter<IOperator>;
349      if (opParam != null) {
[2861]350        this.DeregisterOperatorParameterEvents(opParam);
[2934]351        IOperatorShapeInfo shapeInfo = this.operatorShapeInfoMapping.GetByFirst(op);
352        this.connections.Remove(new KeyValuePair<IOperatorShapeInfo, string>(shapeInfo, param.Name));
353        shapeInfo.RemoveConnector(param.Name);
354      } else
355        this.DeregisterParameterEvents(param);
[2868]356
[2934]357      this.parameterOperatorMapping.Remove(param);
[2853]358    }
359
360    private void opParam_ValueChanged(object sender, EventArgs e) {
361      IValueParameter<IOperator> opParam = (IValueParameter<IOperator>)sender;
[2861]362      if (opParam != null) {
363        IOperator op = this.parameterOperatorMapping[opParam];
[2934]364        IOperatorShapeInfo shapeInfo = this.operatorShapeInfoMapping.GetByFirst(op);
365        KeyValuePair<IOperatorShapeInfo, string> connectionFrom = new KeyValuePair<IOperatorShapeInfo, string>(shapeInfo, opParam.Name);
[2861]366
367        if (opParam.Value == null)
[2868]368          this.connections.Remove(connectionFrom);
[2869]369        else {
[2934]370          if (!this.operatorShapeInfoMapping.ContainsFirst(opParam.Value))
[2869]371            this.AddOperator(opParam.Value);
[2934]372          this.connections[connectionFrom] = this.operatorShapeInfoMapping.GetByFirst(opParam.Value);
[2869]373        }
[2861]374      }
[2853]375    }
376
377    private void Parameters_ItemsAdded(object sender, CollectionItemsChangedEventArgs<IParameter> e) {
[2861]378      IObservableKeyedCollection<string, IParameter> parameterCollection = sender as IObservableKeyedCollection<string, IParameter>;
379      IOperator op = this.operatorParameterCollectionMapping.GetBySecond(parameterCollection);
[2853]380      foreach (IParameter param in e.Items)
[2861]381        AddParameter(op, param);
[2934]382      this.UpdateParameterLabels(op);
[2853]383    }
384    private void Parameters_ItemsRemoved(object sender, CollectionItemsChangedEventArgs<IParameter> e) {
[2861]385      IObservableKeyedCollection<string, IParameter> parameterCollection = sender as IObservableKeyedCollection<string, IParameter>;
386      IOperator op = this.operatorParameterCollectionMapping.GetBySecond(parameterCollection);
[2853]387      foreach (IParameter param in e.Items)
[2861]388        RemoveParameter(op, param);
[2934]389      this.UpdateParameterLabels(op);
[2853]390    }
391    private void Parameters_ItemsReplaced(object sender, CollectionItemsChangedEventArgs<IParameter> e) {
[2861]392      IObservableKeyedCollection<string, IParameter> parameterCollection = sender as IObservableKeyedCollection<string, IParameter>;
393      IOperator op = this.operatorParameterCollectionMapping.GetBySecond(parameterCollection);
[2853]394      foreach (IParameter param in e.OldItems)
[2861]395        RemoveParameter(op, param);
[2853]396      foreach (IParameter param in e.Items)
[2861]397        AddParameter(op, param);
[2934]398      this.UpdateParameterLabels(op);
[2853]399    }
400    private void Parameters_CollectionReset(object sender, CollectionItemsChangedEventArgs<IParameter> e) {
[2861]401      IObservableKeyedCollection<string, IParameter> parameterCollection = sender as IObservableKeyedCollection<string, IParameter>;
402      IOperator op = this.operatorParameterCollectionMapping.GetBySecond(parameterCollection);
[2853]403      foreach (IParameter param in e.OldItems)
[2861]404        RemoveParameter(op, param);
[2853]405      foreach (IParameter param in e.Items)
[2861]406        AddParameter(op, param);
[2934]407      this.UpdateParameterLabels(op);
[2853]408    }
409
410    private void RegisterOperatorParameterEvents(IValueParameter<IOperator> opParam) {
411      opParam.ValueChanged += new EventHandler(opParam_ValueChanged);
412    }
413    private void DeregisterOperatorParameterEvents(IValueParameter<IOperator> opParam) {
414      opParam.ValueChanged -= new EventHandler(opParam_ValueChanged);
415    }
[2934]416    private void RegisterParameterEvents(IParameter param) {
417      param.ToStringChanged += new EventHandler(param_ToStringChanged);
418      param.NameChanged += new EventHandler(param_NameChanged);
419    }
420    private void DeregisterParameterEvents(IParameter param) {
421      param.ToStringChanged -= new EventHandler(param_ToStringChanged);
422      param.NameChanged -= new EventHandler(param_NameChanged);
423    }
424
425    private void param_NameChanged(object sender, EventArgs e) {
426      IParameter param = (IParameter)sender;
427      IOperator op = this.parameterOperatorMapping[param];
428      this.UpdateParameterLabels(op);
429    }
430    private void param_ToStringChanged(object sender, EventArgs e) {
431      IParameter param = (IParameter)sender;
432      IOperator op = this.parameterOperatorMapping[param];
433      this.UpdateParameterLabels(op);
434    }
435
436    private void UpdateParameterLabels(IOperator op) {
437      IEnumerable<IParameter> parameters = op.Parameters.Where(p => !(p is IValueParameter<IOperator>));
438      IOperatorShapeInfo operatorShapeInfo = this.operatorShapeInfoMapping.GetByFirst(op);
439      if (parameters.Count() > 0)
440        operatorShapeInfo.UpdateLabels(parameters.Select(p => p.ToString()));
441      else
442        operatorShapeInfo.UpdateLabels(new List<string>());
443    }
[2853]444    #endregion
445  }
[2893]446}
Note: See TracBrowser for help on using the repository browser.