source: sasview/src/sas/sasgui/perspectives/simulation/ShapeParameters.py @ afb311e

Last change on this file since afb311e was a1b8fee, checked in by andyfaff, 8 years ago

MAINT: from future import print_function

  • Property mode set to 100644
File size: 20.9 KB
Line 
1"""
2This software was developed by the University of Tennessee as part of the
3Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
4project funded by the US National Science Foundation.
5
6See the license text in license.txt
7
8copyright 2009, University of Tennessee
9"""
10from __future__ import print_function
11
12import wx
13import sys
14import wx.lib.newevent
15from copy import deepcopy
16import SimCanvas
17
18(CreateShapeEvent, EVT_ADD_SHAPE) = wx.lib.newevent.NewEvent()
19(EditShapeEvent, EVT_EDIT_SHAPE)  = wx.lib.newevent.NewEvent()
20(DelShapeEvent, EVT_DEL_SHAPE)    = wx.lib.newevent.NewEvent()
21(QRangeEvent, EVT_Q_RANGE)        = wx.lib.newevent.NewEvent() 
22(PtDensityEvent, EVT_PT_DENSITY)  = wx.lib.newevent.NewEvent() 
23
24class ShapeParameterPanel(wx.Panel):
25    #TODO: show units
26    #TODO: order parameters properly
27    CENTER_PANE = True
28   
29    def __init__(self, parent, q_min=0.001, q_max=0.5, q_npts=10, pt_density=0.1, *args, **kwargs):
30        wx.Panel.__init__(self, parent, *args, **kwargs)
31       
32        self.window_name = "ShapeParams"
33        self.window_caption = "Shape parameter"
34       
35     
36        self.params = {}
37        self.parent = parent
38        self.type = None
39        self.listeners = []
40        self.parameters = []
41        self.bck = wx.GridBagSizer(5,5)
42        self.SetSizer(self.bck)
43       
44        # position and orientation ctrl
45        self.xctrl = None
46        self.yctrl = None
47        self.zctrl = None
48        self.actrl = None
49        self.bctrl = None
50        self.cctrl = None
51               
52        # Sizer to hold the shape parameters
53        self.shape_sizer = wx.GridBagSizer(5,5)
54       
55        ny = 0       
56        title = wx.StaticText(self, -1, "[Temporary form]", style=wx.ALIGN_LEFT)
57        self.bck.Add(title, (ny,0), (1,2), flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=15)
58       
59        # Shape list
60        shape_text = wx.StaticText(self, -1, "Geometric shape", style=wx.ALIGN_LEFT)
61        self.shape_list = SimCanvas.getShapes()
62        value_list = []
63        for item in self.shape_list:
64            value_list.append(item['name'])
65           
66        self.model_combo = wx.ComboBox(self, -1, value=value_list[0], choices=value_list, style=wx.CB_READONLY)
67        self.model_combo.SetToolTip(wx.ToolTip("Select a geometric shape from the drop-down list"))
68       
69        ny+=1
70        self.bck.Add(shape_text, (ny,0), flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=15)
71        self.bck.Add(self.model_combo, (ny,1), flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=15)
72        wx.EVT_COMBOBOX(self.model_combo,-1, self._on_select_shape)
73   
74        # Placeholder for parameter form
75        ny+=1
76        self.bck.Add(self.shape_sizer, (ny,0), (1,2), flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=0)   
77       
78        # Point density control
79        point_density_text = wx.StaticText(self, -1, "Point density", style=wx.ALIGN_LEFT)
80        ny+=1
81        self.bck.Add(point_density_text, (ny,0), flag = wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
82        self.point_density_ctrl = wx.TextCtrl(self, -1, size=(40,20), style=wx.TE_PROCESS_ENTER)
83        self.point_density_ctrl.SetValue(str(pt_density))
84        self.point_density_ctrl.SetToolTip(wx.ToolTip("Enter the number of real-space points per Angstrom cube"))
85        self.point_density_ctrl.Bind(wx.EVT_TEXT_ENTER, self._on_density_changed)
86        self.point_density_ctrl.Bind(wx.EVT_KILL_FOCUS, self._on_density_changed)
87        self.bck.Add(self.point_density_ctrl, (ny,1), flag = wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
88       
89        # Q range
90        q_min_text = wx.StaticText(self, -1, "Q min", style=wx.ALIGN_LEFT)
91        ny+=1
92        self.bck.Add(q_min_text, (ny,0), flag = wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
93        self.q_min_ctrl = wx.TextCtrl(self, -1, size=(40,20), style=wx.TE_PROCESS_ENTER)
94        self.q_min_ctrl.SetValue(str(q_min))
95        self.q_min_ctrl.SetToolTip(wx.ToolTip("Enter the minimum Q value to be simulated"))
96        self.q_min_ctrl.Bind(wx.EVT_TEXT_ENTER, self._on_q_range_changed)
97        self.q_min_ctrl.Bind(wx.EVT_KILL_FOCUS, self._on_q_range_changed)
98        self.bck.Add(self.q_min_ctrl, (ny,1), flag = wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
99       
100        q_max_text = wx.StaticText(self, -1, "Q max", style=wx.ALIGN_LEFT)
101        self.bck.Add(q_max_text, (ny,2), flag = wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
102        self.q_max_ctrl = wx.TextCtrl(self, -1, size=(40,20), style=wx.TE_PROCESS_ENTER)
103        self.q_max_ctrl.SetValue(str(q_max))
104        self.q_min_ctrl.SetToolTip(wx.ToolTip("Enter the maximum Q value to be simulated"))
105        self.q_max_ctrl.Bind(wx.EVT_TEXT_ENTER, self._on_q_range_changed)
106        self.q_max_ctrl.Bind(wx.EVT_KILL_FOCUS, self._on_q_range_changed)
107        self.bck.Add(self.q_max_ctrl, (ny,3), flag = wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
108       
109        q_npts_text = wx.StaticText(self, -1, "No. of Q pts", style=wx.ALIGN_LEFT)
110        self.bck.Add(q_npts_text, (ny,4), flag = wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
111        self.q_npts_ctrl = wx.TextCtrl(self, -1, size=(40,20), style=wx.TE_PROCESS_ENTER)
112        self.q_npts_ctrl.SetValue(str(q_npts))
113        self.q_min_ctrl.SetToolTip(wx.ToolTip("Enter the number of Q points to be simulated"))
114        self.q_npts_ctrl.Bind(wx.EVT_TEXT_ENTER, self._on_q_range_changed)
115        self.q_npts_ctrl.Bind(wx.EVT_KILL_FOCUS, self._on_q_range_changed)
116        self.bck.Add(self.q_npts_ctrl, (ny,5), flag = wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
117       
118        # Shape List
119        # Internal counter of shapes in the listbox
120        self.counter = 0
121        # Buffer filled flag
122        self.buffer_filled = False
123        # Save current flag
124        self.current_saved = False       
125         
126        id = wx.NewId()
127        shape_listbox_text = wx.StaticText(self, -1, "List of shapes on 3D canvas", style=wx.ALIGN_LEFT)
128        self.shape_listbox = wx.ListBox(self, id, wx.DefaultPosition, (295, 200), 
129                                   [], wx.LB_SINGLE | wx.LB_HSCROLL)
130       
131        # Listen to history events
132        self.parent.Bind(EVT_ADD_SHAPE, self._on_add_shape_to_listbox)
133        self.shape_listbox.Bind(wx.EVT_LISTBOX, self._on_select_from_listbox, id=id)
134        self.shape_listbox.Bind(wx.EVT_CONTEXT_MENU, self._on_listbox_context_menu, id=id)
135       
136        ny+=1
137        self.bck.Add(shape_listbox_text, (ny,0), (1,2), flag = wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
138        ny+=1
139        self.bck.Add(self.shape_listbox, (ny,0), (1,5), flag = wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
140       
141        self.current_shape = None
142       
143        # Default shape
144        if type(self.shape_list)==list and len(self.shape_list)>0:
145            shape = SimCanvas.getShapeClassByName(self.shape_list[0]['name'])()
146            self.editShape(shape)                       
147
148    def _on_select_shape(self, event):
149        shape = SimCanvas.getShapeClassByName(self.shape_list[event.GetEventObject().GetSelection()]['name'])()
150        self.editShape(shape)   
151
152    def _on_density_changed(self, event):
153        """
154            Process event that might mean a change of the simulation point density
155        """
156        npts  = self._readCtrlFloat(self.point_density_ctrl)
157        if npts is not None:
158            event = PtDensityEvent(npts=npts)
159            wx.PostEvent(self.parent, event)
160           
161    def _on_q_range_changed(self, event):
162        """
163            Process event that might mean a change in Q range
164            @param event: EVT_Q_RANGE event
165        """
166        q_min = self._readCtrlFloat(self.q_min_ctrl)
167        q_max = self._readCtrlFloat(self.q_max_ctrl)
168        npts  = self._readCtrlInt(self.q_npts_ctrl)
169        if q_min is not None or q_max is not None or npts is not None:
170            event = QRangeEvent(q_min=q_min, q_max=q_max, npts=npts)
171            wx.PostEvent(self.parent, event)
172           
173    def _onEditShape(self, evt):
174        """
175            Process an EVT_EDIT_SHAPE event
176            @param evt: EVT_EDIT_SHAPE object
177        """
178        evt.Skip()
179        self.editShape(evt.shape, False)
180       
181    def editShape(self, shape=None, new=True):
182        """
183            Rebuild the panel
184        """
185        self.current_shape = shape
186        self.shape_sizer.Clear(True) 
187        #self.type = type 
188       
189        if shape==None:
190            title = wx.StaticText(self, -1, "Use menu to add a shapes", style=wx.ALIGN_LEFT)
191            self.shape_sizer.Add(title, (0,0), (1,2), flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=15)
192
193        else:
194            if new==False:
195                title = wx.StaticText(self, -1, "Edit shape parameters", style=wx.ALIGN_LEFT)
196            else:
197                title = wx.StaticText(self, -1, "Create shape from parameters", style=wx.ALIGN_LEFT)
198               
199            self.shape_sizer.Add(title, (0,0), (1,2), flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=15)
200           
201            n = 1
202            self.parameters = []
203            keys = shape.params.keys()
204            keys.sort()
205           
206            for item in keys:
207                if item in ['contrast', 'order']:
208                    continue
209                n += 1
210                text = wx.StaticText(self, -1, item, style=wx.ALIGN_LEFT)
211                self.shape_sizer.Add(text, (n-1,0), flag = wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
212                ctl = wx.TextCtrl(self, -1, size=(40,20), style=wx.TE_PROCESS_ENTER)
213               
214               
215                ctl.SetValue(str(shape.params[item]))
216                self.Bind(wx.EVT_TEXT_ENTER, self.onTextEnter)
217                ctl.Bind(wx.EVT_KILL_FOCUS, self.onTextEnter)
218                self.parameters.append([item, ctl])
219                self.shape_sizer.Add(ctl, (n-1,1), flag=wx.TOP|wx.BOTTOM, border = 0)
220               
221                # Units
222                units = wx.StaticText(self, -1, shape.details[item], style=wx.ALIGN_LEFT)
223                self.shape_sizer.Add(units, (n-1,2), flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
224
225            for item in ['contrast', 'order']:
226                n += 1
227                text = wx.StaticText(self, -1, item, style=wx.ALIGN_LEFT)
228                self.shape_sizer.Add(text, (n-1,0), flag = wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
229                ctl = wx.TextCtrl(self, -1, size=(40,20), style=wx.TE_PROCESS_ENTER)
230               
231               
232                ctl.SetValue(str(shape.params[item]))
233                self.Bind(wx.EVT_TEXT_ENTER, self.onTextEnter)
234                ctl.Bind(wx.EVT_KILL_FOCUS, self.onTextEnter)
235                self.parameters.append([item, ctl])
236                self.shape_sizer.Add(ctl, (n-1,1), flag=wx.TOP|wx.BOTTOM, border = 0)
237               
238                # Units
239                units = wx.StaticText(self, -1, shape.details[item], style=wx.ALIGN_LEFT)
240                self.shape_sizer.Add(units, (n-1,2), flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border = 15)
241               
242               
243            # Add position
244            n += 1
245           
246            pos_sizer = wx.GridBagSizer(0,0)
247           
248            text = wx.StaticText(self, -1, 'x', style=wx.ALIGN_LEFT)
249            pos_sizer.Add(text, (0,0), (1,1), flag = wx.LEFT|wx.ALIGN_CENTER, border = 15)
250            text = wx.StaticText(self, -1, 'y', style=wx.ALIGN_LEFT)
251            pos_sizer.Add(text, (0,1), (1,1), flag = wx.LEFT|wx.ALIGN_CENTER, border = 15)
252            text = wx.StaticText(self, -1, 'z', style=wx.ALIGN_LEFT)
253            pos_sizer.Add(text, (0,2), (1,1), flag = wx.LEFT|wx.ALIGN_CENTER, border = 15)
254           
255            self.xctrl = wx.TextCtrl(self, -1, size=(40,20), style=wx.TE_PROCESS_ENTER)
256            self.xctrl.SetValue(str(shape.x))
257            self.xctrl.Bind(wx.EVT_KILL_FOCUS, self.onTextEnter)
258            pos_sizer.Add(self.xctrl, (1,0), (1,1), flag = wx.LEFT|wx.ALIGN_CENTER, border = 15)
259            self.yctrl = wx.TextCtrl(self, -1, size=(40,20), style=wx.TE_PROCESS_ENTER)
260            self.yctrl.SetValue(str(shape.y))
261            self.yctrl.Bind(wx.EVT_KILL_FOCUS, self.onTextEnter)
262            pos_sizer.Add(self.yctrl, (1,1), (1,1), flag = wx.LEFT|wx.ALIGN_CENTER, border = 15)
263            self.zctrl = wx.TextCtrl(self, -1, size=(40,20), style=wx.TE_PROCESS_ENTER)
264            self.zctrl.SetValue(str(shape.z))
265            self.zctrl.Bind(wx.EVT_KILL_FOCUS, self.onTextEnter)
266            pos_sizer.Add(self.zctrl, (1,2), (1,1), flag = wx.LEFT|wx.ALIGN_CENTER, border = 15)
267           
268            self.shape_sizer.Add(pos_sizer, (n-1, 0), (1,3), flag=wx.LEFT|wx.ALIGN_CENTER)
269           
270            # Add orientation
271            n += 1
272           
273            pos_sizer = wx.GridBagSizer(0,0)
274           
275            text = wx.StaticText(self, -1, 'theta_x', style=wx.ALIGN_LEFT)
276            pos_sizer.Add(text, (0,0), (1,1), flag = wx.LEFT|wx.ALIGN_CENTER, border = 15)
277            text = wx.StaticText(self, -1, 'theta_y', style=wx.ALIGN_LEFT)
278            pos_sizer.Add(text, (0,1), (1,1), flag = wx.LEFT|wx.ALIGN_CENTER, border = 15)
279            text = wx.StaticText(self, -1, 'theta_z', style=wx.ALIGN_LEFT)
280            pos_sizer.Add(text, (0,2), (1,1), flag = wx.LEFT|wx.ALIGN_CENTER, border = 15)
281           
282            self.actrl = wx.TextCtrl(self, -1, size=(40,20), style=wx.TE_PROCESS_ENTER)
283            self.actrl.SetValue(str(shape.theta_x))
284            self.actrl.Bind(wx.EVT_KILL_FOCUS, self.onTextEnter)
285            pos_sizer.Add(self.actrl, (1,0), (1,1), flag = wx.LEFT|wx.ALIGN_CENTER, border = 15)
286            self.bctrl = wx.TextCtrl(self, -1, size=(40,20), style=wx.TE_PROCESS_ENTER)
287            self.bctrl.SetValue(str(shape.theta_y))
288            self.bctrl.Bind(wx.EVT_KILL_FOCUS, self.onTextEnter)
289            pos_sizer.Add(self.bctrl, (1,1), (1,1), flag = wx.LEFT|wx.ALIGN_CENTER, border = 15)
290            self.cctrl = wx.TextCtrl(self, -1, size=(40,20), style=wx.TE_PROCESS_ENTER)
291            self.cctrl.SetValue(str(shape.theta_z))
292            self.cctrl.Bind(wx.EVT_KILL_FOCUS, self.onTextEnter)
293            pos_sizer.Add(self.cctrl, (1,2), (1,1), flag = wx.LEFT|wx.ALIGN_CENTER, border = 15)
294           
295            self.shape_sizer.Add(pos_sizer, (n-1, 0), (1,3), flag=wx.LEFT|wx.ALIGN_CENTER)
296           
297           
298            # Add a button to create the new shape on the canvas
299            n += 1
300            if new==True:
301                create = wx.Button(self, 1,"Create")
302                self.shape_sizer.Add(create, (n-1,1), (1,2), flag=wx.ALIGN_RIGHT|wx.ALL, border = 15)
303                self.Bind(wx.EVT_BUTTON, self._onCreate, id = 1)       
304            else:
305                create = wx.Button(self, 1,"Apply")
306                self.shape_sizer.Add(create, (n-1,1), (1,2), flag=wx.ALIGN_RIGHT|wx.ALL, border = 15)
307                self.Bind(wx.EVT_BUTTON, self._onEdited, id = 1)       
308               
309
310
311        self.shape_sizer.Layout()
312        self.shape_sizer.Fit(self)
313        try:
314            self.parent.GetSizer().Layout()
315        except:
316            print("TODO: move the Layout call of editShape up to the caller")
317
318    def _readCtrlFloat(self, ctrl):
319        """
320            Parses a TextCtrl for a float value.
321            Returns None is the value hasn't changed or the value is not a float.
322            The control background turns pink if the value is not a float.
323           
324            @param ctrl: TextCtrl object
325        """
326        if ctrl.IsModified():
327            ctrl.SetModified(False)
328            str_value = ctrl.GetValue().lstrip().rstrip()
329            try:
330                flt_value = float(str_value)
331                ctrl.SetBackgroundColour(
332                        wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
333                ctrl.Refresh()
334                return flt_value
335            except:
336                ctrl.SetBackgroundColour("pink")
337                ctrl.Refresh()
338        return None
339
340    def _readCtrlInt(self, ctrl):
341        """
342            Parses a TextCtrl for a float value.
343            Returns None is the value hasn't changed or the value is not a float.
344            The control background turns pink if the value is not a float.
345           
346            @param ctrl: TextCtrl object
347        """
348        if ctrl.IsModified():
349            ctrl.SetModified(False)
350            str_value = ctrl.GetValue().lstrip().rstrip()
351            try:
352                int_value = int(str_value)
353                ctrl.SetBackgroundColour(
354                        wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
355                ctrl.Refresh()
356                return int_value
357            except:
358                ctrl.SetBackgroundColour("pink")
359                ctrl.Refresh()
360        return None
361
362    def _parseCtrl(self):
363        try:
364            # Position
365            tmp = self._readCtrlFloat(self.xctrl)
366            if not tmp==None:
367                self.current_shape.x = tmp
368               
369            tmp = self._readCtrlFloat(self.yctrl)
370            if not tmp==None:
371                self.current_shape.y = tmp
372               
373            tmp = self._readCtrlFloat(self.zctrl)
374            if not tmp==None:
375                self.current_shape.z = tmp
376               
377            # Orientation
378            tmp = self._readCtrlFloat(self.actrl)
379            if not tmp==None:
380                self.current_shape.theta_x = tmp
381               
382            tmp = self._readCtrlFloat(self.bctrl)
383            if not tmp==None:
384                self.current_shape.theta_y = tmp
385               
386            tmp = self._readCtrlFloat(self.cctrl)
387            if not tmp==None:
388                self.current_shape.theta_z = tmp
389               
390            # Parameters
391            for item in self.parameters:
392                tmp = self._readCtrlFloat(item[1])
393                if not tmp==None:
394                    self.current_shape.params[item[0]] = tmp
395        except:
396            print("Could not create")
397            print(sys.exc_value)
398               
399    def _onCreate(self, evt):
400        """
401            Create a new shape with the parameters given by the user
402        """
403        self._parseCtrl()
404        event = CreateShapeEvent(shape=self.current_shape, new=True)
405        wx.PostEvent(self.parent, event)
406        self.editShape(self.current_shape, False)
407           
408       
409    def _onEdited(self, evt):
410        self._parseCtrl()
411        event = CreateShapeEvent(shape=self.current_shape, new=False)
412        wx.PostEvent(self.parent, event)
413        self.editShape(self.current_shape, False)
414
415    def onTextEnter(self, evt): 
416        """
417            Read text field to check values
418        """ 
419        self._readCtrlFloat(self.xctrl)
420        self._readCtrlFloat(self.yctrl)
421        self._readCtrlFloat(self.zctrl)
422        self._readCtrlFloat(self.actrl)
423        self._readCtrlFloat(self.bctrl)
424        self._readCtrlFloat(self.cctrl)
425        for item in self.parameters:
426            self._readCtrlFloat(item[1])
427 
428    #-- Methods to support list of shapes --
429    def _on_add_shape_to_listbox(self, event):
430        """
431            Process a new shape           
432            @param event: EVT_ADD_SHAPE event
433        """
434        event.Skip()
435        if event.new:
436            self.counter += 1
437            self.shape_listbox.Insert("%d: %s" % (self.counter, event.shape.name), 
438                                 self.shape_listbox.GetCount(), event.shape)
439
440    def _on_select_from_listbox(self, event):
441        """
442            Process item selection events
443        """
444        index = event.GetSelection()
445        view_name = self.shape_listbox.GetString(index)
446        self.editShape(shape=event.GetClientData(), new=False)
447        # The following event is bound to the SimPanel to highlight the shape in blue
448        #TODO: how come it doesn't work?
449        wx.PostEvent(self.parent, EditShapeEvent(shape=event.GetClientData()))
450        #TODO: select the shape from the drop down
451       
452    def _on_listbox_context_menu(self, event):
453        """
454            Popup context menu event
455        """
456        # Create context menu
457        popupmenu = wx.Menu()
458       
459        # Create an item to delete the selected shape from the canvas
460        id = wx.NewId()
461        popupmenu.Append(id, "&Remove Selected")
462        wx.EVT_MENU(self, id, self._on_remove_shape_from_listbox)
463       
464        # Create an item to rename a shape to a more user friendly name
465        #id = wx.NewId()
466        #popupmenu.Append(102, "&Rename Selected")
467        #wx.EVT_MENU(self, 102, self.onRename)
468
469        pos = event.GetPosition()
470        pos = self.ScreenToClient(pos)
471        self.PopupMenu(popupmenu, pos)       
472       
473    def _on_remove_shape_from_listbox(self, ev):
474        """
475            Remove an item
476        """
477        indices = self.shape_listbox.GetSelections()
478        if len(indices)>0:
479            name =  self.shape_listbox.GetClientData(indices[0]).name
480            self.shape_listbox.Delete(indices[0])
481            wx.PostEvent(self.parent, DelShapeEvent(id = name))
482       
483    def _on_rename_shape_from_listbox(self, ev):
484        """
485            Rename an item
486        """
487        indices = self.shape_listbox.GetSelections()
488        if len(indices)>0:
489            print("NOT YET IMPLMENTED")
490            print("renaming", self.shape_listbox.GetString(indices[0]))
491               
Note: See TracBrowser for help on using the repository browser.