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...
207 if value.lower()=='none':
212 self.config[key]=value
215 #PLAYLIST MANAGEMENT AND NAVIGATION
216 #------------------------------------
218 def help_loadlist(self):
221 Loads a file playlist
223 Syntax: loadlist [playlist file]
225 def do_loadlist(self, args):
226 #checking for args: if nothing is given as input, we warn and exit.
228 args=linp.safeinput('File to load?')
231 play_to_load=arglist[0]
233 #We assume a Hooke playlist has the extension .hkp
234 if play_to_load[-4:] != '.hkp':
238 playxml=PlaylistXML()
239 self.current_list, self.playlist_generics=playxml.load(play_to_load)
240 self.current_playxml=playxml
242 print 'File not found.'
245 print 'Loaded %s curves' %len(self.current_list)
247 if 'pointer' in self.playlist_generics.keys():
248 self.pointer=int(self.playlist_generics['pointer'])
250 #if no pointer is found, set the current curve as the first curve of the loaded playlist
252 print 'Starting at curve ',self.pointer
254 self.current=self.current_list[self.pointer]
256 #resets saved/notes saved state
257 self.playlist_saved=0
258 self.playlist_name=''
264 def help_genlist(self):
267 Generates a file playlist.
268 Note it doesn't *save* it: see savelist for this.
270 If [input files] is a directory, it will use all files in the directory for playlist.
276 are all equivalent syntax.
278 Syntax: genlist [input files]
281 def do_genlist(self,args):
282 #args list is: input path, output name
284 args=linp.safeinput('Input files?')
289 #if it's a directory, is like /directory/*.*
290 #FIXME: probably a bit kludgy.
291 if os.path.isdir(list_path):
292 if platform.system == 'Windows':
296 if list_path[-1] == SLASH:
297 list_path=list_path+'*.*'
299 list_path=list_path+SLASH+'*.*'
301 #expanding correctly the input list with the glob module :)
302 list_files=glob.glob(list_path)
306 for item in list_files:
308 self.current_list.append(lhc.HookeCurve(os.path.abspath(item)))
313 if len(self.current_list)>0:
314 self.current=self.current_list[self.pointer]
319 #resets saved/notes saved state
320 self.playlist_saved=0
321 self.playlist_name=''
327 def do_savelist(self,args):
330 Saves the current file playlist on disk.
332 Syntax: savelist [filename]
335 args=linp.safeinput('Output file?',['savedlist.txt'])
339 self.playlist_generics['pointer']=self.pointer
341 #autocomplete filename if not specified
342 if output_filename[-4:] != '.hkp':
343 output_filename+='.hkp'
345 playxml=PlaylistXML()
346 playxml.export(self.current_list, self.playlist_generics)
347 playxml.save(output_filename)
349 #remembers we have saved playlist
350 self.playlist_saved=1
352 def help_addtolist(self):
355 Adds a file to the current playlist
357 Syntax: addtolist [filename]
359 def do_addtolist(self,args):
360 #args list is: input path
362 print 'You must give the input filename you want to add'
363 self.help_addtolist()
366 filenames=glob.glob(args)
368 for filename in filenames:
369 self.current_list.append(lhc.HookeCurve(os.path.abspath(filename)))
370 #we need to save playlist
371 self.playlist_saved=0
373 def help_printlist(self):
376 Prints the list of curves in the current playlist
380 def do_printlist(self,args):
381 for item in self.current_list:
388 Jumps to a given curve.
390 Syntax: jump {$curve}
392 If the curve is not in the current playlist, it politely asks if we want to add it.
394 def do_jump(self,filename):
396 jumps to the curve with the given filename.
397 if the filename is not in the playlist, it asks if we must add it or not.
401 filename=linp.safeinput('Jump to?')
403 filepath=os.path.abspath(filename)
408 while item_not_found:
411 if self.current_list[c].path == filepath:
413 self.current=self.current_list[self.pointer]
419 #We've found the end of the list.
420 answer=linp.safeinput('Curve not found in playlist. Add it to list?',['y'])
421 if answer.lower()[0]=='y':
423 self.do_addtolist(filepath)
425 print 'Curve file not found.'
427 self.current=self.current_list[-1]
428 self.pointer=(len(current_list)-1)
434 def do_index(self,args):
437 Prints the index of the current curve in the list
441 print self.pointer+1, 'of', len(self.current_list)
447 Go the next curve in the playlist.
448 If we are at the last curve, we come back to the first.
452 def do_next(self,args):
454 self.current.curve.close_all()
456 print 'No curve file loaded, currently!'
457 print 'This should not happen, report to http://code.google.com/p/hooke'
460 if self.pointer == (len(self.current_list)-1):
462 print 'Playlist finished; back to first curve.'
466 self.current=self.current_list[self.pointer]
475 def help_previous(self,args):
478 Go to the previous curve in the playlist.
479 If we are at the first curve, we jump to the last.
483 def do_previous(self,args):
485 self.current.curve.close_all()
487 print 'No curve file loaded, currently!'
488 print 'This should not happen, report to http://code.google.com/p/hooke'
490 if self.pointer == 0:
491 self.pointer=(len(self.current_list)-1)
492 print 'Start of playlist; jump to last curve.'
496 self.current=self.current_list[self.pointer]
503 self.do_previous(args)
506 #PLOT INTERACTION COMMANDS
507 #-------------------------------
511 Plots the current force curve
515 def do_plot(self,args):
517 self.current.identify(self.drivers)
518 self.plots=self.current.curve.default_plots()
520 self.plots=self.current.curve.default_plots()
522 print 'Unexpected error occurred in do_plot().'
526 #apply the plotmanip functions eventually present
527 nplots=len(self.plots)
530 for function in self.plotmanip: #FIXME: something strange happens about self.plotmanip[0]
531 self.plots[c]=function(self.plots[c], self.current)
533 self.plots[c].xaxes=self.config['xaxes'] #FIXME: in the future, xaxes and yaxes should be set per-plot
534 self.plots[c].yaxes=self.config['yaxes']
538 self._send_plot(self.plots)
540 def _delta(self, set=1):
542 calculates the difference between two clicked points
544 print 'Click two points'
545 points=self._measure_N_points(N=2, whatset=set)
546 dx=abs(points[0].graph_coords[0]-points[1].graph_coords[0])
547 dy=abs(points[0].graph_coords[1]-points[1].graph_coords[1])
548 unitx=self.plots[points[0].dest].units[0]
549 unity=self.plots[points[0].dest].units[1]
550 return dx,unitx,dy,unity
552 def do_delta(self,args):
556 Measures the delta X and delta Y between two points.
560 dx,unitx,dy,unity=self._delta()
561 print str(dx)+' '+unitx
562 print str(dy)+' '+unity
564 def _point(self, set=1):
565 '''calculates the coordinates of a single clicked point'''
567 print 'Click one point'
568 point=self._measure_N_points(N=1, whatset=set)
570 x=point[0].graph_coords[0]
571 y=point[0].graph_coords[1]
572 unitx=self.plots[point[0].dest].units[0]
573 unity=self.plots[point[0].dest].units[1]
574 return x,unitx,y,unity
576 def do_point(self,args):
580 Returns the coordinates of a point on the graph.
584 x,unitx,y,unity=self._point()
585 print str(x)+' '+unitx
586 print str(y)+' '+unity
587 to_dump='point '+self.current.path+' '+str(x)+' '+unitx+', '+str(y)+' '+unity
588 self.outlet.push(to_dump)
591 def do_close(self,args=None):
594 Closes one of the two plots. If no arguments are given, the bottom plot is closed.
596 Syntax: close [top,bottom]
605 close_plot=self.list_of_events['close_plot']
606 wx.PostEvent(self.frame, close_plot(to_close=to_close))
608 def do_show(self,args=None):
613 show_plots=self.list_of_events['show_plots']
614 wx.PostEvent(self.frame, show_plots())
618 #PLOT EXPORT AND MANIPULATION COMMANDS
619 def help_export(self):
622 Saves the current plot as an image file
624 Syntax: export [filename] {plot to export}
626 The supported formats are PNG and EPS; the file extension of the filename is automatically recognized
627 and correctly exported. Resolution is (for now) fixed at 150 dpi.
629 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)
631 def do_export(self,args):
632 #FIXME: the bottom plot doesn't have the title
636 name=linp.safeinput('Filename?',[self.current.path+'.png'])
643 export_image=self.list_of_events['export_image']
644 wx.PostEvent(self.frame, export_image(name=name, dest=dest))
650 Saves the current curve as a text file
651 Columns are, in order:
652 X1 , Y1 , X2 , Y2 , X3 , Y3 ...
655 Syntax: txt [filename] {plot to export}
657 def do_txt(self,args):
659 def transposed2(lists, defval=0):
661 transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
663 (by Zoran Isailovski on the Python Cookbook online)
665 if not lists: return []
666 return map(lambda *row: [elem or defval for elem in row], *lists)
671 filename=linp.safeinput('Filename?',[self.current.path+'.txt'])
673 filename=linp.checkalphainput(args[0],self.current.path+'.txt',[])
675 whichplot=int(args[1])
680 for dataset in self.plots[whichplot].vectors:
681 for i in range(0,len(dataset)):
683 for value in dataset[i]:
684 columns[-1].append(str(value))
686 rows=transposed2(columns, 'nan')
687 rows=[' , '.join(item) for item in rows]
690 txtfile=open(filename,'w+')
695 #LOGGING, REPORTING, NOTETAKING
698 def do_note_old(self,args):
701 **deprecated**: Use note instead. Will be removed in 0.9
703 Writes or displays a note about the current curve.
704 If [anything] is empty, it displays the note, otherwise it adds a note.
705 The note is then saved in the playlist if you issue a savelist command
707 Syntax: note_old [anything]
711 print self.current_list[self.pointer].notes
713 #bypass UnicodeDecodeError troubles
715 args=args.decode('ascii')
717 args=args.decode('ascii','ignore')
721 self.current_list[self.pointer].notes=args
725 def do_note(self,args):
729 Writes or displays a note about the current curve.
730 If [anything] is empty, it displays the note, otherwise it adds a note.
731 The note is then saved in the playlist if you issue a savelist command.
733 Syntax: note_old [anything]
737 print self.current_list[self.pointer].notes
739 if self.notes_filename == None:
740 self.notes_filename=raw_input('Notebook filename? ')
741 title_line='Notes taken at '+time.asctime()+'\n'
742 f=open(self.notes_filename,'w')
746 #bypass UnicodeDecodeError troubles
748 args=args.decode('ascii')
750 args=args.decode('ascii','ignore')
753 self.current_list[self.pointer].notes=args
755 f=open(self.notes_filename,'a+')
756 note_string=(self.current.path+' | '+self.current.notes+'\n')
760 def help_notelog(self):
763 Writes a log of the notes taken during the session for the current
766 Syntax notelog [filename]
768 def do_notelog(self,args):
771 args=linp.safeinput('Notelog filename?',['notelog.txt'])
773 note_lines='Notes taken at '+time.asctime()+'\n'
774 for item in self.current_list:
775 if len(item.notes)>0:
776 #FIXME: log should be justified
777 #FIXME: file path should be truncated...
778 note_string=(item.path+' | '+item.notes+'\n')
779 note_lines+=note_string
785 except IOError, (ErrorNumber, ErrorMessage):
786 print 'Error: notes cannot be saved. Catched exception:'
791 def help_copylog(self):
794 Moves the annotated curves to another directory
796 Syntax copylog [directory]
798 def do_copylog(self,args):
801 args=linp.safeinput('Destination directory?') #TODO default
803 mydir=os.path.abspath(args)
804 if not os.path.isdir(mydir):
805 print 'Destination is not a directory.'
808 for item in self.current_list:
809 if len(item.notes)>0:
811 shutil.copy(item.path, mydir)
812 except (OSError, IOError):
813 print 'Cannot copy file. '+item.path+' Perhaps you gave me a wrong directory?'
817 def do_outlet_show(self,args):
820 Shows current content of outlet with index for reference
822 self.outlet.printbuf()
824 def do_outlet_undo(self, args):
827 Eliminates last entry in outlet
829 print 'Erasing last entry'
832 def do_outlet_delete(self, args):
834 Eliminates a particular entry from outlet
835 Syntax: outlet_delete n
838 print 'Index needed!, use outlet_show to know it'
840 self.outlet.delete(args)
842 #OS INTERACTION COMMANDS
847 Lists the files in the directory
852 def do_dir(self,args):
856 print glob.glob(args)
860 def do_ls(self,args):
866 Gives the current working directory.
870 def do_pwd(self,args):
876 Changes the current working directory
880 def do_cd(self,args):
881 mypath=os.path.abspath(args)
885 print 'I cannot access that directory.'
888 def help_system(self):
891 Executes a system command line and reports the output
893 Syntax system [command line]
896 def do_system(self,args):
897 waste=os.system(args)
899 def do_debug(self,args):
901 this is a dummy command where I put debugging things
903 print self.config['plotmanips']
906 def help_current(self):
909 Prints the current curve path.
913 def do_current(self,args):
914 print self.current.path
916 def do_info(self,args):
920 Returns informations about the current curve.
922 print 'Path: ',self.current.path
923 print 'Experiment: ',self.current.curve.experiment
924 print 'Filetype: ',self.current.curve.filetype
925 for plot in self.current.curve.default_plots():
926 for set in plot.vectors:
927 lengths=[len(item) for item in set]
928 print 'Data set size: ',lengths
930 def do_version(self,args):
934 Prints the current version and codename, plus library version. Useful for debugging.
936 print 'Hooke '+__version__+' ('+__codename__+')'
937 print 'Released on: '+__releasedate__
939 print 'Python version: '+python_version
940 print 'WxPython version: '+wx_version
941 print 'wxMPL version: '+wxmpl_version
942 print 'Matplotlib version: '+mpl_version
943 print 'SciPy version: '+scipy_version
944 print 'NumPy version: '+numpy_version
946 print 'Platform: '+str(platform.uname())
948 print 'Loaded plugins:',self.config['loaded_plugins']
953 Exits the program cleanly.
958 def do_exit(self,args):
961 if (not self.playlist_saved) or (not self.notes_saved):
962 we_exit=linp.safeinput('You did not save your playlist and/or notes. Exit?',['n'])
964 we_exit=linp.safeinput('Exit?',['y'])
966 if we_exit[0].upper()=='Y':
967 wx.CallAfter(self.frame.Close)
974 def do_quit(self,args):
981 if __name__ == '__main__':