Initial SVN upload
[hooke.git] / hooke_cli.py
1 #!/usr/bin/env python
2
3 '''
4 hooke_cli.py
5
6 Command line module of Hooke.
7
8 Copyright (C) 2006 Massimo Sandal (University of Bologna, Italy).
9
10 This program is released under the GNU General Public License version 2.
11 '''
12
13
14 from libhooke import * #FIXME
15 import libhookecurve as lhc
16
17 from libhooke import WX_GOOD
18 from libhooke import HOOKE_VERSION
19
20 import wxversion
21 wxversion.select(WX_GOOD)
22 import wx
23
24 from wx.lib.newevent import NewEvent
25 from matplotlib.numerix import * #FIXME
26
27 import xml.dom.minidom
28 import sys, os, os.path, glob, shutil
29 import Queue
30 import cmd
31 import time
32
33 global __version__
34 global __codename__
35 global __releasedate__
36 __version__ = HOOKE_VERSION[0]
37 __codename__ = HOOKE_VERSION[1]
38 __releasedate__ = HOOKE_VERSION[2]
39
40 from matplotlib import __version__ as mpl_version
41 from wx import __version__ as wx_version
42 from wxmpl import __version__ as wxmpl_version
43 from scipy import __version__ as scipy_version
44 from numpy import __version__ as numpy_version
45 from sys import version as python_version
46 import platform
47
48
49 class HookeCli(cmd.Cmd):
50     
51     def __init__(self,frame,list_of_events,events_from_gui,config,drivers):
52         cmd.Cmd.__init__(self)
53                        
54         self.prompt = 'hooke: '
55         
56         
57         self.current_list=[] #the playlist we're using
58         
59         self.current=None    #the current curve under analysis. 
60         self.plots=None
61         '''
62         The actual hierarchy of the "current curve" is a bit complex:
63         
64         self.current = the lhc.HookeCurve container object of the current curve
65         self.current.curve = the current "real" curve object as defined in the filetype driver class
66         self.current.curve.default_plots() = the default plots of the filetype driver.
67         
68         The plot objects obtained by mean of self.current.curve.default_plots() 
69         then undergoes modifications by the plotmanip
70         modifier functions. The modified plot is saved in self.plots and used if needed by other functions.       
71         '''
72         
73         
74         self.pointer=0       #a pointer to navigate the current list
75                         
76         #Things that come from outside
77         self.frame=frame                        #the wx frame we refer to
78         self.list_of_events=list_of_events      #a list of wx events we use to interact with the GUI
79         self.events_from_gui=events_from_gui    #the Queue object we use to have messages from the GUI
80         self.config=config                      #the configuration dictionary
81         self.drivers=drivers                    #the file format drivers
82         
83         #get plot manipulation functions
84         plotmanip_functions=[]
85         for object_name in dir(self):
86                 if object_name[0:9]=='plotmanip':
87                     plotmanip_functions.append(getattr(self,object_name))
88         #put plotmanips in order
89         self.plotmanip=[None for item in self.config['plotmanips']]
90         for item in plotmanip_functions:
91             namefunction=item.__name__[10:]
92             if namefunction in self.config['plotmanips']:
93                 nameindex=self.config['plotmanips'].index(namefunction) #index of function in plotmanips config
94                 self.plotmanip[nameindex] = item
95             else:
96                 pass
97            
98             
99         self.playlist_saved=0
100         self.playlist_name=''
101         self.notes_saved=1
102         
103         #Data that must be saved in the playlist, related to the whole playlist (not individual curves)
104         self.playlist_generics={} 
105         
106         #make sure we execute _plug_init() for every command line plugin we import
107         for plugin_name in self.config['plugins']:
108             try:
109                 plugin=__import__(plugin_name)
110                 try:
111                     eval('plugin.'+plugin_name+'Commands._plug_init(self)')
112                 except AttributeError:
113                     pass
114             except ImportError:
115                 pass
116
117         
118 #HELPER FUNCTIONS
119 #Everything sending an event should be here
120     def _measure_N_points(self, N, whatset=1):
121         '''
122         general helper function for N-points measures
123         '''
124         measure_points=self.list_of_events['measure_points']
125         wx.PostEvent(self.frame, measure_points(num_of_points=N, set=whatset))
126         while 1:
127             try:
128                 points=self.frame.events_from_gui.get()
129                 break
130             except Empty:
131                 pass
132         return points
133         
134     def _get_displayed_plot(self,dest=0):
135         '''
136         returns the currently displayed plot.
137         '''
138         wx.PostEvent(self.frame, self.list_of_events['get_displayed_plot'](dest=dest))
139         while 1:
140             try:
141                 displayed_plot=self.events_from_gui.get()
142             except Empty:
143                 pass
144             if displayed_plot:
145                 break
146         return displayed_plot
147     
148     def _send_plot(self,plots):
149         '''
150         sends a plot to the GUI
151         '''
152         wx.PostEvent(self.frame, self.list_of_events['plot_graph'](plots=plots))
153         return
154         
155     def _find_plotmanip(self, name):
156         '''
157         returns a plot manipulator function from its name
158         '''
159         return self.plotmanip[self.config['plotmanips'].index(name)]
160     
161 #HERE COMMANDS BEGIN
162     
163     def help_set(self):
164         print '''
165 SET
166 Sets a local configuration variable
167 -------------
168 Syntax: set [variable] [value]
169         '''
170     def do_set(self,args):
171         #FIXME: some variables in self.config should be hidden or intelligently configurated...
172         args=args.split()
173         if len(args)==0:
174             print 'You must specify a variable and a value'
175             print 'Available variables:'
176             print self.config.keys()
177             return
178         if args[0] not in self.config.keys():
179             print 'This is not an internal Hooke variable!'
180             return
181         if len(args)==1:
182             #FIXME:we should reload the config file and reset the config value
183             print self.config[args[0]]
184             return
185         key=args[0]
186         try: #try to have a numeric value
187             value=float(args[1])
188         except ValueError: #if it cannot be converted to float, it's None, or a string...
189             if value.lower()=='none':
190                 value=None
191             else:
192                 value=args[1]
193                 
194         self.config[key]=value
195         self.do_plot(0)
196         
197 #PLAYLIST MANAGEMENT AND NAVIGATION
198 #------------------------------------
199     
200     def help_loadlist(self):
201         print '''
202 LOADLIST
203 Loads a file playlist
204 -----------
205 Syntax: loadlist [playlist file]
206         '''
207     def do_loadlist(self, args):
208         #checking for args: if nothing is given as input, we warn and exit.
209         while len(args)==0:
210             args=raw_input('File to load?')
211         
212         arglist=args.split()
213         play_to_load=arglist[0]
214         
215         #We assume a Hooke playlist has the extension .hkp
216         if play_to_load[-4:] != '.hkp':
217             play_to_load+='.hkp'
218         
219         try:            
220             playxml=PlaylistXML()
221             self.current_list, self.playlist_generics=playxml.load(play_to_load)
222             self.current_playxml=playxml
223         except IOError:
224             print 'File not found.'
225             return
226         
227         print 'Loaded %s curves' %len(self.current_list)
228         
229         if 'pointer' in self.playlist_generics.keys():
230             self.pointer=int(self.playlist_generics['pointer'])
231         else:
232             #if no pointer is found, set the current curve as the first curve of the loaded playlist
233             self.pointer=0
234         print 'Starting at curve ',self.pointer
235             
236         self.current=self.current_list[self.pointer]
237         
238         #resets saved/notes saved state
239         self.playlist_saved=0
240         self.playlist_name=''
241         self.notes_saved=0        
242     
243         self.do_plot(0)
244         
245         
246     def help_genlist(self):
247         print '''
248 GENLIST
249 Generates a file playlist.
250 Note it doesn't *save* it: see savelist for this.
251
252 If [input files] is a directory, it will use all files in the directory for playlist.
253 So:
254 genlist dir
255 genlist dir/
256 genlist dir/*.*
257
258 are all equivalent syntax.
259 ------------
260 Syntax: genlist [input files]
261         
262 '''
263     def do_genlist(self,args):
264         #args list is: input path, output name
265         if len(args)==0:
266             args=raw_input('Input files?')
267                     
268         arglist=args.split()      
269         list_path=arglist[0]
270                   
271         #if it's a directory, is like /directory/*.*
272         #FIXME: probably a bit kludgy.
273         if os.path.isdir(list_path): 
274             if platform.system == 'Windows':
275                 SLASH="\\"
276             else:
277                 SLASH="/"
278             if list_path[-1] == SLASH:
279                 list_path=list_path+'*.*'
280             else:    
281                 list_path=list_path+SLASH+'*.*'
282         
283         #expanding correctly the input list with the glob module :)        
284         list_files=glob.glob(list_path)
285         list_files.sort()
286
287         self.current_list=[]
288         for item in list_files:
289             try:
290                 self.current_list.append(lhc.HookeCurve(os.path.abspath(item))) 
291             except:
292                 pass
293             
294         self.pointer=0    
295         if len(self.current_list)>0:
296             self.current=self.current_list[self.pointer]
297         else:
298             print 'Empty list!'
299             return
300         
301         #resets saved/notes saved state
302         self.playlist_saved=0
303         self.playlist_name=''
304         self.notes_saved=0  
305         
306         self.do_plot(0)
307        
308         
309     def do_savelist(self,args):
310         '''
311         SAVELIST
312         Saves the current file playlist on disk.
313         ------------
314         Syntax: savelist [filename]
315         '''
316         while len(args)==0:
317             args=raw_input('Input files?')
318     
319         output_filename=args
320         
321         self.playlist_generics['pointer']=self.pointer
322         
323         #autocomplete filename if not specified
324         if output_filename[-4:] != '.hkp':
325             output_filename+='.hkp'
326         
327         playxml=PlaylistXML()
328         playxml.export(self.current_list, self.playlist_generics)
329         playxml.save(output_filename)                  
330         
331         #remembers we have saved playlist
332         self.playlist_saved=1
333         
334     def help_addtolist(self):
335         print '''
336 ADDTOLIST
337 Adds a file to the current playlist
338 --------------
339 Syntax: addtolist [filename]
340 '''
341     def do_addtolist(self,args):
342         #args list is: input path
343         if len(args)==0:
344             print 'You must give the input filename you want to add'
345             self.help_addtolist()
346             return
347           
348         filenames=glob.glob(args)
349         
350         for filename in filenames:
351             self.current_list.append(lhc.HookeCurve(os.path.abspath(filename)))
352         #we need to save playlist
353         self.playlist_saved=0
354     
355     def help_printlist(self):
356         print '''
357 PRINTLIST
358 Prints the list of curves in the current playlist
359 -------------
360 Syntax: printlist
361 '''
362     def do_printlist(self,args):
363         for item in self.current_list:
364             print item.path
365             
366     
367     def help_jump(self):
368         print '''
369 JUMP
370 Jumps to a given curve.
371 ------
372 Syntax: jump {$curve}
373
374 If the curve is not in the current playlist, it politely asks if we want to add it.
375         '''  
376     def do_jump(self,filename):
377         '''
378         jumps to the curve with the given filename.
379         if the filename is not in the playlist, it asks if we must add it or not.
380         '''
381         
382         if filename=='':
383             filename=raw_input('Jump to?')
384             
385         filepath=os.path.abspath(filename)
386         print filepath
387                 
388         c=0
389         item_not_found=1
390         while item_not_found:
391             try:
392                 
393                 if self.current_list[c].path == filepath:
394                     self.pointer=c
395                     self.current=self.current_list[self.pointer]
396                     item_not_found=0
397                     self.do_plot(0)
398                 else:
399                     c+=1  
400             except IndexError:
401                 #We've found the end of the list.
402                 answer=raw_input('Curve not found in playlist. Add it to list?')
403                 if answer.lower()[0]=='y':
404                     try:
405                         self.do_addtolist(filepath)
406                     except:
407                         print 'Curve file not found.'
408                         return
409                     self.current=self.current_list[-1]
410                     self.pointer=(len(current_list)-1)
411                     self.do_plot(0)
412                     
413                 item_not_found=0
414     
415     
416     def do_index(self,args):
417         '''
418         INDEX
419         Prints the index of the current curve in the list
420         -----
421         Syntax: index
422         '''
423         print self.pointer+1, 'of', len(self.current_list) 
424     
425     
426     def help_next(self):
427         print '''
428 NEXT
429 Go the next curve in the playlist.
430 If we are at the last curve, we come back to the first.
431 -----
432 Syntax: next, n
433         '''
434     def do_next(self,args):
435         self.current.curve.close_all()
436         if self.pointer == (len(self.current_list)-1):
437             self.pointer=0
438             print 'Playlist finished; back to first curve.'
439         else:
440             self.pointer+=1
441         
442         self.current=self.current_list[self.pointer]
443         self.do_plot(0)
444         
445     
446     def help_n(self):
447         self.help_next()
448     def do_n(self,args):
449         self.do_next(args)
450         
451     def help_previous(self,args):
452         print '''
453 PREVIOUS
454 Go to the previous curve in the playlist.
455 If we are at the first curve, we jump to the last.
456 -------
457 Syntax: previous, p
458     '''
459     def do_previous(self,args):
460         self.current.curve.close_all()
461         if self.pointer == 0:
462             self.pointer=(len(self.current_list)-1)
463             print 'Start of playlist; jump to last curve.' 
464         else:
465             self.pointer-=1
466             
467         self.current=self.current_list[self.pointer]
468         self.do_plot(args)
469         
470             
471     def help_p(self):
472         self.help_previous()
473     def do_p(self,args):
474         self.do_previous(args)
475
476         
477 #PLOT INTERACTION COMMANDS
478 #-------------------------------    
479     def help_plot(self):
480         print '''
481 PLOT
482 Plots the current force curve
483 -------
484 Syntax: plot
485         '''
486     def do_plot(self,args):
487         
488         self.current.identify(self.drivers)
489         self.plots=self.current.curve.default_plots()
490         try:
491             self.plots=self.current.curve.default_plots()
492         except Exception, e:
493             print 'Unexpected error occurred in do_plot().'
494             print e
495             return
496             
497         #apply the plotmanip functions eventually present
498         nplots=len(self.plots)
499         c=0
500         while c<nplots:
501             for function in self.plotmanip: #FIXME: something strange happens about self.plotmanip[0]
502                 self.plots[c]=function(self.plots[c], self.current)
503                 
504             self.plots[c].xaxes=self.config['xaxes'] #FIXME: in the future, xaxes and yaxes should be set per-plot
505             self.plots[c].yaxes=self.config['yaxes']
506                 
507             c+=1
508
509         self._send_plot(self.plots)
510         
511     def _delta(self, set=1):
512         '''
513         calculates the difference between two clicked points
514         '''
515         print 'Click two points'
516         points=self._measure_N_points(N=2, whatset=set)
517         dx=abs(points[0].graph_coords[0]-points[1].graph_coords[0])
518         dy=abs(points[0].graph_coords[1]-points[1].graph_coords[1])
519         unitx=self.plots[points[0].dest].units[0]
520         unity=self.plots[points[0].dest].units[1]
521         return dx,unitx,dy,unity
522         
523     def do_delta(self,args):
524         '''
525         DELTA
526         
527         Measures the delta X and delta Y between two points.
528         ----
529         Syntax: delta
530         '''
531         dx,unitx,dy,unity=self._delta()
532         print str(dx)+' '+unitx
533         print str(dy)+' '+unity
534     
535     def _point(self, set=1):
536         '''calculates the coordinates of a single clicked point'''
537
538         print 'Click one point'
539         point=self._measure_N_points(N=1, whatset=set)
540         
541         x=point[0].graph_coords[0]
542         y=point[0].graph_coords[1]
543         unitx=self.plots[point[0].dest].units[0]
544         unity=self.plots[point[0].dest].units[1]
545         return x,unitx,y,unity
546         
547     def do_point(self,args):
548         '''
549         POINT
550         
551         Returns the coordinates of a point on the graph.
552         ----
553         Syntax: point
554         '''
555         x,unitx,y,unity=self._point()
556         print str(x)+' '+unitx
557         print str(y)+' '+unity
558             
559    
560         
561     def do_close(self,args=None):
562         '''
563         CLOSE
564         Closes one of the two plots. If no arguments are given, the bottom plot is closed.
565         ------
566         Syntax: close [top,bottom]
567         '''
568         if args=='top':
569             to_close=0
570         elif args=='bottom':
571             to_close=1
572         else:
573             to_close=1
574         
575         close_plot=self.list_of_events['close_plot']
576         wx.PostEvent(self.frame, close_plot(to_close=to_close))
577         
578     def do_show(self,args=None):
579         '''
580         SHOW
581         Shows both plots.
582         ''' 
583         show_plots=self.list_of_events['show_plots']
584         wx.PostEvent(self.frame, show_plots())
585        
586         
587     
588     #PLOT EXPORT AND MANIPULATION COMMANDS
589     def help_export(self):
590         print '''
591 EXPORT
592 Saves the current plot as an image file
593 ---------------
594 Syntax: export [filename] {plot to export}
595
596 The supported formats are PNG and EPS; the file extension of the filename is automatically recognized
597 and correctly exported. Resolution is (for now) fixed at 150 dpi.
598
599 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)
600         '''
601     def do_export(self,args):
602         #FIXME: the bottom plot doesn't have the title
603         
604         dest=0
605         if args=='':
606             name=raw_input('Filename?')
607         else:
608             args=args.split()
609             name=args[0]
610             if len(args) > 1:
611                 dest=int(args[1]) 
612                 
613         export_image=self.list_of_events['export_image']
614         wx.PostEvent(self.frame, export_image(name=name, dest=dest))
615         
616         
617     def help_txt(self):
618         print '''
619 TXT
620 Saves the current curve as a text file
621 Columns are, in order:
622 X1 , Y1 , X2 , Y2 , X3 , Y3 ...
623
624 -------------
625 Syntax: txt [filename] {plot to export}
626         '''
627     def do_txt(self,args):
628         
629         def transposed2(lists, defval=0):
630             '''
631             transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
632             elements
633             (by Zoran Isailovski on the Python Cookbook online)
634             '''
635             if not lists: return []
636             return map(lambda *row: [elem or defval for elem in row], *lists)
637         
638         whichplot=0
639         args=args.split()
640         if len(args)==0:
641             filename=raw_input('Filename?')
642         else:
643             filename=args[0]
644             try:
645                 whichplot=int(args[1])
646             except:
647                 pass
648             
649         columns=[]     
650         for dataset in self.plots[whichplot].vectors:
651             for i in range(0,len(dataset)): 
652                 columns.append([])
653                 for value in dataset[i]:
654                     columns[-1].append(str(value))                   
655         
656         rows=transposed2(columns, 'nan')
657         rows=[' , '.join(item) for item in rows]
658         text='\n'.join(rows)
659         
660         txtfile=open(filename,'w+')
661         txtfile.write(text)
662         txtfile.close()
663         
664     
665     #LOGGING, REPORTING, NOTETAKING
666     def help_note(self):
667         print '''
668 NOTE
669 Writes or displays a note about the current curve.
670 If [anything] is empty, it displays the note, otherwise it adds a note.
671 The note is then saved in the playlist if you issue a savelist command
672 ---------------
673 Syntax: note [anything]        
674 '''
675     def do_note(self,args):
676         if args=='':
677             print self.current_list[self.pointer].notes
678         else:
679             #bypass UnicodeDecodeError troubles
680             try:
681                 args=args.decode('ascii')
682             except:
683                 args=args.decode('ascii','ignore')
684                 if len(args)==0:
685                     args='?'
686                     
687             self.current_list[self.pointer].notes=args
688         self.notes_saved=0
689             
690     def help_notelog(self):
691         print '''
692 NOTELOG
693 Writes a log of the notes taken during the session for the current
694 playlist
695 --------------        
696 Syntax notelog [filename]
697 '''        
698     def do_notelog(self,args):
699         
700         if len(args)==0:
701             args=raw_input('Notelog filename?')
702             
703         note_lines='Notes taken at '+time.asctime()+'\n'
704         for item in self.current_list:
705             if len(item.notes)>0:
706                 #FIXME: log should be justified
707                 #FIXME: file path should be truncated...
708                 note_string=(item.path+'  |  '+item.notes+'\n')
709                 note_lines+=note_string
710                 
711         try:
712             f=open(args,'a+')
713             f.write(note_lines)
714             f.close()
715         except IOError, (ErrorNumber, ErrorMessage):
716             print 'Error: notes cannot be saved. Catched exception:'
717             print ErrorMessage
718         
719         self.notes_saved=1
720
721     def help_copylog(self):
722         print '''
723 COPYLOG
724 Moves the annotated curves to another directory
725 -----------
726 Syntax copylog [directory]
727         '''
728     def do_copylog(self,args):
729         
730         if len(args)==0:
731             args=raw_input('Destination directory?')
732         
733         mydir=os.path.abspath(args)
734         if not os.path.isdir(mydir):
735             print 'Destination is not a directory.'
736             return
737         
738         for item in self.current_list:
739             if len(item.notes)>0:
740                 try:
741                     shutil.copy(item.path, mydir)
742                 except OSError:
743                     print 'OSError. Cannot copy file. Perhaps you gave me a wrong directory?'
744
745
746 #OS INTERACTION COMMANDS
747 #-----------------    
748     def help_dir(self):
749         print '''
750 DIR, LS
751 Lists the files in the directory
752 ---------
753 Syntax: dir [path]
754           ls  [path]
755         '''
756     def do_dir(self,args):
757         
758         if len(args)==0:
759             args='*'
760         print glob.glob(args)
761         
762     def help_ls(self):
763         self.help_dir(self)
764     def do_ls(self,args):
765         self.do_dir(args)
766         
767     def help_pwd(self):
768         print '''
769 PWD
770 Gives the current working directory.
771 ------------
772 Syntax: pwd
773         '''
774     def do_pwd(self,args):
775         print os.getcwd()         
776     
777     def help_cd(self):
778         print '''
779 CD
780 Changes the current working directory
781 -----
782 Syntax: cd
783         '''
784     def do_cd(self,args):
785         mypath=os.path.abspath(args)
786         try:
787             os.chdir(mypath)
788         except OSError:
789             print 'I cannot access that directory.'
790     
791     
792     def help_system(self):
793         print '''
794 SYSTEM
795 Executes a system command line and reports the output
796 -----
797 Syntax system [command line]
798         '''
799         pass
800     def do_system(self,args):
801         waste=os.system(args)           
802             
803     def do_debug(self,args):
804         '''
805         this is a dummy command where I put debugging things
806         '''
807         print self.config['plotmanips']
808         pass
809             
810     def help_current(self):
811         print '''
812 CURRENT
813 Prints the current curve path.
814 ------
815 Syntax: current
816         '''
817     def do_current(self,args):
818         print self.current.path
819         
820     def do_version(self,args):
821         '''
822         VERSION
823         ------
824         Prints the current version and codename, plus library version. Useful for debugging.
825         '''     
826         print 'Hooke '+__version__+' ('+__codename__+')'
827         print 'Released on: '+__releasedate__
828         print '---'
829         print 'Python version: '+python_version
830         print 'WxPython version: '+wx_version
831         print 'wxMPL version: '+wxmpl_version
832         print 'Matplotlib version: '+mpl_version
833         print 'SciPy version: '+scipy_version
834         print 'NumPy version: '+numpy_version
835         print '---'
836         print 'Platform: '+str(platform.uname())
837         print '---'
838         print 'Loaded plugins:',self.config['loaded_plugins']
839         
840     def help_exit(self):
841         print '''
842 EXIT, QUIT
843 Exits the program cleanly.
844 ------
845 Syntax: exit
846 Syntax: quit
847 '''    
848     def do_exit(self,args):
849         we_exit='N'
850         
851         if (not self.playlist_saved) or (not self.notes_saved):
852             we_exit=raw_input('You did not save your playlist and/or notes. Exit?')
853         else:
854             we_exit=raw_input('Exit?')
855         
856         if we_exit[0].upper()=='Y':
857             wx.CallAfter(self.frame.Close)
858             sys.exit(0)
859         else:
860             return
861     
862     def help_quit(self):
863         self.help_exit()
864     def do_quit(self,args):
865         self.do_exit(args)
866
867
868
869 if __name__ == '__main__':
870     mycli=HookeCli(0)
871     mycli.cmdloop()