Run hooke.command.Argument.callback every time (if defined).
[hooke.git] / hooke / hooke_cli.py
1 class HookeCli(cmd.Cmd, object):
2
3     def __init__(self,frame,list_of_events,events_from_gui,config,drivers):
4         cmd.Cmd.__init__(self)
5
6         self.prompt = 'hooke: '
7         self.current_list=[] #the playlist we're using
8         self.current=None    #the current curve under analysis.
9         self.plots=None
10         '''
11         The actual hierarchy of the "current curve" is a bit complex:
12
13         self.current = the lhc.HookeCurve container object of the current curve
14         self.current.curve = the current "real" curve object as defined in the filetype driver class
15         self.current.curve.default_plots() = the default plots of the filetype driver.
16
17         The plot objects obtained by mean of self.current.curve.default_plots()
18         then undergoes modifications by the plotmanip
19         modifier functions. The modified plot is saved in self.plots and used if needed by other functions.
20         '''
21         self.pointer=0       #a pointer to navigate the current list
22
23         #Things that come from outside
24         self.frame=frame                        #the wx frame we refer to
25         self.list_of_events=list_of_events      #a list of wx events we use to interact with the GUI
26         self.events_from_gui=events_from_gui    #the Queue object we use to have messages from the GUI
27         self.config=config                      #the configuration dictionary
28         self.drivers=drivers                    #the file format drivers
29
30         #get plot manipulation functions
31         plotmanip_functions=[]
32         for object_name in dir(self):
33                 if object_name[0:9]=='plotmanip':
34                     plotmanip_functions.append(getattr(self,object_name))
35         #put plotmanips in order
36         self.plotmanip=[None for item in self.config['plotmanips']]
37         for item in plotmanip_functions:
38             namefunction=item.__name__[10:]
39             if namefunction in self.config['plotmanips']:
40                 nameindex=self.config['plotmanips'].index(namefunction) #index of function in plotmanips config
41                 self.plotmanip[nameindex] = item
42             else:
43                 pass
44
45
46         self.playlist_saved=0 #Did we save the playlist?
47         self.playlist_name='' #Name of playlist
48         self.notes_saved=1 #Did we save the notes?
49         self.notes_filename=None #Name of notes
50
51         #create outlet
52         self.outlet=lout.Outlet()
53
54         #Data that must be saved in the playlist, related to the whole playlist (not individual curves)
55         self.playlist_generics={}
56
57         #make sure we execute _plug_init() for every command line plugin we import
58         for plugin_name in self.config['plugins']:
59             try:
60                 plugin=__import__(plugin_name)
61                 try:
62                     eval('plugin.'+plugin_name+'Commands._plug_init(self)')
63                 except AttributeError:
64                     pass
65             except ImportError:
66                 pass
67
68         #load default list, if possible
69         self.do_loadlist(self.config['defaultlist'])
70
71 #HELPER FUNCTIONS
72 #Everything sending an event should be here
73     def _measure_N_points(self, N, whatset=1):
74         '''
75         general helper function for N-points measures
76         '''
77         wx.PostEvent(self.frame,self.list_of_events['measure_points'](num_of_points=N, set=whatset))
78         while 1:
79             try:
80                 points=self.frame.events_from_gui.get()
81                 break
82             except Empty:
83                 pass
84         return points
85
86     def _get_displayed_plot(self,dest=0):
87         '''
88         returns the currently displayed plot.
89         '''
90         wx.PostEvent(self.frame, self.list_of_events['get_displayed_plot'](dest=dest))
91         while 1:
92             try:
93                 displayed_plot=self.events_from_gui.get()
94             except Empty:
95                 pass
96             if displayed_plot:
97                 break
98         return displayed_plot
99
100     def _send_plot(self,plots):
101         '''
102         sends a plot to the GUI
103         '''
104         wx.PostEvent(self.frame, self.list_of_events['plot_graph'](plots=plots))
105         return
106
107     def _find_plotmanip(self, name):
108         '''
109         returns a plot manipulator function from its name
110         '''
111         return self.plotmanip[self.config['plotmanips'].index(name)]
112
113     def _clickize(self, xvector, yvector, index):
114         '''
115         returns a ClickedPoint() object from an index and vectors of x, y coordinates
116         '''
117         point=ClickedPoint()
118         point.index=index
119         point.absolute_coords=xvector[index],yvector[index]
120         point.find_graph_coords(xvector,yvector)
121         return point
122
123 #HERE COMMANDS BEGIN
124
125     def help_set(self):
126         print '''
127 SET
128 Sets a local configuration variable
129 -------------
130 Syntax: set [variable] [value]
131         '''
132     def do_set(self,args):
133         #FIXME: some variables in self.config should be hidden or intelligently configurated...
134         args=args.split()
135         if len(args)==0:
136             print 'You must specify a variable and a value'
137             print 'Available variables:'
138             print self.config.keys()
139             return
140         if args[0] not in self.config.keys():
141             print 'This is not an internal Hooke variable!'
142             return
143         if len(args)==1:
144             #FIXME:we should reload the config file and reset the config value
145             print self.config[args[0]]
146             return
147         key=args[0]
148         try: #try to have a numeric value
149             value=float(args[1])
150         except ValueError: #if it cannot be converted to float, it's None, or a string...
151             value=args[1]
152             if value.lower()=='none':
153                 value=None
154             else:
155                 value=args[1]
156
157         self.config[key]=value
158         self.do_plot(0)
159
160     def help_printlist(self):
161         print '''
162 PRINTLIST
163 Prints the list of curves in the current playlist
164 -------------
165 Syntax: printlist
166 '''
167     def do_printlist(self,args):
168         for item in self.current_list:
169             print item.path
170
171
172     def help_jump(self):
173         print '''
174 JUMP
175 Jumps to a given curve.
176 ------
177 Syntax: jump {$curve}
178
179 If the curve is not in the current playlist, it politely asks if we want to add it.
180         '''
181     def do_jump(self,filename):
182         '''
183         jumps to the curve with the given filename.
184         if the filename is not in the playlist, it asks if we must add it or not.
185         '''
186
187         if filename=='':
188             filename=linp.safeinput('Jump to?')
189
190         filepath=os.path.abspath(filename)
191         print filepath
192
193         c=0
194         item_not_found=1
195         while item_not_found:
196             try:
197
198                 if self.current_list[c].path == filepath:
199                     self.pointer=c
200                     self.current=self.current_list[self.pointer]
201                     item_not_found=0
202                     self.do_plot(0)
203                 else:
204                     c+=1
205             except IndexError:
206                 #We've found the end of the list.
207                 answer=linp.safeinput('Curve not found in playlist. Add it to list?',['y'])
208                 if answer.lower()[0]=='y':
209                     try:
210                         self.do_addtolist(filepath)
211                     except:
212                         print 'Curve file not found.'
213                         return
214                     self.current=self.current_list[-1]
215                     self.pointer=(len(current_list)-1)
216                     self.do_plot(0)
217
218                 item_not_found=0
219
220
221     def do_index(self,args):
222         '''
223         INDEX
224         Prints the index of the current curve in the list
225         -----
226         Syntax: index
227         '''
228         print self.pointer+1, 'of', len(self.current_list)
229
230
231     def help_next(self):
232         print '''
233 NEXT
234 Go the next curve in the playlist.
235 If we are at the last curve, we come back to the first.
236 -----
237 Syntax: next, n
238         '''
239     def do_next(self,args):
240         try:
241             self.current.curve.close_all()
242         except:
243             print 'No curve file loaded, currently!'
244             print 'This should not happen, report to http://code.google.com/p/hooke'
245             return
246
247         if self.pointer == (len(self.current_list)-1):
248             self.pointer=0
249             print 'Playlist finished; back to first curve.'
250         else:
251             self.pointer+=1
252
253         self.current=self.current_list[self.pointer]
254         self.do_plot(0)
255
256
257     def help_n(self):
258         self.help_next()
259     def do_n(self,args):
260         self.do_next(args)
261
262     def help_previous(self,args):
263         print '''
264 PREVIOUS
265 Go to the previous curve in the playlist.
266 If we are at the first curve, we jump to the last.
267 -------
268 Syntax: previous, p
269     '''
270     def do_previous(self,args):
271         try:
272             self.current.curve.close_all()
273         except:
274             print 'No curve file loaded, currently!'
275             print 'This should not happen, report to http://code.google.com/p/hooke'
276             return
277         if self.pointer == 0:
278             self.pointer=(len(self.current_list)-1)
279             print 'Start of playlist; jump to last curve.'
280         else:
281             self.pointer-=1
282
283         self.current=self.current_list[self.pointer]
284         self.do_plot(args)
285
286
287     def help_p(self):
288         self.help_previous()
289     def do_p(self,args):
290         self.do_previous(args)
291
292
293 #PLOT INTERACTION COMMANDS
294 #-------------------------------
295     def help_plot(self):
296         print '''
297 PLOT
298 Plots the current force curve
299 -------
300 Syntax: plot
301         '''
302     def do_plot(self,args):
303         if self.current.identify(self.drivers) == False:
304             return
305         self.plots=self.current.curve.default_plots()
306         try:
307             self.plots=self.current.curve.default_plots()
308         except Exception, e:
309             print 'Unexpected error occurred in do_plot().'
310             print e
311             return
312
313         #apply the plotmanip functions eventually present
314         nplots=len(self.plots)
315         c=0
316         while c<nplots:
317             for function in self.plotmanip: #FIXME: something strange happens about self.plotmanip[0]
318                 self.plots[c]=function(self.plots[c], self.current)
319
320             self.plots[c].xaxes=self.config['xaxes'] #FIXME: in the future, xaxes and yaxes should be set per-plot
321             self.plots[c].yaxes=self.config['yaxes']
322
323             c+=1
324
325         self._send_plot(self.plots)
326
327     def _delta(self, set=1):
328         '''
329         calculates the difference between two clicked points
330         '''
331         print 'Click two points'
332         points=self._measure_N_points(N=2, whatset=set)
333         dx=abs(points[0].graph_coords[0]-points[1].graph_coords[0])
334         dy=abs(points[0].graph_coords[1]-points[1].graph_coords[1])
335         unitx=self.plots[points[0].dest].units[0]
336         unity=self.plots[points[0].dest].units[1]
337         return dx,unitx,dy,unity
338
339     def do_delta(self,args):
340         '''
341         DELTA
342
343         Measures the delta X and delta Y between two points.
344         ----
345         Syntax: delta
346         '''
347         dx,unitx,dy,unity=self._delta()
348         print str(dx)+' '+unitx
349         print str(dy)+' '+unity
350
351     def _point(self, set=1):
352         '''calculates the coordinates of a single clicked point'''
353
354         print 'Click one point'
355         point=self._measure_N_points(N=1, whatset=set)
356
357         x=point[0].graph_coords[0]
358         y=point[0].graph_coords[1]
359         unitx=self.plots[point[0].dest].units[0]
360         unity=self.plots[point[0].dest].units[1]
361         return x,unitx,y,unity
362
363     def do_point(self,args):
364         '''
365         POINT
366
367         Returns the coordinates of a point on the graph.
368         ----
369         Syntax: point
370         '''
371         x,unitx,y,unity=self._point()
372         print str(x)+' '+unitx
373         print str(y)+' '+unity
374         to_dump='point '+self.current.path+' '+str(x)+' '+unitx+', '+str(y)+' '+unity
375         self.outlet.push(to_dump)
376
377
378     def do_close(self,args=None):
379         '''
380         CLOSE
381         Closes one of the two plots. If no arguments are given, the bottom plot is closed.
382         ------
383         Syntax: close [top,bottom]
384         '''
385         if args=='top':
386             to_close=0
387         elif args=='bottom':
388             to_close=1
389         else:
390             to_close=1
391
392         close_plot=self.list_of_events['close_plot']
393         wx.PostEvent(self.frame, close_plot(to_close=to_close))
394
395     def do_show(self,args=None):
396         '''
397         SHOW
398         Shows both plots.
399         '''
400         show_plots=self.list_of_events['show_plots']
401         wx.PostEvent(self.frame, show_plots())
402
403
404
405     #PLOT EXPORT AND MANIPULATION COMMANDS
406     def help_export(self):
407         print '''
408 EXPORT
409 Saves the current plot as an image file
410 ---------------
411 Syntax: export [filename] {plot to export}
412
413 The supported formats are PNG and EPS; the file extension of the filename is automatically recognized
414 and correctly exported. Resolution is (for now) fixed at 150 dpi.
415
416 If you have a multiple plot, the optional plot to export argument tells Hooke which plot you want to export. If 0, the top plot is exported. If 1, the bottom plot is exported (Exporting both plots is still to implement)
417         '''
418     def do_export(self,args):
419         #FIXME: the bottom plot doesn't have the title
420
421         dest=0
422
423         if len(args)==0:
424             #FIXME: We have to go into the libinput stuff and fix it, for now here's a dummy replacement...
425             #name=linp.safeinput('Filename?',[self.current.path+'.png'])
426             name=raw_input('Filename? ')
427         else:
428             args=args.split()
429             name=args[0]
430             if len(args) > 1:
431                 dest=int(args[1])
432
433         export_image=self.list_of_events['export_image']
434         wx.PostEvent(self.frame, export_image(name=name, dest=dest))
435
436
437     def help_txt(self):
438         print '''
439 TXT
440 Saves the current curve as a text file
441 Columns are, in order:
442 X1 , Y1 , X2 , Y2 , X3 , Y3 ...
443
444 -------------
445 Syntax: txt [filename] {plot to export}
446         '''
447     def do_txt(self,args):
448
449         def transposed2(lists, defval=0):
450             '''
451             transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
452             elements
453             (by Zoran Isailovski on the Python Cookbook online)
454             '''
455             if not lists: return []
456             return map(lambda *row: [elem or defval for elem in row], *lists)
457
458         whichplot=0
459         args=args.split()
460         if len(args)==0:
461             filename=linp.safeinput('Filename?',[self.current.path+'.txt'])
462         else:
463             filename=linp.checkalphainput(args[0],self.current.path+'.txt',[])
464             try:
465                 whichplot=int(args[1])
466             except:
467                 pass
468
469         try:
470             outofplot=self.plots[whichplot].vectors
471         except:
472             print "Plot index out of range."
473             return 0
474
475         columns=[]     
476         for dataset in self.plots[whichplot].vectors:
477             for i in range(0,len(dataset)):
478                 columns.append([])
479                 for value in dataset[i]:
480                     columns[-1].append(str(value))
481
482         rows=transposed2(columns, 'nan')
483         rows=[' , '.join(item) for item in rows]
484         text='\n'.join(rows)
485
486         txtfile=open(filename,'w+')
487         #Save units of measure in header
488         txtfile.write('X:'+self.plots[whichplot].units[0]+'\n')
489         txtfile.write('Y:'+self.plots[whichplot].units[1]+'\n')
490         txtfile.write(text)
491         txtfile.close()
492
493
494     #LOGGING, REPORTING, NOTETAKING
495
496
497     def do_note_old(self,args):
498         '''
499         NOTE_OLD
500         **deprecated**: Use note instead. Will be removed in 0.9
501
502         Writes or displays a note about the current curve.
503         If [anything] is empty, it displays the note, otherwise it adds a note.
504         The note is then saved in the playlist if you issue a savelist command
505         ---------------
506         Syntax: note_old [anything]
507
508         '''
509         if args=='':
510             print self.current_list[self.pointer].notes
511         else:
512             #bypass UnicodeDecodeError troubles
513             try:
514                 args=args.decode('ascii')
515             except:
516                 args=args.decode('ascii','ignore')
517                 if len(args)==0:
518                     args='?'
519
520             self.current_list[self.pointer].notes=args
521         self.notes_saved=0
522
523
524     def do_note(self,args):
525         '''
526         NOTE
527
528         Writes or displays a note about the current curve.
529         If [anything] is empty, it displays the note, otherwise it adds a note.
530         The note is then saved in the playlist if you issue a savelist command.
531         ---------------
532         Syntax: note_old [anything]
533
534         '''
535         if args=='':
536             print self.current_list[self.pointer].notes
537         else:
538             if self.notes_filename == None:
539                 if not os.path.exists(os.path.realpath('output')):
540                     os.mkdir('output')
541                 self.notes_filename=raw_input('Notebook filename? ')
542                 self.notes_filename=os.path.join(os.path.realpath('output'),self.notes_filename)
543                 title_line='Notes taken at '+time.asctime()+'\n'
544                 f=open(self.notes_filename,'a')
545                 f.write(title_line)
546                 f.close()
547
548             #bypass UnicodeDecodeError troubles
549             try:
550                args=args.decode('ascii')
551             except:
552                args=args.decode('ascii','ignore')
553                if len(args)==0:
554                    args='?'
555             self.current_list[self.pointer].notes=args
556
557             f=open(self.notes_filename,'a+')
558             note_string=(self.current.path+'  |  '+self.current.notes+'\n')
559             f.write(note_string)
560             f.close()
561
562     def help_notelog(self):
563         print '''
564 NOTELOG
565 Writes a log of the notes taken during the session for the current
566 playlist
567 --------------
568 Syntax notelog [filename]
569 '''
570     def do_notelog(self,args):
571
572         if len(args)==0:
573             args=linp.safeinput('Notelog filename?',['notelog.txt'])
574
575         note_lines='Notes taken at '+time.asctime()+'\n'
576         for item in self.current_list:
577             if len(item.notes)>0:
578                 #FIXME: log should be justified
579                 #FIXME: file path should be truncated...
580                 note_string=(item.path+'  |  '+item.notes+'\n')
581                 note_lines+=note_string
582
583         try:
584             f=open(args,'a+')
585             f.write(note_lines)
586             f.close()
587         except IOError, (ErrorNumber, ErrorMessage):
588             print 'Error: notes cannot be saved. Catched exception:'
589             print ErrorMessage
590
591         self.notes_saved=1
592
593     def help_copylog(self):
594         print '''
595 COPYLOG
596 Moves the annotated curves to another directory
597 -----------
598 Syntax copylog [directory]
599         '''
600     def do_copylog(self,args):
601
602         if len(args)==0:
603             args=linp.safeinput('Destination directory?')  #TODO default
604
605         mydir=os.path.abspath(args)
606         if not os.path.isdir(mydir):
607             print 'Destination is not a directory.'
608             return
609
610         for item in self.current_list:
611             if len(item.notes)>0:
612                 try:
613                     shutil.copy(item.path, mydir)
614                 except (OSError, IOError):
615                     print 'Cannot copy file. '+item.path+' Perhaps you gave me a wrong directory?'