6 Command line module of Hooke.
8 Copyright (C) 2006 Massimo Sandal (University of Bologna, Italy).
10 This program is released under the GNU General Public License version 2.
14 from libhooke import * #FIXME
15 import libhookecurve as lhc
17 import libinput as linp
18 import liboutlet as lout
20 from libhooke import WX_GOOD
21 from libhooke import HOOKE_VERSION
24 wxversion.select(WX_GOOD)
27 from wx.lib.newevent import NewEvent
28 from matplotlib.numerix import * #FIXME
30 import xml.dom.minidom
31 import sys, os, os.path, glob, shutil
38 global __releasedate__
39 __version__ = HOOKE_VERSION[0]
40 __codename__ = HOOKE_VERSION[1]
41 __releasedate__ = HOOKE_VERSION[2]
43 from matplotlib import __version__ as mpl_version
44 from wx import __version__ as wx_version
45 from wxmpl import __version__ as wxmpl_version
46 from scipy import __version__ as scipy_version
47 from numpy import __version__ as numpy_version
48 from sys import version as python_version
52 class HookeCli(cmd.Cmd):
54 def __init__(self,frame,list_of_events,events_from_gui,config,drivers):
55 cmd.Cmd.__init__(self)
57 self.prompt = 'hooke: '
60 self.current_list=[] #the playlist we're using
62 self.current=None #the current curve under analysis.
65 The actual hierarchy of the "current curve" is a bit complex:
67 self.current = the lhc.HookeCurve container object of the current curve
68 self.current.curve = the current "real" curve object as defined in the filetype driver class
69 self.current.curve.default_plots() = the default plots of the filetype driver.
71 The plot objects obtained by mean of self.current.curve.default_plots()
72 then undergoes modifications by the plotmanip
73 modifier functions. The modified plot is saved in self.plots and used if needed by other functions.
77 self.pointer=0 #a pointer to navigate the current list
79 #Things that come from outside
80 self.frame=frame #the wx frame we refer to
81 self.list_of_events=list_of_events #a list of wx events we use to interact with the GUI
82 self.events_from_gui=events_from_gui #the Queue object we use to have messages from the GUI
83 self.config=config #the configuration dictionary
84 self.drivers=drivers #the file format drivers
86 #get plot manipulation functions
87 plotmanip_functions=[]
88 for object_name in dir(self):
89 if object_name[0:9]=='plotmanip':
90 plotmanip_functions.append(getattr(self,object_name))
91 #put plotmanips in order
92 self.plotmanip=[None for item in self.config['plotmanips']]
93 for item in plotmanip_functions:
94 namefunction=item.__name__[10:]
95 if namefunction in self.config['plotmanips']:
96 nameindex=self.config['plotmanips'].index(namefunction) #index of function in plotmanips config
97 self.plotmanip[nameindex] = item
102 self.playlist_saved=0 #Did we save the playlist?
103 self.playlist_name='' #Name of playlist
104 self.notes_saved=1 #Did we save the notes?
105 self.notes_filename=None #Name of notes
108 self.outlet=lout.Outlet()
110 #Data that must be saved in the playlist, related to the whole playlist (not individual curves)
111 self.playlist_generics={}
113 #make sure we execute _plug_init() for every command line plugin we import
114 for plugin_name in self.config['plugins']:
116 plugin=__import__(plugin_name)
118 eval('plugin.'+plugin_name+'Commands._plug_init(self)')
119 except AttributeError:
124 #load default list, if possible
125 self.do_loadlist(self.config['defaultlist'])
128 #Everything sending an event should be here
129 def _measure_N_points(self, N, whatset=1):
131 general helper function for N-points measures
133 wx.PostEvent(self.frame,self.list_of_events['measure_points'](num_of_points=N, set=whatset))
136 points=self.frame.events_from_gui.get()
142 def _get_displayed_plot(self,dest=0):
144 returns the currently displayed plot.
146 wx.PostEvent(self.frame, self.list_of_events['get_displayed_plot'](dest=dest))
149 displayed_plot=self.events_from_gui.get()
154 return displayed_plot
156 def _send_plot(self,plots):
158 sends a plot to the GUI
160 wx.PostEvent(self.frame, self.list_of_events['plot_graph'](plots=plots))
163 def _find_plotmanip(self, name):
165 returns a plot manipulator function from its name
167 return self.plotmanip[self.config['plotmanips'].index(name)]
169 def _clickize(self, xvector, yvector, index):
171 returns a ClickedPoint() object from an index and vectors of x, y coordinates
175 point.absolute_coords=xvector[index],yvector[index]
176 point.find_graph_coords(xvector,yvector)
184 Sets a local configuration variable
186 Syntax: set [variable] [value]
188 def do_set(self,args):
189 #FIXME: some variables in self.config should be hidden or intelligently configurated...
192 print 'You must specify a variable and a value'
193 print 'Available variables:'
194 print self.config.keys()
196 if args[0] not in self.config.keys():
197 print 'This is not an internal Hooke variable!'
200 #FIXME:we should reload the config file and reset the config value
201 print self.config[args[0]]
204 try: #try to have a numeric value
206 except ValueError: #if it cannot be converted to float, it's None, or a string...
208 if value.lower()=='none':
213 self.config[key]=value
216 #PLAYLIST MANAGEMENT AND NAVIGATION
217 #------------------------------------
219 def help_loadlist(self):
222 Loads a file playlist
224 Syntax: loadlist [playlist file]
226 def do_loadlist(self, args):
227 #checking for args: if nothing is given as input, we warn and exit.
229 args=linp.safeinput('File to load?')
232 play_to_load=arglist[0]
234 #We assume a Hooke playlist has the extension .hkp
235 if play_to_load[-4:] != '.hkp':
239 playxml=PlaylistXML()
240 self.current_list, self.playlist_generics=playxml.load(play_to_load)
241 self.current_playxml=playxml
243 print 'File not found.'
246 print 'Loaded %s curves' %len(self.current_list)
248 if 'pointer' in self.playlist_generics.keys():
249 self.pointer=int(self.playlist_generics['pointer'])
251 #if no pointer is found, set the current curve as the first curve of the loaded playlist
253 print 'Starting at curve ',self.pointer
255 self.current=self.current_list[self.pointer]
257 #resets saved/notes saved state
258 self.playlist_saved=0
259 self.playlist_name=''
265 def help_genlist(self):
268 Generates a file playlist.
269 Note it doesn't *save* it: see savelist for this.
271 If [input files] is a directory, it will use all files in the directory for playlist.
277 are all equivalent syntax.
279 Syntax: genlist [input files]
282 def do_genlist(self,args):
283 #args list is: input path, output name
285 args=linp.safeinput('Input files?')
290 #if it's a directory, is like /directory/*.*
291 #FIXME: probably a bit kludgy.
292 if os.path.isdir(list_path):
293 if platform.system == 'Windows':
297 if list_path[-1] == SLASH:
298 list_path=list_path+'*.*'
300 list_path=list_path+SLASH+'*.*'
302 #expanding correctly the input list with the glob module :)
303 list_files=glob.glob(list_path)
307 for item in list_files:
309 if os.path.isfile(item):
310 self.current_list.append(lhc.HookeCurve(os.path.abspath(item)))
315 if len(self.current_list)>0:
316 self.current=self.current_list[self.pointer]
321 #resets saved/notes saved state
322 self.playlist_saved=0
323 self.playlist_name=''
329 def do_savelist(self,args):
332 Saves the current file playlist on disk.
334 Syntax: savelist [filename]
337 args=linp.safeinput('Output file?',['savedlist.txt'])
341 self.playlist_generics['pointer']=self.pointer
343 #autocomplete filename if not specified
344 if output_filename[-4:] != '.hkp':
345 output_filename+='.hkp'
347 playxml=PlaylistXML()
348 playxml.export(self.current_list, self.playlist_generics)
349 playxml.save(output_filename)
351 #remembers we have saved playlist
352 self.playlist_saved=1
354 def help_addtolist(self):
357 Adds a file to the current playlist
359 Syntax: addtolist [filename]
361 def do_addtolist(self,args):
362 #args list is: input path
364 print 'You must give the input filename you want to add'
365 self.help_addtolist()
368 filenames=glob.glob(args)
370 for filename in filenames:
371 self.current_list.append(lhc.HookeCurve(os.path.abspath(filename)))
372 #we need to save playlist
373 self.playlist_saved=0
375 def help_printlist(self):
378 Prints the list of curves in the current playlist
382 def do_printlist(self,args):
383 for item in self.current_list:
390 Jumps to a given curve.
392 Syntax: jump {$curve}
394 If the curve is not in the current playlist, it politely asks if we want to add it.
396 def do_jump(self,filename):
398 jumps to the curve with the given filename.
399 if the filename is not in the playlist, it asks if we must add it or not.
403 filename=linp.safeinput('Jump to?')
405 filepath=os.path.abspath(filename)
410 while item_not_found:
413 if self.current_list[c].path == filepath:
415 self.current=self.current_list[self.pointer]
421 #We've found the end of the list.
422 answer=linp.safeinput('Curve not found in playlist. Add it to list?',['y'])
423 if answer.lower()[0]=='y':
425 self.do_addtolist(filepath)
427 print 'Curve file not found.'
429 self.current=self.current_list[-1]
430 self.pointer=(len(current_list)-1)
436 def do_index(self,args):
439 Prints the index of the current curve in the list
443 print self.pointer+1, 'of', len(self.current_list)
449 Go the next curve in the playlist.
450 If we are at the last curve, we come back to the first.
454 def do_next(self,args):
456 self.current.curve.close_all()
458 print 'No curve file loaded, currently!'
459 print 'This should not happen, report to http://code.google.com/p/hooke'
462 if self.pointer == (len(self.current_list)-1):
464 print 'Playlist finished; back to first curve.'
468 self.current=self.current_list[self.pointer]
477 def help_previous(self,args):
480 Go to the previous curve in the playlist.
481 If we are at the first curve, we jump to the last.
485 def do_previous(self,args):
487 self.current.curve.close_all()
489 print 'No curve file loaded, currently!'
490 print 'This should not happen, report to http://code.google.com/p/hooke'
492 if self.pointer == 0:
493 self.pointer=(len(self.current_list)-1)
494 print 'Start of playlist; jump to last curve.'
498 self.current=self.current_list[self.pointer]
505 self.do_previous(args)
508 #PLOT INTERACTION COMMANDS
509 #-------------------------------
513 Plots the current force curve
517 def do_plot(self,args):
519 self.current.identify(self.drivers)
520 self.plots=self.current.curve.default_plots()
522 self.plots=self.current.curve.default_plots()
524 print 'Unexpected error occurred in do_plot().'
528 #apply the plotmanip functions eventually present
529 nplots=len(self.plots)
532 for function in self.plotmanip: #FIXME: something strange happens about self.plotmanip[0]
533 self.plots[c]=function(self.plots[c], self.current)
535 self.plots[c].xaxes=self.config['xaxes'] #FIXME: in the future, xaxes and yaxes should be set per-plot
536 self.plots[c].yaxes=self.config['yaxes']
540 self._send_plot(self.plots)
542 def _delta(self, set=1):
544 calculates the difference between two clicked points
546 print 'Click two points'
547 points=self._measure_N_points(N=2, whatset=set)
548 dx=abs(points[0].graph_coords[0]-points[1].graph_coords[0])
549 dy=abs(points[0].graph_coords[1]-points[1].graph_coords[1])
550 unitx=self.plots[points[0].dest].units[0]
551 unity=self.plots[points[0].dest].units[1]
552 return dx,unitx,dy,unity
554 def do_delta(self,args):
558 Measures the delta X and delta Y between two points.
562 dx,unitx,dy,unity=self._delta()
563 print str(dx)+' '+unitx
564 print str(dy)+' '+unity
566 def _point(self, set=1):
567 '''calculates the coordinates of a single clicked point'''
569 print 'Click one point'
570 point=self._measure_N_points(N=1, whatset=set)
572 x=point[0].graph_coords[0]
573 y=point[0].graph_coords[1]
574 unitx=self.plots[point[0].dest].units[0]
575 unity=self.plots[point[0].dest].units[1]
576 return x,unitx,y,unity
578 def do_point(self,args):
582 Returns the coordinates of a point on the graph.
586 x,unitx,y,unity=self._point()
587 print str(x)+' '+unitx
588 print str(y)+' '+unity
589 to_dump='point '+self.current.path+' '+str(x)+' '+unitx+', '+str(y)+' '+unity
590 self.outlet.push(to_dump)
593 def do_close(self,args=None):
596 Closes one of the two plots. If no arguments are given, the bottom plot is closed.
598 Syntax: close [top,bottom]
607 close_plot=self.list_of_events['close_plot']
608 wx.PostEvent(self.frame, close_plot(to_close=to_close))
610 def do_show(self,args=None):
615 show_plots=self.list_of_events['show_plots']
616 wx.PostEvent(self.frame, show_plots())
620 #PLOT EXPORT AND MANIPULATION COMMANDS
621 def help_export(self):
624 Saves the current plot as an image file
626 Syntax: export [filename] {plot to export}
628 The supported formats are PNG and EPS; the file extension of the filename is automatically recognized
629 and correctly exported. Resolution is (for now) fixed at 150 dpi.
631 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)
633 def do_export(self,args):
634 #FIXME: the bottom plot doesn't have the title
639 #FIXME: We have to go into the libinput stuff and fix it, for now here's a dummy replacement...
640 #name=linp.safeinput('Filename?',[self.current.path+'.png'])
641 name=raw_input('Filename? ')
648 export_image=self.list_of_events['export_image']
649 wx.PostEvent(self.frame, export_image(name=name, dest=dest))
655 Saves the current curve as a text file
656 Columns are, in order:
657 X1 , Y1 , X2 , Y2 , X3 , Y3 ...
660 Syntax: txt [filename] {plot to export}
662 def do_txt(self,args):
664 def transposed2(lists, defval=0):
666 transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
668 (by Zoran Isailovski on the Python Cookbook online)
670 if not lists: return []
671 return map(lambda *row: [elem or defval for elem in row], *lists)
676 filename=linp.safeinput('Filename?',[self.current.path+'.txt'])
678 filename=linp.checkalphainput(args[0],self.current.path+'.txt',[])
680 whichplot=int(args[1])
685 for dataset in self.plots[whichplot].vectors:
686 for i in range(0,len(dataset)):
688 for value in dataset[i]:
689 columns[-1].append(str(value))
691 rows=transposed2(columns, 'nan')
692 rows=[' , '.join(item) for item in rows]
695 txtfile=open(filename,'w+')
696 #Save units of measure in header
697 txtfile.write('X:'+self.plots[whichplot].units[0]+'\n')
698 txtfile.write('Y:'+self.plots[whichplot].units[1]+'\n')
703 #LOGGING, REPORTING, NOTETAKING
706 def do_note_old(self,args):
709 **deprecated**: Use note instead. Will be removed in 0.9
711 Writes or displays a note about the current curve.
712 If [anything] is empty, it displays the note, otherwise it adds a note.
713 The note is then saved in the playlist if you issue a savelist command
715 Syntax: note_old [anything]
719 print self.current_list[self.pointer].notes
721 #bypass UnicodeDecodeError troubles
723 args=args.decode('ascii')
725 args=args.decode('ascii','ignore')
729 self.current_list[self.pointer].notes=args
733 def do_note(self,args):
737 Writes or displays a note about the current curve.
738 If [anything] is empty, it displays the note, otherwise it adds a note.
739 The note is then saved in the playlist if you issue a savelist command.
741 Syntax: note_old [anything]
745 print self.current_list[self.pointer].notes
747 if self.notes_filename == None:
748 self.notes_filename=raw_input('Notebook filename? ')
749 title_line='Notes taken at '+time.asctime()+'\n'
750 f=open(self.notes_filename,'w')
754 #bypass UnicodeDecodeError troubles
756 args=args.decode('ascii')
758 args=args.decode('ascii','ignore')
761 self.current_list[self.pointer].notes=args
763 f=open(self.notes_filename,'a+')
764 note_string=(self.current.path+' | '+self.current.notes+'\n')
768 def help_notelog(self):
771 Writes a log of the notes taken during the session for the current
774 Syntax notelog [filename]
776 def do_notelog(self,args):
779 args=linp.safeinput('Notelog filename?',['notelog.txt'])
781 note_lines='Notes taken at '+time.asctime()+'\n'
782 for item in self.current_list:
783 if len(item.notes)>0:
784 #FIXME: log should be justified
785 #FIXME: file path should be truncated...
786 note_string=(item.path+' | '+item.notes+'\n')
787 note_lines+=note_string
793 except IOError, (ErrorNumber, ErrorMessage):
794 print 'Error: notes cannot be saved. Catched exception:'
799 def help_copylog(self):
802 Moves the annotated curves to another directory
804 Syntax copylog [directory]
806 def do_copylog(self,args):
809 args=linp.safeinput('Destination directory?') #TODO default
811 mydir=os.path.abspath(args)
812 if not os.path.isdir(mydir):
813 print 'Destination is not a directory.'
816 for item in self.current_list:
817 if len(item.notes)>0:
819 shutil.copy(item.path, mydir)
820 except (OSError, IOError):
821 print 'Cannot copy file. '+item.path+' Perhaps you gave me a wrong directory?'
825 def do_outlet_show(self,args):
828 Shows current content of outlet with index for reference
830 self.outlet.printbuf()
832 def do_outlet_undo(self, args):
835 Eliminates last entry in outlet
837 print 'Erasing last entry'
840 def do_outlet_delete(self, args):
842 Eliminates a particular entry from outlet
843 Syntax: outlet_delete n
846 print 'Index needed!, use outlet_show to know it'
848 self.outlet.delete(args)
850 #OS INTERACTION COMMANDS
855 Lists the files in the directory
860 def do_dir(self,args):
864 print glob.glob(args)
868 def do_ls(self,args):
874 Gives the current working directory.
878 def do_pwd(self,args):
884 Changes the current working directory
888 def do_cd(self,args):
889 mypath=os.path.abspath(args)
893 print 'I cannot access that directory.'
896 def help_system(self):
899 Executes a system command line and reports the output
901 Syntax system [command line]
904 def do_system(self,args):
905 waste=os.system(args)
907 def do_debug(self,args):
909 this is a dummy command where I put debugging things
911 print self.config['plotmanips']
914 def help_current(self):
917 Prints the current curve path.
921 def do_current(self,args):
922 print self.current.path
924 def do_info(self,args):
928 Returns informations about the current curve.
930 print 'Path: ',self.current.path
931 print 'Experiment: ',self.current.curve.experiment
932 print 'Filetype: ',self.current.curve.filetype
933 for plot in self.current.curve.default_plots():
934 for set in plot.vectors:
935 lengths=[len(item) for item in set]
936 print 'Data set size: ',lengths
938 def do_version(self,args):
942 Prints the current version and codename, plus library version. Useful for debugging.
944 print 'Hooke '+__version__+' ('+__codename__+')'
945 print 'Released on: '+__releasedate__
947 print 'Python version: '+python_version
948 print 'WxPython version: '+wx_version
949 print 'wxMPL version: '+wxmpl_version
950 print 'Matplotlib version: '+mpl_version
951 print 'SciPy version: '+scipy_version
952 print 'NumPy version: '+numpy_version
954 print 'Platform: '+str(platform.uname())
956 print 'Loaded plugins:',self.config['loaded_plugins']
961 Exits the program cleanly.
966 def do_exit(self,args):
969 if (not self.playlist_saved) or (not self.notes_saved):
970 we_exit=linp.safeinput('You did not save your playlist and/or notes. Exit?',['n'])
972 we_exit=linp.safeinput('Exit?',['y'])
974 if we_exit[0].upper()=='Y':
975 wx.CallAfter(self.frame.Close)
982 def do_quit(self,args):
989 if __name__ == '__main__':