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} or
663 all : To save all the curves in different windows in a single file.
665 def do_txt(self,args):
667 def transposed2(lists, defval=0):
669 transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
671 (by Zoran Isailovski on the Python Cookbook online)
673 if not lists: return []
674 return map(lambda *row: [elem or defval for elem in row], *lists)
679 filename=linp.safeinput('Filename?',[self.current.path+'.txt'])
681 filename=linp.checkalphainput(args[0],self.current.path+'.txt',[])
686 whichplot=int(args[1])
692 outofplot=self.plots[whichplot].vectors
694 print "Plot index out of range."
697 for dataset in self.plots[whichplot].vectors:
698 for i in range(0,len(dataset)):
700 for value in dataset[i]:
701 #columns[-1].append(str(value*(10**9)))
702 columns[-1].append(str(value))
703 rows=transposed2(columns, 'nan')
704 rows=[' , '.join(item) for item in rows]
707 txtfile=open(filename,'w+')
708 #Save units of measure in header
709 txtfile.write('X:'+self.plots[whichplot].units[0]+'\n')
710 txtfile.write('Y:'+self.plots[whichplot].units[1]+'\n')
716 for wp in range(len(self.plots)):
717 for dataset in self.plots[wp].vectors:
718 for i in range(0,len(dataset)):
720 for value in dataset[i]:
721 #columns[-1].append(str(value*(10**9)))
722 columns[-1].append(str(value))
723 rows=transposed2(columns, 'nan')
724 rows=[' , '.join(item) for item in rows]
727 txtfile=open(filename,'w+')
728 #Save units of measure in header
729 for i in range(len(self.plots)):
730 txtfile.write('X:'+self.plots[i].units[0]+'\n')
731 txtfile.write('Y:'+self.plots[i].units[1]+'\n')
736 #LOGGING, REPORTING, NOTETAKING
739 def do_note_old(self,args):
742 **deprecated**: Use note instead. Will be removed in 0.9
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 #bypass UnicodeDecodeError troubles
756 args=args.decode('ascii')
758 args=args.decode('ascii','ignore')
762 self.current_list[self.pointer].notes=args
766 def do_note(self,args):
770 Writes or displays a note about the current curve.
771 If [anything] is empty, it displays the note, otherwise it adds a note.
772 The note is then saved in the playlist if you issue a savelist command.
774 Syntax: note_old [anything]
778 print self.current_list[self.pointer].notes
780 if self.notes_filename == None:
781 if not os.path.exists(os.path.realpath('output')):
783 self.notes_filename=raw_input('Notebook filename? ')
784 self.notes_filename=os.path.join(os.path.realpath('output'),self.notes_filename)
785 title_line='Notes taken at '+time.asctime()+'\n'
786 f=open(self.notes_filename,'a')
790 #bypass UnicodeDecodeError troubles
792 args=args.decode('ascii')
794 args=args.decode('ascii','ignore')
797 self.current_list[self.pointer].notes=args
799 f=open(self.notes_filename,'a+')
800 note_string=(self.current.path+' | '+self.current.notes+'\n')
804 def help_notelog(self):
807 Writes a log of the notes taken during the session for the current
810 Syntax notelog [filename]
812 def do_notelog(self,args):
815 args=linp.safeinput('Notelog filename?',['notelog.txt'])
817 note_lines='Notes taken at '+time.asctime()+'\n'
818 for item in self.current_list:
819 if len(item.notes)>0:
820 #FIXME: log should be justified
821 #FIXME: file path should be truncated...
822 note_string=(item.path+' | '+item.notes+'\n')
823 note_lines+=note_string
829 except IOError, (ErrorNumber, ErrorMessage):
830 print 'Error: notes cannot be saved. Catched exception:'
835 def help_copylog(self):
838 Moves the annotated curves to another directory
840 Syntax copylog [directory]
842 def do_copylog(self,args):
845 args=linp.safeinput('Destination directory?') #TODO default
847 mydir=os.path.abspath(args)
848 if not os.path.isdir(mydir):
849 print 'Destination is not a directory.'
852 for item in self.current_list:
853 if len(item.notes)>0:
855 shutil.copy(item.path, mydir)
856 except (OSError, IOError):
857 print 'Cannot copy file. '+item.path+' Perhaps you gave me a wrong directory?'
861 def do_outlet_show(self,args):
864 Shows current content of outlet with index for reference
866 self.outlet.printbuf()
868 def do_outlet_undo(self, args):
871 Eliminates last entry in outlet
873 print 'Erasing last entry'
876 def do_outlet_delete(self, args):
878 Eliminates a particular entry from outlet
879 Syntax: outlet_delete n
882 print 'Index needed!, use outlet_show to know it'
884 self.outlet.delete(args)
886 #OS INTERACTION COMMANDS
891 Lists the files in the directory
896 def do_dir(self,args):
900 print glob.glob(args)
904 def do_ls(self,args):
910 Gives the current working directory.
914 def do_pwd(self,args):
920 Changes the current working directory
924 def do_cd(self,args):
925 mypath=os.path.abspath(args)
929 print 'I cannot access that directory.'
932 def help_system(self):
935 Executes a system command line and reports the output
937 Syntax system [command line]
940 def do_system(self,args):
941 waste=os.system(args)
943 def do_debug(self,args):
945 this is a dummy command where I put debugging things
947 print self.config['plotmanips']
950 def help_current(self):
953 Prints the current curve path.
957 def do_current(self,args):
958 print self.current.path
960 def do_info(self,args):
964 Returns informations about the current curve.
966 print 'Path: ',self.current.path
967 print 'Experiment: ',self.current.curve.experiment
968 print 'Filetype: ',self.current.curve.filetype
969 for plot in self.current.curve.default_plots():
970 for set in plot.vectors:
971 lengths=[len(item) for item in set]
972 print 'Data set size: ',lengths
974 def do_version(self,args):
978 Prints the current version and codename, plus library version. Useful for debugging.
980 print 'Hooke '+__version__+' ('+__codename__+')'
981 print 'Released on: '+__releasedate__
983 print 'Python version: '+python_version
984 print 'WxPython version: '+wx_version
985 print 'wxMPL version: '+wxmpl_version
986 print 'Matplotlib version: '+mpl_version
987 print 'SciPy version: '+scipy_version
988 print 'NumPy version: '+numpy_version
990 print 'Platform: '+str(platform.uname())
992 print 'Loaded plugins:',self.config['loaded_plugins']
997 Exits the program cleanly.
1002 def do_exit(self,args):
1005 if (not self.playlist_saved) or (not self.notes_saved):
1006 we_exit=linp.safeinput('You did not save your playlist and/or notes. Exit?',['n'])
1008 we_exit=linp.safeinput('Exit?',['y'])
1010 if we_exit[0].upper()=='Y':
1011 wx.CallAfter(self.frame.Close)
1016 def help_quit(self):
1018 def do_quit(self,args):
1025 if __name__ == '__main__':