source: sasview/guiframe/local_perspectives/plotting/SectorSlicer.py @ 1b16714f

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 1b16714f was 70cf5d3, checked in by Gervaise Alina <gervyh@…>, 16 years ago

allow selecting for fit for average data1D

  • Property mode set to 100644
File size: 22.1 KB
RevLine 
[ef0c170]1#TODO: the line slicer should listen to all 2DREFRESH events, get the data and slice it
2#      before pushing a new 1D data update.
3
4#
5#TODO: NEED MAJOR REFACTOR
6#
7
8
9# Debug printout
10import math
[0d9dae8]11import wx
12from copy import deepcopy
13
14from BaseInteractor import _BaseInteractor
[6c0568b]15from sans.guicomm.events import NewPlotEvent, StatusEvent
16from sans.guicomm.events import SlicerParameterEvent,EVT_SLICER_PARS
[ef0c170]17
18
19class SectorInteractor(_BaseInteractor):
20    """
[6c0568b]21         Draw a sector slicer.Allow to performQ averaging on data 2D
[ef0c170]22    """
23    def __init__(self,base,axes,color='black', zorder=3):
24       
25        _BaseInteractor.__init__(self, base, axes, color=color)
[6c0568b]26        ## Class initialization
[ef0c170]27        self.markers = []
[6c0568b]28        self.axes = axes   
29        ## connect the plot to event
[ef0c170]30        self.connect = self.base.connect
31       
[6c0568b]32        ## compute qmax limit to reset the graph     
33        x = math.pow(max(self.base.data2D.xmax,math.fabs(self.base.data2D.xmin)),2)
34        y = math.pow(max(self.base.data2D.ymax,math.fabs(self.base.data2D.ymin)),2)
35        self.qmax= math.sqrt(x + y)
[ef0c170]36        ## Number of points on the plot
[b783024]37        self.nbins = 20
[6c0568b]38        ## Angle of the middle line
[ef0c170]39        self.theta2= math.pi/3
[6c0568b]40        ## Absolute value of the Angle between the middle line and any side line
[ef0c170]41        self.phi=math.pi/12
[6c0568b]42       
43        ## Middle line
44        self.main_line = LineInteractor(self, self.base.subplot,color='blue', zorder=zorder, r=self.qmax,
[ef0c170]45                                           theta= self.theta2)
[2dda74ac]46        self.main_line.qmax = self.qmax
[6c0568b]47        ## Right Side line
[ef0c170]48        self.right_line= SideInteractor(self, self.base.subplot,color='black', zorder=zorder,
49                                     r=self.qmax,
50                                           phi= -1*self.phi,
51                                           theta2=self.theta2)
[2dda74ac]52        self.right_line.qmax = self.qmax
[6c0568b]53        ## Left Side line
54        self.left_line= SideInteractor(self, self.base.subplot,color='black', zorder=zorder,
[ef0c170]55                                     r=self.qmax,
56                                           phi= self.phi,
57                                           theta2=self.theta2)
[2dda74ac]58        self.left_line.qmax = self.qmax
[6c0568b]59        ## draw the sector               
[ef0c170]60        self.update()
61        self._post_data()
[6c0568b]62
63        ## Bind to slice parameter events
[1ce365f8]64        self.base.Bind(EVT_SLICER_PARS, self._onEVT_SLICER_PARS)
[ef0c170]65
[6c0568b]66
[ef0c170]67    def _onEVT_SLICER_PARS(self, event):
[6c0568b]68        """
69            receive an event containing parameters values to reset the slicer
70            @param event: event of type SlicerParameterEvent with params as
71            attribute
72        """
[18eba35]73        wx.PostEvent(self.base.parent, StatusEvent(status="SectorSlicer._onEVT_SLICER_PARS"))
[ef0c170]74        event.Skip()
75        if event.type == self.__class__.__name__:
76            self.set_params(event.params)
77            self.base.update()
78
79
80    def set_layer(self, n):
[6c0568b]81        """
82             Allow adding plot to the same panel
83             @param n: the number of layer
84        """
[ef0c170]85        self.layernum = n
86        self.update()
87       
[6c0568b]88       
[ef0c170]89    def clear(self):
[6c0568b]90        """
91            Clear the slicer and all connected events related to this slicer
92        """
[ef0c170]93        self.clear_markers()
94        self.main_line.clear()
95        self.left_line.clear()
96        self.right_line.clear()
[18eba35]97        self.base.connect.clearall()
[d468daa]98        self.base.Unbind(EVT_SLICER_PARS)
[ef0c170]99       
[6c0568b]100       
[ef0c170]101    def update(self):
102        """
[6c0568b]103            Respond to changes in the model by recalculating the profiles and
104            resetting the widgets.
[ef0c170]105        """
[6c0568b]106        # Update locations 
107        ## Check if the middle line was dragged and update the picture accordingly     
[ef0c170]108        if self.main_line.has_move:
109            self.main_line.update()
[7b73518]110            self.right_line.update( delta= -self.left_line.phi/2,
[6c0568b]111                                    mline= self.main_line.theta )
[7b73518]112            self.left_line.update( delta = self.left_line.phi/2,
[6c0568b]113                                   mline= self.main_line.theta )
114        ## Check if the left side has moved and update the slicer accordingly 
[ef0c170]115        if self.left_line.has_move:
116            self.main_line.update()
[6c0568b]117            self.left_line.update( phi=None, delta=None, mline=self.main_line ,
118                                  side=True, left=True )
119            self.right_line.update( phi= self.left_line.phi, delta= None,
120                                     mline= self.main_line, side= True,
121                                     left=False, right= True )
122        ## Check if the right side line has moved and update the slicer accordingly
[ef0c170]123        if self.right_line.has_move:
124            self.main_line.update()
[6c0568b]125            self.right_line.update( phi=None, delta=None, mline=self.main_line,
126                                   side=True, left=False, right=True )
127            self.left_line.update( phi=self.right_line.phi, delta=None,
128                                    mline=self.main_line, side=True, left=False )
129
[ef0c170]130
131    def save(self, ev):
132        """
133        Remember the roughness for this layer and the next so that we
134        can restore on Esc.
135        """
136        self.base.freeze_axes()
[6c0568b]137        self.main_line.save(ev)
138        self.right_line.save(ev)
139        self.left_line.save(ev)
[ef0c170]140
141    def _post_data(self, nbins=None):
[6c0568b]142        """
143            compute sector averaging of data2D into data1D
144            @param nbins: the number of point to plot for the average 1D data
145        """
146        ## get the data2D to average
[ef0c170]147        data = self.base.data2D
148        # If we have no data, just return
149        if data == None:
150            return
[6c0568b]151        ## Averaging
[ef0c170]152        from DataLoader.manipulations import SectorQ
[6c0568b]153        radius = self.qmax
154        phimin =  -self.left_line.phi + self.main_line.theta
155        phimax = self.left_line.phi + self.main_line.theta
156       
[1ce365f8]157        if nbins==None:
158            nbins = 20
[6c0568b]159        sect = SectorQ(r_min= 0.0, r_max= radius ,
160                        phi_min= phimin + math.pi,
161                        phi_max= phimax + math.pi, nbins=nbins)
162     
[ef0c170]163        sector = sect(self.base.data2D)
[6c0568b]164        ##Create 1D data resulting from average
[ef0c170]165        from sans.guiframe.dataFitting import Data1D
166        if hasattr(sector,"dxl"):
167            dxl= sector.dxl
168        else:
169            dxl= None
170        if hasattr(sector,"dxw"):
171            dxw= sector.dxw
172        else:
173            dxw= None
174       
175        new_plot = Data1D(x=sector.x,y=sector.y,dy=sector.dy,dxl=dxl,dxw=dxw)
176        new_plot.name = "SectorQ" +"("+ self.base.data2D.name+")"
177       
178        new_plot.source=self.base.data2D.source
[8b30e62]179        #new_plot.info=self.base.data2D.info
[ef0c170]180        new_plot.interactive = True
181        new_plot.detector =self.base.data2D.detector
182        # If the data file does not tell us what the axes are, just assume...
[e23a20c]183        new_plot.xaxis("\\rm{Q}", 'A^{-1}')
[ef0c170]184        new_plot.yaxis("\\rm{Intensity} ","cm^{-1}")
185        new_plot.group_id = "SectorQ"+self.base.data2D.name
[8b30e62]186        new_plot.id = "SectorQ"+self.base.data2D.name
[70cf5d3]187        new_plot.is_data= True
[ef0c170]188        wx.PostEvent(self.base.parent, NewPlotEvent(plot=new_plot,
[fbd0bece]189                                                 title="SectorQ"+self.base.data2D.name ))
[ef0c170]190       
191         
192       
193    def moveend(self, ev):
[6c0568b]194        """
195            Called a dragging motion ends.Get slicer event
196        """
[ef0c170]197        self.base.thaw_axes()
[6c0568b]198        ## Post parameters
[0d9dae8]199        event = SlicerParameterEvent()
[ef0c170]200        event.type = self.__class__.__name__
201        event.params = self.get_params()
[6c0568b]202        ## Send slicer paramers to plotter2D
[d468daa]203        wx.PostEvent(self.base, event)
[ef0c170]204        self._post_data()
205           
[6c0568b]206           
[ef0c170]207    def restore(self):
208        """
209        Restore the roughness for this layer.
210        """
211        self.main_line.restore()
212        self.left_line.restore()
213        self.right_line.restore()
214
215    def move(self, x, y, ev):
216        """
217        Process move to a new position, making sure that the move is allowed.
218        """
219        pass
220       
[6c0568b]221       
[ef0c170]222    def set_cursor(self, x, y):
223        pass
224       
[6c0568b]225       
[ef0c170]226    def get_params(self):
[6c0568b]227        """
228            Store a copy of values of parameters of the slicer into a dictionary.
229            @return params: the dictionary created
230        """
[ef0c170]231        params = {}
[6c0568b]232        ## Always make sure that the left and the right line are at phi
233        ## angle of the middle line
[ef0c170]234        if math.fabs(self.left_line.phi) != math.fabs(self.right_line.phi):
235            raise ValueError,"Phi left and phi right are different %f, %f"%(self.left_line.phi, self.right_line.phi)
[6c0568b]236       
237        params["Phi"] = self.main_line.theta
[1a6ec25]238        params["Delta_Phi"] = math.fabs(self.left_line.phi)
[ef0c170]239        params["nbins"] = self.nbins
240        return params
241   
[6c0568b]242   
[ef0c170]243    def set_params(self, params):
[6c0568b]244        """
245            Receive a dictionary and reset the slicer with values contained
246            in the values of the dictionary.
247            @param params: a dictionary containing name of slicer parameters and
248            values the user assigned to the slicer.
249        """
[1a6ec25]250        main = params["Phi"] 
251        phi = math.fabs(params["Delta_Phi"] )
[ef0c170]252        self.nbins = int(params["nbins"])
253        self.main_line.theta= main
[6c0568b]254        ## Reset the slicer parameters
[ef0c170]255        self.main_line.update()
[6c0568b]256        self.right_line.update( phi=phi,delta=None, mline=self.main_line,
257                               side=True, right=True )
258        self.left_line.update( phi=phi, delta=None, mline=self.main_line, side=True )
259        ## post the new corresponding data
[ef0c170]260        self._post_data(nbins=self.nbins)
261       
[6c0568b]262       
[ef0c170]263    def freeze_axes(self):
264        self.base.freeze_axes()
265       
[6c0568b]266       
[ef0c170]267    def thaw_axes(self):
268        self.base.thaw_axes()
269
[6c0568b]270
[ef0c170]271    def draw(self):
272        self.base.draw()
273
274       
275class SideInteractor(_BaseInteractor):
276    """
[6c0568b]277        Draw an oblique line
278        @param phi: the phase between the middle line and one side line
279        @param theta2: the angle between the middle line and x- axis
[ef0c170]280    """
281    def __init__(self,base,axes,color='black', zorder=5, r=1.0,phi=math.pi/4, theta2= math.pi/3):
282       
283        _BaseInteractor.__init__(self, base, axes, color=color)
[6c0568b]284        ## Initialize the class
[ef0c170]285        self.markers = []
286        self.axes = axes
[6c0568b]287        ## compute the value of the angle between the current line and
288        ## the x-axis 
[ef0c170]289        self.save_theta = theta2 + phi
[31e3298]290        self.theta= theta2 + phi
[6c0568b]291        ## the value of the middle line angle with respect to the x-axis
[ef0c170]292        self.theta2 = theta2
[6c0568b]293        ## Radius to find polar coordinates this line's endpoints
[ef0c170]294        self.radius = r
[6c0568b]295        ## phi is the phase between the current line and the middle line
[ef0c170]296        self.phi = phi
[6c0568b]297       
298        ## End points polar coordinates
[ef0c170]299        x1= self.radius*math.cos(self.theta)
300        y1= self.radius*math.sin(self.theta)
301        x2= -1*self.radius*math.cos(self.theta)
302        y2= -1*self.radius*math.sin(self.theta)
[6c0568b]303        ## defining a new marker
[7a28ba7]304        try:
[0c218d9]305            self.inner_marker = self.axes.plot([x1/2.5],[y1/2.5], linestyle='',
[7a28ba7]306                                          marker='s', markersize=10,
307                                          color=self.color, alpha=0.6,
308                                          pickradius=5, label="pick", 
309                                          zorder=zorder, # Prefer this to other lines
310                                          visible=True)[0]
311        except:
[0c218d9]312            self.inner_marker = self.axes.plot([x1/2.5],[y1/2.5], linestyle='',
[7a28ba7]313                                          marker='s', markersize=10,
314                                          color=self.color, alpha=0.6,
315                                          label="pick", 
316                                          visible=True)[0]
317            message  = "\nTHIS PROTOTYPE NEEDS THE LATEST VERSION OF MATPLOTLIB\n"
318            message += "Get the SVN version that is at least as recent as June 1, 2007"
[6c0568b]319            owner=self.base.base.parent
320            wx.PostEvent(owner, StatusEvent(status="sectorSlicer: %s"%message))
321       
322        ## Defining the current line
[ef0c170]323        self.line = self.axes.plot([x1,x2],[y1,y2],
324                                      linestyle='-', marker='',
325                                      color=self.color,
326                                      visible=True)[0]
[6c0568b]327        ## Flag to differentiate the left line from the right line motion
[7a28ba7]328        self.left_moving=False
[6c0568b]329        ## Flag to define a motion
[ef0c170]330        self.has_move=False
[6c0568b]331        ## connecting markers and draw the picture
[7a28ba7]332        self.connect_markers([self.inner_marker, self.line])
[6c0568b]333       
[ef0c170]334
335    def set_layer(self, n):
[6c0568b]336        """
337             Allow adding plot to the same panel
338             @param n: the number of layer
339        """
[ef0c170]340        self.layernum = n
341        self.update()
342       
343    def clear(self):
[6c0568b]344        """
345            Clear the slicer and all connected events related to this slicer
346        """
[ef0c170]347        self.clear_markers()
348        try:
349            self.line.remove()
[7a28ba7]350            self.inner_marker.remove()
[ef0c170]351        except:
352            # Old version of matplotlib
353            for item in range(len(self.axes.lines)):
354                del self.axes.lines[0]
355       
356       
[6c0568b]357    def update(self, phi=None, delta=None, mline=None,
358               side=False, left= False, right=False):
[ef0c170]359        """
[6c0568b]360            Draw oblique line
361            @param phi: the phase between the middle line and the current line
[7b73518]362            @param delta: phi/2 applied only when the mline was moved
[ef0c170]363        """
364        #print "update left or right ", self.has_move
[7a28ba7]365        self.left_moving=left
[31e3298]366        theta3=0
[ef0c170]367        if phi !=None:
[7a28ba7]368            self.phi= phi
[31e3298]369        if delta==None:
370            delta = 0
371
[7a28ba7]372        if  right:
373            self.phi = -1*math.fabs(self.phi)
[31e3298]374            #delta=-delta
[7a28ba7]375        else:
376            self.phi =math.fabs(self.phi)
[e23a20c]377        if side:
[ef0c170]378            self.theta=  mline.theta + self.phi
[31e3298]379                   
380        if mline!=None :
381            if delta!=0:
382                self.theta2 = mline+delta
383            else:
384                self.theta2 = mline.theta
385        if delta==0:
386            theta3=self.theta+delta
387        else:
388            theta3=self.theta2+delta
[7a28ba7]389       
[6c0568b]390     
[31e3298]391        x1= self.radius*math.cos(theta3)
392        y1= self.radius*math.sin(theta3)
393        x2= -1*self.radius*math.cos(theta3)
394        y2= -1*self.radius*math.sin(theta3)
[ef0c170]395       
[0c218d9]396        self.inner_marker.set(xdata=[x1/2.5],ydata=[y1/2.5])
[ef0c170]397        self.line.set(xdata=[x1,x2], ydata=[y1,y2]) 
398       
399       
400       
401       
402    def save(self, ev):
403        """
404        Remember the roughness for this layer and the next so that we
405        can restore on Esc.
406        """
407        self.save_theta= self.theta
408        self.base.freeze_axes()
409
410    def moveend(self, ev):
411       
412        self.has_move=False
413        self.base.moveend(ev)
414           
415    def restore(self):
416        """
417        Restore the roughness for this layer.
418        """
419        self.theta = self.save_theta
420
421    def move(self, x, y, ev):
422        """
423        Process move to a new position, making sure that the move is allowed.
424        """
425       
426        self.theta= math.atan2(y,x)
[7a28ba7]427        self.has_move=True
[0c218d9]428        #ToDo: Simplify below
[7a28ba7]429        if not self.left_moving:
[0c218d9]430            if  self.theta2-self.theta <= 0 and self.theta2>0:#>= self.theta2:
431                #print "my theta", self.theta
432                self.restore()
433                return 
434            elif self.theta2 < 0 and self.theta < 0 and self.theta-self.theta2 >= 0:
435                self.restore()
436                return                             
437            elif  self.theta2 < 0 and self.theta > 0 and self.theta2+2*math.pi-self.theta >=math.pi/2:
438                #print "my theta", self.theta
439                self.restore()
440                return 
441            elif  self.theta2 < 0 and self.theta < 0 and self.theta2-self.theta >=math.pi/2:
442                #print "my theta", self.theta
[7a28ba7]443                self.restore()
444                return 
[0c218d9]445            elif self.theta2>0 and (self.theta2-self.theta >= math.pi/2 or (self.theta2-self.theta >= math.pi/2)):#<= self.theta2 -math.pi/2:
446                #print "self theta encore"
[7a28ba7]447                self.restore()
448                return 
449        else:
[0c218d9]450            #print "left move"
451            if  self.theta < 0 and self.theta+math.pi*2-self.theta2 <= 0:
[7a28ba7]452                self.restore()
453                return 
[0c218d9]454            elif self.theta2 < 0 and self.theta-self.theta2 <= 0:
455                self.restore()
456                return                             
457            elif  self.theta > 0 and self.theta-self.theta2 <=0:
458                #print "my theta", self.theta
459                self.restore()
460                return 
461            elif self.theta-self.theta2 >= math.pi/2 or  (self.theta+math.pi*2-self.theta2 >= math.pi/2 and self.theta<0 and self.theta2>0):
462                #print "self theta encore"
[7a28ba7]463                self.restore()
464                return 
[0c218d9]465           
[e23a20c]466        self.phi= math.fabs(self.theta2 - self.theta)
[0c218d9]467        if self.phi>math.pi:
468            self.phi= 2*math.pi-math.fabs(self.theta2 - self.theta)
[ef0c170]469       
[0c218d9]470        #print "move , self.phi, self.theta,", self.theta*180/math.pi,self.theta2*180/math.pi,self.phi*180/math.pi
[7a28ba7]471       
472           
473       
[ef0c170]474        self.base.base.update()
475       
476    def set_cursor(self, x, y):
477        self.move(x, y, None)
478        self.update()
479       
480       
481    def get_params(self):
482        params = {}
483        params["radius"] = self.radius
484        params["theta"] = self.theta
485        return params
486   
487    def set_params(self, params):
488
489        x = params["radius"] 
490        self.set_cursor(x, self._inner_mouse_y)
491       
[7a28ba7]492 
[ef0c170]493       
494class LineInteractor(_BaseInteractor):
495    """
496         Select an annulus through a 2D plot
497    """
498    def __init__(self,base,axes,color='black', zorder=5, r=1.0,theta=math.pi/4):
499       
500        _BaseInteractor.__init__(self, base, axes, color=color)
501        self.markers = []
502        self.axes = axes
503       
504        self.save_theta = theta
505        self.theta= theta
506       
507        self.radius = r
508     
509        self.scale = 10.0
510     
511        #raise "Version error", message
512           
513        # Inner circle
514        x1= self.radius*math.cos(self.theta)
515        y1= self.radius*math.sin(self.theta)
516        x2= -1*self.radius*math.cos(self.theta)
517        y2= -1*self.radius*math.sin(self.theta)
[7a28ba7]518        try:
519            # Inner circle marker
[0c218d9]520            self.inner_marker = self.axes.plot([x1/2.5],[y1/2.5], linestyle='',
[7a28ba7]521                                          marker='s', markersize=10,
522                                          color=self.color, alpha=0.6,
523                                          pickradius=5, label="pick", 
524                                          zorder=zorder, # Prefer this to other lines
525                                          visible=True)[0]
526        except:
[0c218d9]527            self.inner_marker = self.axes.plot([x1/2.5],[y1/2.5], linestyle='',
[7a28ba7]528                                          marker='s', markersize=10,
529                                          color=self.color, alpha=0.6,
530                                          label="pick", 
531                                          visible=True)[0]
532            message  = "\nTHIS PROTOTYPE NEEDS THE LATEST VERSION OF MATPLOTLIB\n"
533            message += "Get the SVN version that is at least as recent as June 1, 2007"
534           
[ef0c170]535       
536        self.line = self.axes.plot([x1,x2],[y1,y2],
537                                      linestyle='-', marker='',
538                                      color=self.color,
539                                      visible=True)[0]
540     
541        self.npts = 20
542        self.has_move=False
[7a28ba7]543        self.connect_markers([self.inner_marker, self.line])
[ef0c170]544        self.update()
545
546    def set_layer(self, n):
547        self.layernum = n
548        self.update()
549       
550    def clear(self):
551        self.clear_markers()
552        try:
[7a28ba7]553            self.inner_marker.remove()
[ef0c170]554            self.line.remove()
555        except:
556            # Old version of matplotlib
557            for item in range(len(self.axes.lines)):
558                del self.axes.lines[0]
[6c0568b]559 
560 
[ef0c170]561    def update(self, theta=None):
562        """
563        Draw the new roughness on the graph.
564        """
[6c0568b]565       
[ef0c170]566        if theta !=None:
567            self.theta= theta
568        x1= self.radius*math.cos(self.theta)
569        y1= self.radius*math.sin(self.theta)
570        x2= -1*self.radius*math.cos(self.theta)
571        y2= -1*self.radius*math.sin(self.theta)
[7a28ba7]572       
[0c218d9]573        self.inner_marker.set(xdata=[x1/2.5],ydata=[y1/2.5])
[ef0c170]574        self.line.set(xdata=[x1,x2], ydata=[y1,y2]) 
575     
576       
577       
578    def save(self, ev):
579        """
580        Remember the roughness for this layer and the next so that we
581        can restore on Esc.
582        """
583        self.save_theta= self.theta
584        self.base.freeze_axes()
585
586    def moveend(self, ev):
587       
588        self.has_move=False
589        self.base.moveend(ev)
590           
591    def restore(self):
592        """
593        Restore the roughness for this layer.
594        """
595        self.theta = self.save_theta
596
597    def move(self, x, y, ev):
598        """
599        Process move to a new position, making sure that the move is allowed.
600        """
601       
602        self.theta= math.atan2(y,x)
[0c218d9]603        #print "main_line previous theta --- next theta ",math.degrees(self.save_theta),math.degrees(self.theta)
[ef0c170]604       
605        self.has_move=True
[0c218d9]606       
[ef0c170]607        self.base.base.update()
608       
609    def set_cursor(self, x, y):
610        self.move(x, y, None)
611        self.update()
612       
613       
614    def get_params(self):
615        params = {}
616        params["radius"] = self.radius
617        params["theta"] = self.theta
618        return params
619   
620    def set_params(self, params):
621
622        x = params["radius"] 
623        self.set_cursor(x, self._inner_mouse_y)
624
625
[356aea78]626       
Note: See TracBrowser for help on using the repository browser.