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.
14 from .libhooke import HOOKE_VERSION, WX_GOOD
17 wxversion.select(WX_GOOD)
20 from wx.lib.newevent import NewEvent
21 from matplotlib.numerix import * #FIXME
23 import xml.dom.minidom
24 import sys, os, os.path, glob, shutil
31 global __releasedate__
32 __version__ = HOOKE_VERSION[0]
33 __codename__ = HOOKE_VERSION[1]
34 __releasedate__ = HOOKE_VERSION[2]
36 from matplotlib import __version__ as mpl_version
37 from wx import __version__ as wx_version
38 from wxmpl import __version__ as wxmpl_version
39 from scipy import __version__ as scipy_version
40 from numpy import __version__ as numpy_version
41 from sys import version as python_version
44 from .libhooke import PlaylistXML
45 from . import libhookecurve as lhc
46 from . import libinput as linp
47 from . import liboutlet as lout
50 class HookeCli(cmd.Cmd, object):
52 def __init__(self,frame,list_of_events,events_from_gui,config,drivers):
53 cmd.Cmd.__init__(self)
55 self.prompt = 'hooke: '
56 self.current_list=[] #the playlist we're using
57 self.current=None #the current curve under analysis.
60 The actual hierarchy of the "current curve" is a bit complex:
62 self.current = the lhc.HookeCurve container object of the current curve
63 self.current.curve = the current "real" curve object as defined in the filetype driver class
64 self.current.curve.default_plots() = the default plots of the filetype driver.
66 The plot objects obtained by mean of self.current.curve.default_plots()
67 then undergoes modifications by the plotmanip
68 modifier functions. The modified plot is saved in self.plots and used if needed by other functions.
70 self.pointer=0 #a pointer to navigate the current list
72 #Things that come from outside
73 self.frame=frame #the wx frame we refer to
74 self.list_of_events=list_of_events #a list of wx events we use to interact with the GUI
75 self.events_from_gui=events_from_gui #the Queue object we use to have messages from the GUI
76 self.config=config #the configuration dictionary
77 self.drivers=drivers #the file format drivers
79 #get plot manipulation functions
80 plotmanip_functions=[]
81 for object_name in dir(self):
82 if object_name[0:9]=='plotmanip':
83 plotmanip_functions.append(getattr(self,object_name))
84 #put plotmanips in order
85 self.plotmanip=[None for item in self.config['plotmanips']]
86 for item in plotmanip_functions:
87 namefunction=item.__name__[10:]
88 if namefunction in self.config['plotmanips']:
89 nameindex=self.config['plotmanips'].index(namefunction) #index of function in plotmanips config
90 self.plotmanip[nameindex] = item
95 self.playlist_saved=0 #Did we save the playlist?
96 self.playlist_name='' #Name of playlist
97 self.notes_saved=1 #Did we save the notes?
98 self.notes_filename=None #Name of notes
101 self.outlet=lout.Outlet()
103 #Data that must be saved in the playlist, related to the whole playlist (not individual curves)
104 self.playlist_generics={}
106 #make sure we execute _plug_init() for every command line plugin we import
107 for plugin_name in self.config['plugins']:
109 plugin=__import__(plugin_name)
111 eval('plugin.'+plugin_name+'Commands._plug_init(self)')
112 except AttributeError:
117 #load default list, if possible
118 self.do_loadlist(self.config['defaultlist'])
121 #Everything sending an event should be here
122 def _measure_N_points(self, N, whatset=1):
124 general helper function for N-points measures
126 wx.PostEvent(self.frame,self.list_of_events['measure_points'](num_of_points=N, set=whatset))
129 points=self.frame.events_from_gui.get()
135 def _get_displayed_plot(self,dest=0):
137 returns the currently displayed plot.
139 wx.PostEvent(self.frame, self.list_of_events['get_displayed_plot'](dest=dest))
142 displayed_plot=self.events_from_gui.get()
147 return displayed_plot
149 def _send_plot(self,plots):
151 sends a plot to the GUI
153 wx.PostEvent(self.frame, self.list_of_events['plot_graph'](plots=plots))
156 def _find_plotmanip(self, name):
158 returns a plot manipulator function from its name
160 return self.plotmanip[self.config['plotmanips'].index(name)]
162 def _clickize(self, xvector, yvector, index):
164 returns a ClickedPoint() object from an index and vectors of x, y coordinates
168 point.absolute_coords=xvector[index],yvector[index]
169 point.find_graph_coords(xvector,yvector)
177 Sets a local configuration variable
179 Syntax: set [variable] [value]
181 def do_set(self,args):
182 #FIXME: some variables in self.config should be hidden or intelligently configurated...
185 print 'You must specify a variable and a value'
186 print 'Available variables:'
187 print self.config.keys()
189 if args[0] not in self.config.keys():
190 print 'This is not an internal Hooke variable!'
193 #FIXME:we should reload the config file and reset the config value
194 print self.config[args[0]]
197 try: #try to have a numeric value
199 except ValueError: #if it cannot be converted to float, it's None, or a string...
201 if value.lower()=='none':
206 self.config[key]=value
209 #PLAYLIST MANAGEMENT AND NAVIGATION
210 #------------------------------------
212 def help_loadlist(self):
215 Loads a file playlist
217 Syntax: loadlist [playlist file]
219 def do_loadlist(self, args):
220 #checking for args: if nothing is given as input, we warn and exit.
222 args=linp.safeinput('File to load?')
225 play_to_load=arglist[0]
227 #We assume a Hooke playlist has the extension .hkp
228 if play_to_load[-4:] != '.hkp':
232 playxml=PlaylistXML()
233 self.current_list, self.playlist_generics=playxml.load(play_to_load)
234 self.current_playxml=playxml
236 print 'File not found.', play_to_load
239 print 'Loaded %s curves from %s' \
240 % (len(self.current_list), play_to_load)
242 if 'pointer' in self.playlist_generics.keys():
243 self.pointer=int(self.playlist_generics['pointer'])
245 #if no pointer is found, set the current curve as the first curve of the loaded playlist
247 print 'Starting at curve ',self.pointer
249 self.current=self.current_list[self.pointer]
251 #resets saved/notes saved state
252 self.playlist_saved=0
253 self.playlist_name=''
259 def help_genlist(self):
262 Generates a file playlist.
263 Note it doesn't *save* it: see savelist for this.
265 If [input files] is a directory, it will use all files in the directory for playlist.
271 are all equivalent syntax.
273 Syntax: genlist [input files]
276 def do_genlist(self,args):
277 #args list is: input path, output name
279 args=linp.safeinput('Input files?')
284 #if it's a directory, is like /directory/*.*
285 #FIXME: probably a bit kludgy.
286 if os.path.isdir(list_path):
287 if platform.system == 'Windows':
291 if list_path[-1] == SLASH:
292 list_path=list_path+'*'
294 list_path=list_path+SLASH+'*'
296 #expanding correctly the input list with the glob module :)
297 list_files=glob.glob(list_path)
301 for item in list_files:
303 if os.path.isfile(item):
304 self.current_list.append(lhc.HookeCurve(os.path.abspath(item)))
309 if len(self.current_list)>0:
310 self.current=self.current_list[self.pointer]
315 #resets saved/notes saved state
316 self.playlist_saved=0
317 self.playlist_name=''
323 def do_savelist(self,args):
326 Saves the current file playlist on disk.
328 Syntax: savelist [filename]
331 args=linp.safeinput('Output file?',['savedlist.txt'])
335 self.playlist_generics['pointer']=self.pointer
337 #autocomplete filename if not specified
338 if output_filename[-4:] != '.hkp':
339 output_filename+='.hkp'
341 playxml=PlaylistXML()
342 playxml.export(self.current_list, self.playlist_generics)
343 playxml.save(output_filename)
345 #remembers we have saved playlist
346 self.playlist_saved=1
348 def help_addtolist(self):
351 Adds a file to the current playlist
353 Syntax: addtolist [filename]
355 def do_addtolist(self,args):
356 #args list is: input path
358 print 'You must give the input filename you want to add'
359 self.help_addtolist()
362 filenames=glob.glob(args)
364 for filename in filenames:
365 self.current_list.append(lhc.HookeCurve(os.path.abspath(filename)))
366 #we need to save playlist
367 self.playlist_saved=0
369 def help_printlist(self):
372 Prints the list of curves in the current playlist
376 def do_printlist(self,args):
377 for item in self.current_list:
384 Jumps to a given curve.
386 Syntax: jump {$curve}
388 If the curve is not in the current playlist, it politely asks if we want to add it.
390 def do_jump(self,filename):
392 jumps to the curve with the given filename.
393 if the filename is not in the playlist, it asks if we must add it or not.
397 filename=linp.safeinput('Jump to?')
399 filepath=os.path.abspath(filename)
404 while item_not_found:
407 if self.current_list[c].path == filepath:
409 self.current=self.current_list[self.pointer]
415 #We've found the end of the list.
416 answer=linp.safeinput('Curve not found in playlist. Add it to list?',['y'])
417 if answer.lower()[0]=='y':
419 self.do_addtolist(filepath)
421 print 'Curve file not found.'
423 self.current=self.current_list[-1]
424 self.pointer=(len(current_list)-1)
430 def do_index(self,args):
433 Prints the index of the current curve in the list
437 print self.pointer+1, 'of', len(self.current_list)
443 Go the next curve in the playlist.
444 If we are at the last curve, we come back to the first.
448 def do_next(self,args):
450 self.current.curve.close_all()
452 print 'No curve file loaded, currently!'
453 print 'This should not happen, report to http://code.google.com/p/hooke'
456 if self.pointer == (len(self.current_list)-1):
458 print 'Playlist finished; back to first curve.'
462 self.current=self.current_list[self.pointer]
471 def help_previous(self,args):
474 Go to the previous curve in the playlist.
475 If we are at the first curve, we jump to the last.
479 def do_previous(self,args):
481 self.current.curve.close_all()
483 print 'No curve file loaded, currently!'
484 print 'This should not happen, report to http://code.google.com/p/hooke'
486 if self.pointer == 0:
487 self.pointer=(len(self.current_list)-1)
488 print 'Start of playlist; jump to last curve.'
492 self.current=self.current_list[self.pointer]
499 self.do_previous(args)
502 #PLOT INTERACTION COMMANDS
503 #-------------------------------
507 Plots the current force curve
511 def do_plot(self,args):
512 if self.current.identify(self.drivers) == False:
514 self.plots=self.current.curve.default_plots()
516 self.plots=self.current.curve.default_plots()
518 print 'Unexpected error occurred in do_plot().'
522 #apply the plotmanip functions eventually present
523 nplots=len(self.plots)
526 for function in self.plotmanip: #FIXME: something strange happens about self.plotmanip[0]
527 self.plots[c]=function(self.plots[c], self.current)
529 self.plots[c].xaxes=self.config['xaxes'] #FIXME: in the future, xaxes and yaxes should be set per-plot
530 self.plots[c].yaxes=self.config['yaxes']
534 self._send_plot(self.plots)
536 def _delta(self, set=1):
538 calculates the difference between two clicked points
540 print 'Click two points'
541 points=self._measure_N_points(N=2, whatset=set)
542 dx=abs(points[0].graph_coords[0]-points[1].graph_coords[0])
543 dy=abs(points[0].graph_coords[1]-points[1].graph_coords[1])
544 unitx=self.plots[points[0].dest].units[0]
545 unity=self.plots[points[0].dest].units[1]
546 return dx,unitx,dy,unity
548 def do_delta(self,args):
552 Measures the delta X and delta Y between two points.
556 dx,unitx,dy,unity=self._delta()
557 print str(dx)+' '+unitx
558 print str(dy)+' '+unity
560 def _point(self, set=1):
561 '''calculates the coordinates of a single clicked point'''
563 print 'Click one point'
564 point=self._measure_N_points(N=1, whatset=set)
566 x=point[0].graph_coords[0]
567 y=point[0].graph_coords[1]
568 unitx=self.plots[point[0].dest].units[0]
569 unity=self.plots[point[0].dest].units[1]
570 return x,unitx,y,unity
572 def do_point(self,args):
576 Returns the coordinates of a point on the graph.
580 x,unitx,y,unity=self._point()
581 print str(x)+' '+unitx
582 print str(y)+' '+unity
583 to_dump='point '+self.current.path+' '+str(x)+' '+unitx+', '+str(y)+' '+unity
584 self.outlet.push(to_dump)
587 def do_close(self,args=None):
590 Closes one of the two plots. If no arguments are given, the bottom plot is closed.
592 Syntax: close [top,bottom]
601 close_plot=self.list_of_events['close_plot']
602 wx.PostEvent(self.frame, close_plot(to_close=to_close))
604 def do_show(self,args=None):
609 show_plots=self.list_of_events['show_plots']
610 wx.PostEvent(self.frame, show_plots())
614 #PLOT EXPORT AND MANIPULATION COMMANDS
615 def help_export(self):
618 Saves the current plot as an image file
620 Syntax: export [filename] {plot to export}
622 The supported formats are PNG and EPS; the file extension of the filename is automatically recognized
623 and correctly exported. Resolution is (for now) fixed at 150 dpi.
625 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)
627 def do_export(self,args):
628 #FIXME: the bottom plot doesn't have the title
633 #FIXME: We have to go into the libinput stuff and fix it, for now here's a dummy replacement...
634 #name=linp.safeinput('Filename?',[self.current.path+'.png'])
635 name=raw_input('Filename? ')
642 export_image=self.list_of_events['export_image']
643 wx.PostEvent(self.frame, export_image(name=name, dest=dest))
649 Saves the current curve as a text file
650 Columns are, in order:
651 X1 , Y1 , X2 , Y2 , X3 , Y3 ...
654 Syntax: txt [filename] {plot to export}
656 def do_txt(self,args):
658 def transposed2(lists, defval=0):
660 transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
662 (by Zoran Isailovski on the Python Cookbook online)
664 if not lists: return []
665 return map(lambda *row: [elem or defval for elem in row], *lists)
670 filename=linp.safeinput('Filename?',[self.current.path+'.txt'])
672 filename=linp.checkalphainput(args[0],self.current.path+'.txt',[])
674 whichplot=int(args[1])
679 outofplot=self.plots[whichplot].vectors
681 print "Plot index out of range."
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 if not os.path.exists(os.path.realpath('output')):
750 self.notes_filename=raw_input('Notebook filename? ')
751 self.notes_filename=os.path.join(os.path.realpath('output'),self.notes_filename)
752 title_line='Notes taken at '+time.asctime()+'\n'
753 f=open(self.notes_filename,'a')
757 #bypass UnicodeDecodeError troubles
759 args=args.decode('ascii')
761 args=args.decode('ascii','ignore')
764 self.current_list[self.pointer].notes=args
766 f=open(self.notes_filename,'a+')
767 note_string=(self.current.path+' | '+self.current.notes+'\n')
771 def help_notelog(self):
774 Writes a log of the notes taken during the session for the current
777 Syntax notelog [filename]
779 def do_notelog(self,args):
782 args=linp.safeinput('Notelog filename?',['notelog.txt'])
784 note_lines='Notes taken at '+time.asctime()+'\n'
785 for item in self.current_list:
786 if len(item.notes)>0:
787 #FIXME: log should be justified
788 #FIXME: file path should be truncated...
789 note_string=(item.path+' | '+item.notes+'\n')
790 note_lines+=note_string
796 except IOError, (ErrorNumber, ErrorMessage):
797 print 'Error: notes cannot be saved. Catched exception:'
802 def help_copylog(self):
805 Moves the annotated curves to another directory
807 Syntax copylog [directory]
809 def do_copylog(self,args):
812 args=linp.safeinput('Destination directory?') #TODO default
814 mydir=os.path.abspath(args)
815 if not os.path.isdir(mydir):
816 print 'Destination is not a directory.'
819 for item in self.current_list:
820 if len(item.notes)>0:
822 shutil.copy(item.path, mydir)
823 except (OSError, IOError):
824 print 'Cannot copy file. '+item.path+' Perhaps you gave me a wrong directory?'
828 def do_outlet_show(self,args):
831 Shows current content of outlet with index for reference
833 self.outlet.printbuf()
835 def do_outlet_undo(self, args):
838 Eliminates last entry in outlet
840 print 'Erasing last entry'
843 def do_outlet_delete(self, args):
845 Eliminates a particular entry from outlet
846 Syntax: outlet_delete n
849 print 'Index needed!, use outlet_show to know it'
851 self.outlet.delete(args)
853 #OS INTERACTION COMMANDS
858 Lists the files in the directory
863 def do_dir(self,args):
867 print glob.glob(args)
871 def do_ls(self,args):
877 Gives the current working directory.
881 def do_pwd(self,args):
887 Changes the current working directory
891 def do_cd(self,args):
892 mypath=os.path.abspath(args)
896 print 'I cannot access that directory.'
899 def help_system(self):
902 Executes a system command line and reports the output
904 Syntax system [command line]
907 def do_system(self,args):
908 waste=os.system(args)
910 def do_debug(self,args):
912 this is a dummy command where I put debugging things
914 print self.config['plotmanips']
917 def help_current(self):
920 Prints the current curve path.
924 def do_current(self,args):
925 print self.current.path
927 def do_info(self,args):
931 Returns informations about the current curve.
933 print 'Path: ',self.current.path
934 print 'Experiment: ',self.current.curve.experiment
935 print 'Filetype: ',self.current.curve.filetype
936 for plot in self.current.curve.default_plots():
937 for set in plot.vectors:
938 lengths=[len(item) for item in set]
939 print 'Data set size: ',lengths
941 def do_version(self,args):
945 Prints the current version and codename, plus library version. Useful for debugging.
947 print 'Hooke '+__version__+' ('+__codename__+')'
948 print 'Released on: '+__releasedate__
950 print 'Python version: '+python_version
951 print 'WxPython version: '+wx_version
952 print 'wxMPL version: '+wxmpl_version
953 print 'Matplotlib version: '+mpl_version
954 print 'SciPy version: '+scipy_version
955 print 'NumPy version: '+numpy_version
957 print 'Platform: '+str(platform.uname())
959 print 'Loaded plugins:',self.config['loaded_plugins']
964 Exits the program cleanly.
969 def do_exit(self,args):
972 if (not self.playlist_saved) or (not self.notes_saved):
973 we_exit=linp.safeinput('You did not save your playlist and/or notes. Exit?',['n'])
975 we_exit=linp.safeinput('Exit?',['y'])
977 if we_exit[0].upper()=='Y':
978 wx.CallAfter(self.frame.Close)
985 def do_quit(self,args):
989 if __name__ == '__main__':