2 # -*- coding: utf-8 -*-
7 Command line module of Hooke.
9 Copyright (C) 2006 Massimo Sandal (University of Bologna, Italy).
11 This program is released under the GNU General Public License version 2.
15 from libhooke import * #FIXME
16 import libhookecurve as lhc
18 import libinput as linp
19 import liboutlet as lout
21 from libhooke import WX_GOOD
22 from libhooke import HOOKE_VERSION
25 wxversion.select(WX_GOOD)
28 from wx.lib.newevent import NewEvent
29 from matplotlib.numerix import * #FIXME
31 import xml.dom.minidom
32 import sys, os, os.path, glob, shutil
39 global __releasedate__
40 __version__ = HOOKE_VERSION[0]
41 __codename__ = HOOKE_VERSION[1]
42 __releasedate__ = HOOKE_VERSION[2]
44 from matplotlib import __version__ as mpl_version
45 from wx import __version__ as wx_version
46 from wxmpl import __version__ as wxmpl_version
47 from scipy import __version__ as scipy_version
48 from numpy import __version__ as numpy_version
49 from sys import version as python_version
53 class HookeCli(cmd.Cmd):
55 def __init__(self,frame,list_of_events,events_from_gui,config,drivers):
56 cmd.Cmd.__init__(self)
58 self.prompt = 'hooke: '
61 self.current_list=[] #the playlist we're using
63 self.current=None #the current curve under analysis.
66 The actual hierarchy of the "current curve" is a bit complex:
68 self.current = the lhc.HookeCurve container object of the current curve
69 self.current.curve = the current "real" curve object as defined in the filetype driver class
70 self.current.curve.default_plots() = the default plots of the filetype driver.
72 The plot objects obtained by mean of self.current.curve.default_plots()
73 then undergoes modifications by the plotmanip
74 modifier functions. The modified plot is saved in self.plots and used if needed by other functions.
78 self.pointer=0 #a pointer to navigate the current list
80 #Things that come from outside
81 self.frame=frame #the wx frame we refer to
82 self.list_of_events=list_of_events #a list of wx events we use to interact with the GUI
83 self.events_from_gui=events_from_gui #the Queue object we use to have messages from the GUI
84 self.config=config #the configuration dictionary
85 self.drivers=drivers #the file format drivers
87 #get plot manipulation functions
88 plotmanip_functions=[]
89 for object_name in dir(self):
90 if object_name[0:9]=='plotmanip':
91 plotmanip_functions.append(getattr(self,object_name))
92 #put plotmanips in order
93 self.plotmanip=[None for item in self.config['plotmanips']]
94 for item in plotmanip_functions:
95 namefunction=item.__name__[10:]
96 if namefunction in self.config['plotmanips']:
97 nameindex=self.config['plotmanips'].index(namefunction) #index of function in plotmanips config
98 self.plotmanip[nameindex] = item
103 self.playlist_saved=0 #Did we save the playlist?
104 self.playlist_name='' #Name of playlist
105 self.notes_saved=1 #Did we save the notes?
106 self.notes_filename=None #Name of notes
109 self.outlet=lout.Outlet()
111 #Data that must be saved in the playlist, related to the whole playlist (not individual curves)
112 self.playlist_generics={}
114 #make sure we execute _plug_init() for every command line plugin we import
115 for plugin_name in self.config['plugins']:
117 plugin=__import__(plugin_name)
119 eval('plugin.'+plugin_name+'Commands._plug_init(self)')
120 except AttributeError:
125 #load default list, if possible
126 self.do_loadlist(self.config['defaultlist'])
129 #Everything sending an event should be here
130 def _measure_N_points(self, N, whatset=1):
132 general helper function for N-points measures
134 wx.PostEvent(self.frame,self.list_of_events['measure_points'](num_of_points=N, set=whatset))
137 points=self.frame.events_from_gui.get()
143 def _get_displayed_plot(self,dest=0):
145 returns the currently displayed plot.
147 wx.PostEvent(self.frame, self.list_of_events['get_displayed_plot'](dest=dest))
150 displayed_plot=self.events_from_gui.get()
155 return displayed_plot
157 def _send_plot(self,plots):
159 sends a plot to the GUI
161 wx.PostEvent(self.frame, self.list_of_events['plot_graph'](plots=plots))
164 def _find_plotmanip(self, name):
166 returns a plot manipulator function from its name
168 return self.plotmanip[self.config['plotmanips'].index(name)]
170 def _clickize(self, xvector, yvector, index):
172 returns a ClickedPoint() object from an index and vectors of x, y coordinates
176 point.absolute_coords=xvector[index],yvector[index]
177 point.find_graph_coords(xvector,yvector)
185 Sets a local configuration variable
187 Syntax: set [variable] [value]
189 def do_set(self,args):
190 #FIXME: some variables in self.config should be hidden or intelligently configurated...
193 print 'You must specify a variable and a value'
194 print 'Available variables:'
195 print self.config.keys()
197 if args[0] not in self.config.keys():
198 print 'This is not an internal Hooke variable!'
201 #FIXME:we should reload the config file and reset the config value
202 print self.config[args[0]]
205 try: #try to have a numeric value
207 except ValueError: #if it cannot be converted to float, it's None, or a string...
209 if value.lower()=='none':
214 self.config[key]=value
217 #PLAYLIST MANAGEMENT AND NAVIGATION
218 #------------------------------------
220 def help_loadlist(self):
223 Loads a file playlist
225 Syntax: loadlist [playlist file]
227 def do_loadlist(self, args):
228 #checking for args: if nothing is given as input, we warn and exit.
230 args=linp.safeinput('File to load?')
233 play_to_load=arglist[0]
235 #We assume a Hooke playlist has the extension .hkp
236 if play_to_load[-4:] != '.hkp':
240 playxml=PlaylistXML()
241 self.current_list, self.playlist_generics=playxml.load(play_to_load)
242 self.current_playxml=playxml
244 print 'File not found.'
247 print 'Loaded %s curves' %len(self.current_list)
249 if 'pointer' in self.playlist_generics.keys():
250 self.pointer=int(self.playlist_generics['pointer'])
252 #if no pointer is found, set the current curve as the first curve of the loaded playlist
254 print 'Starting at curve ',self.pointer
256 self.current=self.current_list[self.pointer]
258 #resets saved/notes saved state
259 self.playlist_saved=0
260 self.playlist_name=''
266 def help_genlist(self):
269 Generates a file playlist.
270 Note it doesn't *save* it: see savelist for this.
272 If [input files] is a directory, it will use all files in the directory for playlist.
278 are all equivalent syntax.
280 Syntax: genlist [input files]
283 def do_genlist(self,args):
284 #args list is: input path, output name
286 args=linp.safeinput('Input files?')
291 #if it's a directory, is like /directory/*.*
292 #FIXME: probably a bit kludgy.
293 if os.path.isdir(list_path):
294 if platform.system == 'Windows':
298 if list_path[-1] == SLASH:
299 list_path=list_path+'*'
301 list_path=list_path+SLASH+'*'
303 #expanding correctly the input list with the glob module :)
304 list_files=glob.glob(list_path)
308 for item in list_files:
310 if os.path.isfile(item):
311 self.current_list.append(lhc.HookeCurve(os.path.abspath(item)))
316 if len(self.current_list)>0:
317 self.current=self.current_list[self.pointer]
322 #resets saved/notes saved state
323 self.playlist_saved=0
324 self.playlist_name=''
330 def do_savelist(self,args):
333 Saves the current file playlist on disk.
335 Syntax: savelist [filename]
338 args=linp.safeinput('Output file?',['savedlist.txt'])
342 self.playlist_generics['pointer']=self.pointer
344 #autocomplete filename if not specified
345 if output_filename[-4:] != '.hkp':
346 output_filename+='.hkp'
348 playxml=PlaylistXML()
349 playxml.export(self.current_list, self.playlist_generics)
350 playxml.save(output_filename)
352 #remembers we have saved playlist
353 self.playlist_saved=1
355 def help_addtolist(self):
358 Adds a file to the current playlist
360 Syntax: addtolist [filename]
362 def do_addtolist(self,args):
363 #args list is: input path
365 print 'You must give the input filename you want to add'
366 self.help_addtolist()
369 filenames=glob.glob(args)
371 for filename in filenames:
372 self.current_list.append(lhc.HookeCurve(os.path.abspath(filename)))
373 #we need to save playlist
374 self.playlist_saved=0
376 def help_printlist(self):
379 Prints the list of curves in the current playlist
383 def do_printlist(self,args):
384 for item in self.current_list:
391 Jumps to a given curve.
393 Syntax: jump {$curve}
395 If the curve is not in the current playlist, it politely asks if we want to add it.
397 def do_jump(self,filename):
399 jumps to the curve with the given filename.
400 if the filename is not in the playlist, it asks if we must add it or not.
404 filename=linp.safeinput('Jump to?')
406 filepath=os.path.abspath(filename)
411 while item_not_found:
414 if self.current_list[c].path == filepath:
416 self.current=self.current_list[self.pointer]
422 #We've found the end of the list.
423 answer=linp.safeinput('Curve not found in playlist. Add it to list?',['y'])
424 if answer.lower()[0]=='y':
426 self.do_addtolist(filepath)
428 print 'Curve file not found.'
430 self.current=self.current_list[-1]
431 self.pointer=(len(current_list)-1)
437 def do_index(self,args):
440 Prints the index of the current curve in the list
444 print self.pointer+1, 'of', len(self.current_list)
450 Go the next curve in the playlist.
451 If we are at the last curve, we come back to the first.
455 def do_next(self,args):
457 self.current.curve.close_all()
459 print 'No curve file loaded, currently!'
460 print 'This should not happen, report to http://code.google.com/p/hooke'
463 if self.pointer == (len(self.current_list)-1):
465 print 'Playlist finished; back to first curve.'
469 self.current=self.current_list[self.pointer]
478 def help_previous(self,args):
481 Go to the previous curve in the playlist.
482 If we are at the first curve, we jump to the last.
486 def do_previous(self,args):
488 self.current.curve.close_all()
490 print 'No curve file loaded, currently!'
491 print 'This should not happen, report to http://code.google.com/p/hooke'
493 if self.pointer == 0:
494 self.pointer=(len(self.current_list)-1)
495 print 'Start of playlist; jump to last curve.'
499 self.current=self.current_list[self.pointer]
506 self.do_previous(args)
509 #PLOT INTERACTION COMMANDS
510 #-------------------------------
514 Plots the current force curve
518 def do_plot(self,args):
520 self.current.identify(self.drivers)
521 self.plots=self.current.curve.default_plots()
523 self.plots=self.current.curve.default_plots()
525 print 'Unexpected error occurred in do_plot().'
529 #apply the plotmanip functions eventually present
530 nplots=len(self.plots)
533 for function in self.plotmanip: #FIXME: something strange happens about self.plotmanip[0]
534 self.plots[c]=function(self.plots[c], self.current)
536 self.plots[c].xaxes=self.config['xaxes'] #FIXME: in the future, xaxes and yaxes should be set per-plot
537 self.plots[c].yaxes=self.config['yaxes']
541 self._send_plot(self.plots)
543 def _delta(self, set=1):
545 calculates the difference between two clicked points
547 print 'Click two points'
548 points=self._measure_N_points(N=2, whatset=set)
549 dx=abs(points[0].graph_coords[0]-points[1].graph_coords[0])
550 dy=abs(points[0].graph_coords[1]-points[1].graph_coords[1])
551 unitx=self.plots[points[0].dest].units[0]
552 unity=self.plots[points[0].dest].units[1]
553 return dx,unitx,dy,unity
555 def do_delta(self,args):
559 Measures the delta X and delta Y between two points.
563 dx,unitx,dy,unity=self._delta()
564 print str(dx)+' '+unitx
565 print str(dy)+' '+unity
567 def _point(self, set=1):
568 '''calculates the coordinates of a single clicked point'''
570 print 'Click one point'
571 point=self._measure_N_points(N=1, whatset=set)
573 x=point[0].graph_coords[0]
574 y=point[0].graph_coords[1]
575 unitx=self.plots[point[0].dest].units[0]
576 unity=self.plots[point[0].dest].units[1]
577 return x,unitx,y,unity
579 def do_point(self,args):
583 Returns the coordinates of a point on the graph.
587 x,unitx,y,unity=self._point()
588 print str(x)+' '+unitx
589 print str(y)+' '+unity
590 to_dump='point '+self.current.path+' '+str(x)+' '+unitx+', '+str(y)+' '+unity
591 self.outlet.push(to_dump)
594 def do_close(self,args=None):
597 Closes one of the two plots. If no arguments are given, the bottom plot is closed.
599 Syntax: close [top,bottom]
608 close_plot=self.list_of_events['close_plot']
609 wx.PostEvent(self.frame, close_plot(to_close=to_close))
611 def do_show(self,args=None):
616 show_plots=self.list_of_events['show_plots']
617 wx.PostEvent(self.frame, show_plots())
621 #PLOT EXPORT AND MANIPULATION COMMANDS
622 def help_export(self):
625 Saves the current plot as an image file
627 Syntax: export [filename] {plot to export}
629 The supported formats are PNG and EPS; the file extension of the filename is automatically recognized
630 and correctly exported. Resolution is (for now) fixed at 150 dpi.
632 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)
634 def do_export(self,args):
635 #FIXME: the bottom plot doesn't have the title
640 #FIXME: We have to go into the libinput stuff and fix it, for now here's a dummy replacement...
641 #name=linp.safeinput('Filename?',[self.current.path+'.png'])
642 name=raw_input('Filename? ')
649 export_image=self.list_of_events['export_image']
650 wx.PostEvent(self.frame, export_image(name=name, dest=dest))
656 Saves the current curve as a text file
657 Columns are, in order:
658 X1 , Y1 , X2 , Y2 , X3 , Y3 ...
661 Syntax: txt [filename] {plot to export}
663 def do_txt(self,args):
665 def transposed2(lists, defval=0):
667 transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
669 (by Zoran Isailovski on the Python Cookbook online)
671 if not lists: return []
672 return map(lambda *row: [elem or defval for elem in row], *lists)
677 filename=linp.safeinput('Filename?',[self.current.path+'.txt'])
679 filename=linp.checkalphainput(args[0],self.current.path+'.txt',[])
681 whichplot=int(args[1])
686 outofplot=self.plots[whichplot].vectors
688 print "Plot index out of range."
692 for dataset in self.plots[whichplot].vectors:
693 for i in range(0,len(dataset)):
695 for value in dataset[i]:
696 columns[-1].append(str(value))
698 rows=transposed2(columns, 'nan')
699 rows=[' , '.join(item) for item in rows]
702 txtfile=open(filename,'w+')
703 #Save units of measure in header
704 txtfile.write('X:'+self.plots[whichplot].units[0]+'\n')
705 txtfile.write('Y:'+self.plots[whichplot].units[1]+'\n')
710 #LOGGING, REPORTING, NOTETAKING
713 def do_note_old(self,args):
716 **deprecated**: Use note instead. Will be removed in 0.9
718 Writes or displays a note about the current curve.
719 If [anything] is empty, it displays the note, otherwise it adds a note.
720 The note is then saved in the playlist if you issue a savelist command
722 Syntax: note_old [anything]
726 print self.current_list[self.pointer].notes
728 #bypass UnicodeDecodeError troubles
730 args=args.decode('ascii')
732 args=args.decode('ascii','ignore')
736 self.current_list[self.pointer].notes=args
740 def do_note(self,args):
744 Writes or displays a note about the current curve.
745 If [anything] is empty, it displays the note, otherwise it adds a note.
746 The note is then saved in the playlist if you issue a savelist command.
748 Syntax: note_old [anything]
752 print self.current_list[self.pointer].notes
754 if self.notes_filename == None:
755 if not os.path.exists(os.path.realpath('output')):
757 self.notes_filename=raw_input('Notebook filename? ')
758 self.notes_filename=os.path.join(os.path.realpath('output'),self.notes_filename)
759 title_line='Notes taken at '+time.asctime()+'\n'
760 f=open(self.notes_filename,'a')
764 #bypass UnicodeDecodeError troubles
766 args=args.decode('ascii')
768 args=args.decode('ascii','ignore')
771 self.current_list[self.pointer].notes=args
773 f=open(self.notes_filename,'a+')
774 note_string=(self.current.path+' | '+self.current.notes+'\n')
778 def help_notelog(self):
781 Writes a log of the notes taken during the session for the current
784 Syntax notelog [filename]
786 def do_notelog(self,args):
789 args=linp.safeinput('Notelog filename?',['notelog.txt'])
791 note_lines='Notes taken at '+time.asctime()+'\n'
792 for item in self.current_list:
793 if len(item.notes)>0:
794 #FIXME: log should be justified
795 #FIXME: file path should be truncated...
796 note_string=(item.path+' | '+item.notes+'\n')
797 note_lines+=note_string
803 except IOError, (ErrorNumber, ErrorMessage):
804 print 'Error: notes cannot be saved. Catched exception:'
809 def help_copylog(self):
812 Moves the annotated curves to another directory
814 Syntax copylog [directory]
816 def do_copylog(self,args):
819 args=linp.safeinput('Destination directory?') #TODO default
821 mydir=os.path.abspath(args)
822 if not os.path.isdir(mydir):
823 print 'Destination is not a directory.'
826 for item in self.current_list:
827 if len(item.notes)>0:
829 shutil.copy(item.path, mydir)
830 except (OSError, IOError):
831 print 'Cannot copy file. '+item.path+' Perhaps you gave me a wrong directory?'
835 def do_outlet_show(self,args):
838 Shows current content of outlet with index for reference
840 self.outlet.printbuf()
842 def do_outlet_undo(self, args):
845 Eliminates last entry in outlet
847 print 'Erasing last entry'
850 def do_outlet_delete(self, args):
852 Eliminates a particular entry from outlet
853 Syntax: outlet_delete n
856 print 'Index needed!, use outlet_show to know it'
858 self.outlet.delete(args)
860 #OS INTERACTION COMMANDS
865 Lists the files in the directory
870 def do_dir(self,args):
874 print glob.glob(args)
878 def do_ls(self,args):
884 Gives the current working directory.
888 def do_pwd(self,args):
894 Changes the current working directory
898 def do_cd(self,args):
899 mypath=os.path.abspath(args)
903 print 'I cannot access that directory.'
906 def help_system(self):
909 Executes a system command line and reports the output
911 Syntax system [command line]
914 def do_system(self,args):
915 waste=os.system(args)
917 def do_debug(self,args):
919 this is a dummy command where I put debugging things
921 print self.config['plotmanips']
924 def help_current(self):
927 Prints the current curve path.
931 def do_current(self,args):
932 print self.current.path
934 def do_info(self,args):
938 Returns informations about the current curve.
940 print 'Path: ',self.current.path
941 print 'Experiment: ',self.current.curve.experiment
942 print 'Filetype: ',self.current.curve.filetype
943 for plot in self.current.curve.default_plots():
944 for set in plot.vectors:
945 lengths=[len(item) for item in set]
946 print 'Data set size: ',lengths
948 def do_version(self,args):
952 Prints the current version and codename, plus library version. Useful for debugging.
954 print 'Hooke '+__version__+' ('+__codename__+')'
955 print 'Released on: '+__releasedate__
957 print 'Python version: '+python_version
958 print 'WxPython version: '+wx_version
959 print 'wxMPL version: '+wxmpl_version
960 print 'Matplotlib version: '+mpl_version
961 print 'SciPy version: '+scipy_version
962 print 'NumPy version: '+numpy_version
964 print 'Platform: '+str(platform.uname())
966 print 'Loaded plugins:',self.config['loaded_plugins']
971 Exits the program cleanly.
976 def do_exit(self,args):
979 if (not self.playlist_saved) or (not self.notes_saved):
980 we_exit=linp.safeinput('You did not save your playlist and/or notes. Exit?',['n'])
982 we_exit=linp.safeinput('Exit?',['y'])
984 if we_exit[0].upper()=='Y':
985 wx.CallAfter(self.frame.Close)
992 def do_quit(self,args):
999 if __name__ == '__main__':