source: sasview/src/sas/qtgui/Plotting/Slicers/BoxSum.py @ 67642f7

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 67642f7 was e20870bc, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Masking dialog for fitting

  • Property mode set to 100644
File size: 26.3 KB
Line 
1"""
2Boxsum Class: determine 2 rectangular area to compute
3the sum of pixel of a Data.
4"""
5import numpy
6from PyQt5 import QtGui
7
8from sas.qtgui.Utilities.GuiUtils import formatNumber, toDouble
9
10from sas.qtgui.Plotting.Slicers.BaseInteractor import BaseInteractor
11from sas.sascalc.dataloader.manipulations import Boxavg
12from sas.sascalc.dataloader.manipulations import Boxsum
13
14from sas.qtgui.Plotting.SlicerModel import SlicerModel
15
16
17class BoxSumCalculator(BaseInteractor):
18    """
19    Boxsum Class: determine 2 rectangular area to compute
20    the sum of pixel of a Data.
21    Uses PointerInteractor , VerticalDoubleLine,HorizontalDoubleLine.
22    @param zorder:  Artists with lower zorder values are drawn first.
23    @param x_min: the minimum value of the x coordinate
24    @param x_max: the maximum value of the x coordinate
25    @param y_min: the minimum value of the y coordinate
26    @param y_max: the maximum value of the y coordinate
27
28    """
29    def __init__(self, base, axes, color='black', zorder=3):
30        BaseInteractor.__init__(self, base, axes, color=color)
31
32        # list of Boxsmun markers
33        self.markers = []
34        self.axes = axes
35        self._model = None
36        self.update_model = False
37
38        # connect the artist for the motion
39        self.connect = self.base.connect
40
41        # when qmax is reached the selected line is reset the its previous value
42        self.qmax = min(self.base.data.xmax, self.base.data.xmin)
43
44        # Define the boxsum limits
45        self.xmin = -1 * 0.5 * min(numpy.fabs(self.base.data.xmax),
46                                   numpy.fabs(self.base.data.xmin))
47        self.ymin = -1 * 0.5 * min(numpy.fabs(self.base.data.xmax),
48                                   numpy.fabs(self.base.data.xmin))
49        self.xmax = 0.5 * min(numpy.fabs(self.base.data.xmax),
50                              numpy.fabs(self.base.data.xmin))
51        self.ymax = 0.5 * min(numpy.fabs(self.base.data.xmax),
52                              numpy.fabs(self.base.data.xmin))
53        # center of the boxSum
54        self.center_x = 0.0002
55        self.center_y = 0.0003
56        # Number of points on the plot
57        self.nbins = 20
58        # Define initial result the summation
59        self.count = 0
60        self.error = 0
61        self.total = 0
62        self.totalerror = 0
63        self.points = 0
64        # Flag to determine if the current figure has moved
65        # set to False == no motion , set to True== motion
66        self.has_move = False
67        # Create Boxsum edges
68        self.horizontal_lines = HorizontalDoubleLine(self,
69                                                     self.axes,
70                                                     color='blue',
71                                                     zorder=zorder,
72                                                     y=self.ymax,
73                                                     x=self.xmax,
74                                                     center_x=self.center_x,
75                                                     center_y=self.center_y)
76        self.horizontal_lines.qmax = self.qmax
77
78        self.vertical_lines = VerticalDoubleLine(self,
79                                                 self.axes,
80                                                 color='black',
81                                                 zorder=zorder,
82                                                 y=self.ymax,
83                                                 x=self.xmax,
84                                                 center_x=self.center_x,
85                                                 center_y=self.center_y)
86        self.vertical_lines.qmax = self.qmax
87
88        self.center = PointInteractor(self,
89                                      self.axes, color='grey',
90                                      zorder=zorder,
91                                      center_x=self.center_x,
92                                      center_y=self.center_y)
93        # Save the name of the slicer panel associate with this slicer
94        self.panel_name = ""
95        # Update and post slicer parameters
96        self.update_model = False
97        self.update()
98        self.postData()
99
100        # set up the model
101        self._model = QtGui.QStandardItemModel(1, 9)
102        self.setModelFromParams()
103        self.update_model = True
104        self._model.itemChanged.connect(self.setParamsFromModel)
105
106    def setModelFromParams(self):
107        """
108        Set up the Qt model for data handling between controls
109        """
110        parameters = self.getParams()
111        # Crete/overwrite model items
112        self._model.setData(self._model.index(0, 0), formatNumber(parameters['Height']))
113        self._model.setData(self._model.index(0, 1), formatNumber(parameters['Width']))
114        self._model.setData(self._model.index(0, 2), formatNumber(parameters['center_x']))
115        self._model.setData(self._model.index(0, 3), formatNumber(parameters['center_y']))
116
117        self.setReadOnlyParametersFromModel()
118
119    def model(self):
120        ''' model accessor '''
121        return self._model
122
123    def setReadOnlyParametersFromModel(self):
124        """
125        Cast model content onto "read-only" subset of parameters
126        """
127        parameters = self.getParams()
128        self._model.setData(self._model.index(0, 4), formatNumber(parameters['avg']))
129        self._model.setData(self._model.index(0, 5), formatNumber(parameters['avg_error']))
130        self._model.setData(self._model.index(0, 6), formatNumber(parameters['sum']))
131        self._model.setData(self._model.index(0, 7), formatNumber(parameters['sum_error']))
132        self._model.setData(self._model.index(0, 8), formatNumber(parameters['num_points']))
133
134    def setParamsFromModel(self):
135        """
136        Cast model content onto params dict
137        """
138        params = {}
139        params["Height"] = toDouble(self.model().item(0, 0).text())
140        params["Width"] = toDouble(self.model().item(0, 1).text())
141        params["center_x"] = toDouble(self.model().item(0, 2).text())
142        params["center_y"] = toDouble(self.model().item(0, 3).text())
143        self.update_model = False
144        self.setParams(params)
145        self.setReadOnlyParametersFromModel()
146        self.update_model = True
147
148    def setPanelName(self, name):
149        """
150        Store the name of the panel associated to this slicer
151        @param name: the name of this panel
152        """
153        self.panel_name = name
154
155    def setLayer(self, n):
156        """
157        Allow adding plot to the same panel
158        :param n: the number of layer
159        """
160        self.layernum = n
161        self.update()
162
163    def clear(self):
164        """
165        Clear the slicer and all connected events related to this slicer
166        """
167        self.clear_markers()
168        self.horizontal_lines.clear()
169        self.vertical_lines.clear()
170        self.center.clear()
171        self.base.connect.clearall()
172
173    def update(self):
174        """
175        Respond to changes in the model by recalculating the profiles and
176        resetting the widgets.
177        """
178        # check if the center point has moved and update the figure accordingly
179        if self.center.has_move:
180            self.center.update()
181            self.horizontal_lines.update(center=self.center)
182            self.vertical_lines.update(center=self.center)
183        # check if the horizontal lines have moved and
184        # update the figure accordingly
185        if self.horizontal_lines.has_move:
186            self.horizontal_lines.update()
187            self.vertical_lines.update(y1=self.horizontal_lines.y1,
188                                       y2=self.horizontal_lines.y2,
189                                       height=self.horizontal_lines.half_height)
190        # check if the vertical lines have moved and
191        # update the figure accordingly
192        if self.vertical_lines.has_move:
193            self.vertical_lines.update()
194            self.horizontal_lines.update(x1=self.vertical_lines.x1,
195                                         x2=self.vertical_lines.x2,
196                                         width=self.vertical_lines.half_width)
197
198    def save(self, ev):
199        """
200        Remember the roughness for this layer and the next so that we
201        can restore on Esc.
202        """
203        self.horizontal_lines.save(ev)
204        self.vertical_lines.save(ev)
205        self.center.save(ev)
206
207    def postData(self):
208        """
209        Get the limits of the boxsum and compute the sum of the pixel
210        contained in that region and the error on that sum
211        """
212        # the region of the summation
213        x_min = self.horizontal_lines.x2
214        x_max = self.horizontal_lines.x1
215        y_min = self.vertical_lines.y2
216        y_max = self.vertical_lines.y1
217        #computation of the sum and its error
218        box = Boxavg(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max)
219        self.count, self.error = box(self.base.data)
220        # Dig out number of points summed, SMK & PDB, 04/03/2013
221        boxtotal = Boxsum(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max)
222        self.total, self.totalerror, self.points = boxtotal(self.base.data)
223        if self.update_model:
224            self.setModelFromParams()
225        self.draw()
226
227    def moveend(self, ev):
228        """
229        After a dragging motion this function is called to compute
230        the error and the sum of pixel of a given data 2D
231        """
232        # compute error an d sum of data's pixel
233        self.postData()
234
235    def restore(self):
236        """
237        Restore the roughness for this layer.
238        """
239        self.horizontal_lines.restore()
240        self.vertical_lines.restore()
241        self.center.restore()
242
243    def getParams(self):
244        """
245        Store a copy of values of parameters of the slicer into a dictionary.
246        :return params: the dictionary created
247        """
248        params = {}
249        params["Width"] = numpy.fabs(self.vertical_lines.half_width) * 2
250        params["Height"] = numpy.fabs(self.horizontal_lines.half_height) * 2
251        params["center_x"] = self.center.x
252        params["center_y"] = self.center.y
253        params["num_points"] = self.points
254        params["avg"] = self.count
255        params["avg_error"] = self.error
256        params["sum"] = self.total
257        params["sum_error"] = self.totalerror
258        return params
259
260    def getResult(self):
261        """
262        Return the result of box summation
263        """
264        result = {}
265        result["num_points"] = self.points
266        result["avg"] = self.count
267        result["avg_error"] = self.error
268        result["sum"] = self.total
269        result["sum_error"] = self.totalerror
270        return result
271
272    def setParams(self, params):
273        """
274        Receive a dictionary and reset the slicer with values contained
275        in the values of the dictionary.
276        :param params: a dictionary containing name of slicer parameters
277        and values the user assigned to the slicer.
278        """
279        x_max = numpy.fabs(params["Width"]) / 2
280        y_max = numpy.fabs(params["Height"]) / 2
281
282        self.center_x = params["center_x"]
283        self.center_y = params["center_y"]
284        # update the slicer given values of params
285        self.center.update(center_x=self.center_x, center_y=self.center_y)
286        self.horizontal_lines.update(center=self.center,
287                                     width=x_max, height=y_max)
288        self.vertical_lines.update(center=self.center,
289                                   width=x_max, height=y_max)
290        # compute the new error and sum given values of params
291        self.postData()
292
293    def draw(self):
294        """ Redraw canvas"""
295        self.base.draw()
296
297
298
299class PointInteractor(BaseInteractor):
300    """
301    Draw a point that can be dragged with the marker.
302    this class controls the motion the center of the BoxSum
303    """
304    def __init__(self, base, axes, color='black', zorder=5, center_x=0.0,
305                 center_y=0.0):
306        BaseInteractor.__init__(self, base, axes, color=color)
307        # Initialization the class
308        self.markers = []
309        self.axes = axes
310        # center coordinates
311        self.x = center_x
312        self.y = center_y
313        # saved value of the center coordinates
314        self.save_x = center_x
315        self.save_y = center_y
316        # Create a marker
317        self.center_marker = self.axes.plot([self.x], [self.y], linestyle='',
318                                            marker='s', markersize=10,
319                                            color=self.color, alpha=0.6,
320                                            pickradius=5, label="pick",
321                                            zorder=zorder,
322                                            visible=True)[0]
323        # Draw a point
324        self.center = self.axes.plot([self.x], [self.y],
325                                     linestyle='-', marker='',
326                                     color=self.color,
327                                     visible=True)[0]
328        # Flag to determine the motion this point
329        self.has_move = False
330        # connecting the marker to allow them to move
331        self.connect_markers([self.center_marker])
332        # Update the figure
333        self.update()
334
335    def setLayer(self, n):
336        """
337        Allow adding plot to the same panel
338        @param n: the number of layer
339        """
340        self.layernum = n
341        self.update()
342
343    def clear(self):
344        """
345        Clear this figure and its markers
346        """
347        self.clear_markers()
348        self.center.remove()
349        self.center_marker.remove()
350
351    def update(self, center_x=None, center_y=None):
352        """
353        Draw the new roughness on the graph.
354        """
355        if center_x is not None:
356            self.x = center_x
357        if center_y is not None:
358            self.y = center_y
359        self.center_marker.set(xdata=[self.x], ydata=[self.y])
360        self.center.set(xdata=[self.x], ydata=[self.y])
361
362    def save(self, ev):
363        """
364        Remember the roughness for this layer and the next so that we
365        can restore on Esc.
366        """
367        self.save_x = self.x
368        self.save_y = self.y
369
370    def moveend(self, ev):
371        """
372        """
373        self.has_move = False
374        self.base.moveend(ev)
375
376    def restore(self):
377        """
378        Restore the roughness for this layer.
379        """
380        self.y = self.save_y
381        self.x = self.save_x
382
383    def move(self, x, y, ev):
384        """
385        Process move to a new position, making sure that the move is allowed.
386        """
387        self.x = x
388        self.y = y
389        self.has_move = True
390        self.base.base.update()
391
392    def setCursor(self, x, y):
393        """
394        """
395        self.move(x, y, None)
396        self.update()
397
398class VerticalDoubleLine(BaseInteractor):
399    """
400    Draw 2 vertical lines moving in opposite direction and centered on
401    a point (PointInteractor)
402    """
403    def __init__(self, base, axes, color='black', zorder=5, x=0.5, y=0.5,
404                 center_x=0.0, center_y=0.0):
405        BaseInteractor.__init__(self, base, axes, color=color)
406        # Initialization the class
407        self.markers = []
408        self.axes = axes
409        # Center coordinates
410        self.center_x = center_x
411        self.center_y = center_y
412        # defined end points vertical lignes and their saved values
413        self.y1 = y + self.center_y
414        self.save_y1 = self.y1
415
416        delta = self.y1 - self.center_y
417        self.y2 = self.center_y - delta
418        self.save_y2 = self.y2
419
420        self.x1 = x + self.center_x
421        self.save_x1 = self.x1
422
423        delta = self.x1 - self.center_x
424        self.x2 = self.center_x - delta
425        self.save_x2 = self.x2
426        # # save the color of the line
427        self.color = color
428        # the height of the rectangle
429        self.half_height = numpy.fabs(y)
430        self.save_half_height = numpy.fabs(y)
431        # the with of the rectangle
432        self.half_width = numpy.fabs(self.x1 - self.x2) / 2
433        self.save_half_width = numpy.fabs(self.x1 - self.x2) / 2
434        # Create marker
435        self.right_marker = self.axes.plot([self.x1], [0], linestyle='',
436                                           marker='s', markersize=10,
437                                           color=self.color, alpha=0.6,
438                                           pickradius=5, label="pick",
439                                           zorder=zorder, visible=True)[0]
440
441        # Define the left and right lines of the rectangle
442        self.right_line = self.axes.plot([self.x1, self.x1], [self.y1, self.y2],
443                                         linestyle='-', marker='',
444                                         color=self.color, visible=True)[0]
445        self.left_line = self.axes.plot([self.x2, self.x2], [self.y1, self.y2],
446                                        linestyle='-', marker='',
447                                        color=self.color, visible=True)[0]
448        # Flag to determine if the lines have moved
449        self.has_move = False
450        # Connection the marker and draw the pictures
451        self.connect_markers([self.right_marker])
452        self.update()
453
454    def setLayer(self, n):
455        """
456        Allow adding plot to the same panel
457        :param n: the number of layer
458        """
459        self.layernum = n
460        self.update()
461
462    def clear(self):
463        """
464        Clear this slicer  and its markers
465        """
466        self.clear_markers()
467        self.right_marker.remove()
468        self.right_line.remove()
469        self.left_line.remove()
470
471    def update(self, x1=None, x2=None, y1=None, y2=None, width=None,
472               height=None, center=None):
473        """
474        Draw the new roughness on the graph.
475        :param x1: new maximum value of x coordinates
476        :param x2: new minimum value of x coordinates
477        :param y1: new maximum value of y coordinates
478        :param y2: new minimum value of y coordinates
479        :param width: is the width of the new rectangle
480        :param height: is the height of the new rectangle
481        :param center: provided x, y  coordinates of the center point
482        """
483        # Save the new height, witdh of the rectangle if given as a param
484        if width is not None:
485            self.half_width = width
486        if height is not None:
487            self.half_height = height
488        # If new  center coordinates are given draw the rectangle
489        # given these value
490        if center is not None:
491            self.center_x = center.x
492            self.center_y = center.y
493            self.x1 = self.half_width + self.center_x
494            self.x2 = -self.half_width + self.center_x
495            self.y1 = self.half_height + self.center_y
496            self.y2 = -self.half_height + self.center_y
497
498            self.right_marker.set(xdata=[self.x1], ydata=[self.center_y])
499            self.right_line.set(xdata=[self.x1, self.x1],
500                                ydata=[self.y1, self.y2])
501            self.left_line.set(xdata=[self.x2, self.x2],
502                               ydata=[self.y1, self.y2])
503            return
504        # if x1, y1, y2, y3 are given draw the rectangle with this value
505        if x1 is not None:
506            self.x1 = x1
507        if x2 is not None:
508            self.x2 = x2
509        if y1 is not None:
510            self.y1 = y1
511        if y2 is not None:
512            self.y2 = y2
513        # Draw 2 vertical lines and a marker
514        self.right_marker.set(xdata=[self.x1], ydata=[self.center_y])
515        self.right_line.set(xdata=[self.x1, self.x1], ydata=[self.y1, self.y2])
516        self.left_line.set(xdata=[self.x2, self.x2], ydata=[self.y1, self.y2])
517
518    def save(self, ev):
519        """
520        Remember the roughness for this layer and the next so that we
521        can restore on Esc.
522        """
523        self.save_x2 = self.x2
524        self.save_y2 = self.y2
525        self.save_x1 = self.x1
526        self.save_y1 = self.y1
527        self.save_half_height = self.half_height
528        self.save_half_width = self.half_width
529
530    def moveend(self, ev):
531        """
532        After a dragging motion reset the flag self.has_move to False
533        """
534        self.has_move = False
535        self.base.moveend(ev)
536
537    def restore(self):
538        """
539        Restore the roughness for this layer.
540        """
541        self.y2 = self.save_y2
542        self.x2 = self.save_x2
543        self.y1 = self.save_y1
544        self.x1 = self.save_x1
545        self.half_height = self.save_half_height
546        self.half_width = self.save_half_width
547
548    def move(self, x, y, ev):
549        """
550        Process move to a new position, making sure that the move is allowed.
551        """
552        self.x1 = x
553        delta = self.x1 - self.center_x
554        self.x2 = self.center_x - delta
555        self.half_width = numpy.fabs(self.x1 - self.x2) / 2
556        self.has_move = True
557        self.base.base.update()
558
559    def setCursor(self, x, y):
560        """
561        Update the figure given x and y
562        """
563        self.move(x, y, None)
564        self.update()
565
566class HorizontalDoubleLine(BaseInteractor):
567    """
568    Select an annulus through a 2D plot
569    """
570    def __init__(self, base, axes, color='black', zorder=5, x=0.5, y=0.5,
571                 center_x=0.0, center_y=0.0):
572
573        BaseInteractor.__init__(self, base, axes, color=color)
574        # Initialization the class
575        self.markers = []
576        self.axes = axes
577        # Center coordinates
578        self.center_x = center_x
579        self.center_y = center_y
580        self.y1 = y + self.center_y
581        self.save_y1 = self.y1
582        delta = self.y1 - self.center_y
583        self.y2 = self.center_y - delta
584        self.save_y2 = self.y2
585        self.x1 = x + self.center_x
586        self.save_x1 = self.x1
587        delta = self.x1 - self.center_x
588        self.x2 = self.center_x - delta
589        self.save_x2 = self.x2
590        self.color = color
591        self.half_height = numpy.fabs(y)
592        self.save_half_height = numpy.fabs(y)
593        self.half_width = numpy.fabs(x)
594        self.save_half_width = numpy.fabs(x)
595        self.top_marker = self.axes.plot([0], [self.y1], linestyle='',
596                                         marker='s', markersize=10,
597                                         color=self.color, alpha=0.6,
598                                         pickradius=5, label="pick",
599                                         zorder=zorder, visible=True)[0]
600
601        # Define 2 horizotnal lines
602        self.top_line = self.axes.plot([self.x1, -self.x1], [self.y1, self.y1],
603                                       linestyle='-', marker='',
604                                       color=self.color, visible=True)[0]
605        self.bottom_line = self.axes.plot([self.x1, -self.x1],
606                                          [self.y2, self.y2],
607                                          linestyle='-', marker='',
608                                          color=self.color, visible=True)[0]
609        # Flag to determine if the lines have moved
610        self.has_move = False
611        # connection the marker and draw the pictures
612        self.connect_markers([self.top_marker])
613        self.update()
614
615    def setLayer(self, n):
616        """
617        Allow adding plot to the same panel
618        @param n: the number of layer
619        """
620        self.layernum = n
621        self.update()
622
623    def clear(self):
624        """
625        Clear this figure and its markers
626        """
627        self.clear_markers()
628        self.top_marker.remove()
629        self.bottom_line.remove()
630        self.top_line.remove()
631
632    def update(self, x1=None, x2=None, y1=None, y2=None,
633               width=None, height=None, center=None):
634        """
635        Draw the new roughness on the graph.
636        :param x1: new maximum value of x coordinates
637        :param x2: new minimum value of x coordinates
638        :param y1: new maximum value of y coordinates
639        :param y2: new minimum value of y coordinates
640        :param width: is the width of the new rectangle
641        :param height: is the height of the new rectangle
642        :param center: provided x, y  coordinates of the center point
643        """
644        # Save the new height, witdh of the rectangle if given as a param
645        if width is not None:
646            self.half_width = width
647        if height is not None:
648            self.half_height = height
649        # If new  center coordinates are given draw the rectangle
650        # given these value
651        if center is not None:
652            self.center_x = center.x
653            self.center_y = center.y
654            self.x1 = self.half_width + self.center_x
655            self.x2 = -self.half_width + self.center_x
656
657            self.y1 = self.half_height + self.center_y
658            self.y2 = -self.half_height + self.center_y
659
660            self.top_marker.set(xdata=[self.center_x], ydata=[self.y1])
661            self.top_line.set(xdata=[self.x1, self.x2],
662                              ydata=[self.y1, self.y1])
663            self.bottom_line.set(xdata=[self.x1, self.x2],
664                                 ydata=[self.y2, self.y2])
665            return
666        # if x1, y1, y2, y3 are given draw the rectangle with this value
667        if x1 is not None:
668            self.x1 = x1
669        if x2 is not None:
670            self.x2 = x2
671        if y1 is not None:
672            self.y1 = y1
673        if y2 is not None:
674            self.y2 = y2
675        # Draw 2 vertical lines and a marker
676        self.top_marker.set(xdata=[self.center_x], ydata=[self.y1])
677        self.top_line.set(xdata=[self.x1, self.x2], ydata=[self.y1, self.y1])
678        self.bottom_line.set(xdata=[self.x1, self.x2], ydata=[self.y2, self.y2])
679
680    def save(self, ev):
681        """
682        Remember the roughness for this layer and the next so that we
683        can restore on Esc.
684        """
685        self.save_x2 = self.x2
686        self.save_y2 = self.y2
687        self.save_x1 = self.x1
688        self.save_y1 = self.y1
689        self.save_half_height = self.half_height
690        self.save_half_width = self.half_width
691
692    def moveend(self, ev):
693        """
694        After a dragging motion reset the flag self.has_move to False
695        """
696        self.has_move = False
697        self.base.moveend(ev)
698
699    def restore(self):
700        """
701        Restore the roughness for this layer.
702        """
703        self.y2 = self.save_y2
704        self.x2 = self.save_x2
705        self.y1 = self.save_y1
706        self.x1 = self.save_x1
707        self.half_height = self.save_half_height
708        self.half_width = self.save_half_width
709
710    def move(self, x, y, ev):
711        """
712        Process move to a new position, making sure that the move is allowed.
713        """
714        self.y1 = y
715        delta = self.y1 - self.center_y
716        self.y2 = self.center_y - delta
717        self.half_height = numpy.fabs(self.y1) - self.center_y
718        self.has_move = True
719        self.base.base.update()
720
721    def setCursor(self, x, y):
722        """
723        Update the figure given x and y
724        """
725        self.move(x, y, None)
726        self.update()
Note: See TracBrowser for help on using the repository browser.