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.
13 from .libhooke import HOOKE_VERSION, WX_GOOD
16 wxversion.select(WX_GOOD)
19 from wx.lib.newevent import NewEvent
20 from matplotlib.numerix import * #FIXME
22 import xml.dom.minidom
23 import sys, os, os.path, glob, shutil
30 global __releasedate__
31 __version__ = HOOKE_VERSION[0]
32 __codename__ = HOOKE_VERSION[1]
33 __releasedate__ = HOOKE_VERSION[2]
35 from matplotlib import __version__ as mpl_version
36 from wx import __version__ as wx_version
37 from wxmpl import __version__ as wxmpl_version
38 from scipy import __version__ as scipy_version
39 from numpy import __version__ as numpy_version
40 from sys import version as python_version
43 from .libhooke import PlaylistXML
44 from . import libhookecurve as lhc
45 from . import libinput as linp
46 from . import liboutlet as lout
49 class HookeCli(cmd.Cmd, object):
51 def __init__(self,frame,list_of_events,events_from_gui,config,drivers):
52 cmd.Cmd.__init__(self)
54 self.prompt = 'hooke: '
55 self.current_list=[] #the playlist we're using
56 self.current=None #the current curve under analysis.
59 The actual hierarchy of the "current curve" is a bit complex:
61 self.current = the lhc.HookeCurve container object of the current curve
62 self.current.curve = the current "real" curve object as defined in the filetype driver class
63 self.current.curve.default_plots() = the default plots of the filetype driver.
65 The plot objects obtained by mean of self.current.curve.default_plots()
66 then undergoes modifications by the plotmanip
67 modifier functions. The modified plot is saved in self.plots and used if needed by other functions.
69 self.pointer=0 #a pointer to navigate the current list
71 #Things that come from outside
72 self.frame=frame #the wx frame we refer to
73 self.list_of_events=list_of_events #a list of wx events we use to interact with the GUI
74 self.events_from_gui=events_from_gui #the Queue object we use to have messages from the GUI
75 self.config=config #the configuration dictionary
76 self.drivers=drivers #the file format drivers
78 #get plot manipulation functions
79 plotmanip_functions=[]
80 for object_name in dir(self):
81 if object_name[0:9]=='plotmanip':
82 plotmanip_functions.append(getattr(self,object_name))
83 #put plotmanips in order
84 self.plotmanip=[None for item in self.config['plotmanips']]
85 for item in plotmanip_functions:
86 namefunction=item.__name__[10:]
87 if namefunction in self.config['plotmanips']:
88 nameindex=self.config['plotmanips'].index(namefunction) #index of function in plotmanips config
89 self.plotmanip[nameindex] = item
94 self.playlist_saved=0 #Did we save the playlist?
95 self.playlist_name='' #Name of playlist
96 self.notes_saved=1 #Did we save the notes?
97 self.notes_filename=None #Name of notes
100 self.outlet=lout.Outlet()
102 #Data that must be saved in the playlist, related to the whole playlist (not individual curves)
103 self.playlist_generics={}
105 #make sure we execute _plug_init() for every command line plugin we import
106 for plugin_name in self.config['plugins']:
108 plugin=__import__(plugin_name)
110 eval('plugin.'+plugin_name+'Commands._plug_init(self)')
111 except AttributeError:
116 #load default list, if possible
117 self.do_loadlist(self.config['defaultlist'])
120 #Everything sending an event should be here
121 def _measure_N_points(self, N, whatset=1):
123 general helper function for N-points measures
125 wx.PostEvent(self.frame,self.list_of_events['measure_points'](num_of_points=N, set=whatset))
128 points=self.frame.events_from_gui.get()
134 def _get_displayed_plot(self,dest=0):
136 returns the currently displayed plot.
138 wx.PostEvent(self.frame, self.list_of_events['get_displayed_plot'](dest=dest))
141 displayed_plot=self.events_from_gui.get()
146 return displayed_plot
148 def _send_plot(self,plots):
150 sends a plot to the GUI
152 wx.PostEvent(self.frame, self.list_of_events['plot_graph'](plots=plots))
155 def _find_plotmanip(self, name):
157 returns a plot manipulator function from its name
159 return self.plotmanip[self.config['plotmanips'].index(name)]
161 def _clickize(self, xvector, yvector, index):
163 returns a ClickedPoint() object from an index and vectors of x, y coordinates
167 point.absolute_coords=xvector[index],yvector[index]
168 point.find_graph_coords(xvector,yvector)
176 Sets a local configuration variable
178 Syntax: set [variable] [value]
180 def do_set(self,args):
181 #FIXME: some variables in self.config should be hidden or intelligently configurated...
184 print 'You must specify a variable and a value'
185 print 'Available variables:'
186 print self.config.keys()
188 if args[0] not in self.config.keys():
189 print 'This is not an internal Hooke variable!'
192 #FIXME:we should reload the config file and reset the config value
193 print self.config[args[0]]
196 try: #try to have a numeric value
198 except ValueError: #if it cannot be converted to float, it's None, or a string...
200 if value.lower()=='none':
205 self.config[key]=value
208 #PLAYLIST MANAGEMENT AND NAVIGATION
209 #------------------------------------
211 def help_loadlist(self):
214 Loads a file playlist
216 Syntax: loadlist [playlist file]
218 def do_loadlist(self, args):
219 #checking for args: if nothing is given as input, we warn and exit.
221 args=linp.safeinput('File to load?')
224 play_to_load=arglist[0]
226 #We assume a Hooke playlist has the extension .hkp
227 if play_to_load[-4:] != '.hkp':
231 playxml=PlaylistXML()
232 self.current_list, self.playlist_generics=playxml.load(play_to_load)
233 self.current_playxml=playxml
235 print 'File not found.', play_to_load
238 print 'Loaded %s curves from %s' \
239 % (len(self.current_list), play_to_load)
241 if 'pointer' in self.playlist_generics.keys():
242 self.pointer=int(self.playlist_generics['pointer'])
244 #if no pointer is found, set the current curve as the first curve of the loaded playlist
246 print 'Starting at curve ',self.pointer
248 self.current=self.current_list[self.pointer]
250 #resets saved/notes saved state
251 self.playlist_saved=0
252 self.playlist_name=''
258 def help_genlist(self):
261 Generates a file playlist.
262 Note it doesn't *save* it: see savelist for this.
264 If [input files] is a directory, it will use all files in the directory for playlist.
270 are all equivalent syntax.
272 Syntax: genlist [input files]
275 def do_genlist(self,args):
276 #args list is: input path, output name
278 args=linp.safeinput('Input files?')
283 #if it's a directory, is like /directory/*.*
284 #FIXME: probably a bit kludgy.
285 if os.path.isdir(list_path):
286 if platform.system == 'Windows':
290 if list_path[-1] == SLASH:
291 list_path=list_path+'*.*'
293 list_path=list_path+SLASH+'*.*'
295 #expanding correctly the input list with the glob module :)
296 list_files=glob.glob(list_path)
300 for item in list_files:
302 if os.path.isfile(item):
303 self.current_list.append(lhc.HookeCurve(os.path.abspath(item)))
308 if len(self.current_list)>0:
309 self.current=self.current_list[self.pointer]
314 #resets saved/notes saved state
315 self.playlist_saved=0
316 self.playlist_name=''
322 def do_savelist(self,args):
325 Saves the current file playlist on disk.
327 Syntax: savelist [filename]
330 args=linp.safeinput('Output file?',['savedlist.txt'])
334 self.playlist_generics['pointer']=self.pointer
336 #autocomplete filename if not specified
337 if output_filename[-4:] != '.hkp':
338 output_filename+='.hkp'
340 playxml=PlaylistXML()
341 playxml.export(self.current_list, self.playlist_generics)
342 playxml.save(output_filename)
344 #remembers we have saved playlist
345 self.playlist_saved=1
347 def help_addtolist(self):
350 Adds a file to the current playlist
352 Syntax: addtolist [filename]
354 def do_addtolist(self,args):
355 #args list is: input path
357 print 'You must give the input filename you want to add'
358 self.help_addtolist()
361 filenames=glob.glob(args)
363 for filename in filenames:
364 self.current_list.append(lhc.HookeCurve(os.path.abspath(filename)))
365 #we need to save playlist
366 self.playlist_saved=0
368 def help_printlist(self):
371 Prints the list of curves in the current playlist
375 def do_printlist(self,args):
376 for item in self.current_list:
383 Jumps to a given curve.
385 Syntax: jump {$curve}
387 If the curve is not in the current playlist, it politely asks if we want to add it.
389 def do_jump(self,filename):
391 jumps to the curve with the given filename.
392 if the filename is not in the playlist, it asks if we must add it or not.
396 filename=linp.safeinput('Jump to?')
398 filepath=os.path.abspath(filename)
403 while item_not_found:
406 if self.current_list[c].path == filepath:
408 self.current=self.current_list[self.pointer]
414 #We've found the end of the list.
415 answer=linp.safeinput('Curve not found in playlist. Add it to list?',['y'])
416 if answer.lower()[0]=='y':
418 self.do_addtolist(filepath)
420 print 'Curve file not found.'
422 self.current=self.current_list[-1]
423 self.pointer=(len(current_list)-1)
429 def do_index(self,args):
432 Prints the index of the current curve in the list
436 print self.pointer+1, 'of', len(self.current_list)
442 Go the next curve in the playlist.
443 If we are at the last curve, we come back to the first.
447 def do_next(self,args):
449 self.current.curve.close_all()
451 print 'No curve file loaded, currently!'
452 print 'This should not happen, report to http://code.google.com/p/hooke'
455 if self.pointer == (len(self.current_list)-1):
457 print 'Playlist finished; back to first curve.'
461 self.current=self.current_list[self.pointer]
470 def help_previous(self,args):
473 Go to the previous curve in the playlist.
474 If we are at the first curve, we jump to the last.
478 def do_previous(self,args):
480 self.current.curve.close_all()
482 print 'No curve file loaded, currently!'
483 print 'This should not happen, report to http://code.google.com/p/hooke'
485 if self.pointer == 0:
486 self.pointer=(len(self.current_list)-1)
487 print 'Start of playlist; jump to last curve.'
491 self.current=self.current_list[self.pointer]
498 self.do_previous(args)
501 #PLOT INTERACTION COMMANDS
502 #-------------------------------
506 Plots the current force curve
510 def do_plot(self,args):
511 if self.current.identify(self.drivers) == False:
513 self.plots=self.current.curve.default_plots()
515 self.plots=self.current.curve.default_plots()
517 print 'Unexpected error occurred in do_plot().'
521 #apply the plotmanip functions eventually present
522 nplots=len(self.plots)
525 for function in self.plotmanip: #FIXME: something strange happens about self.plotmanip[0]
526 self.plots[c]=function(self.plots[c], self.current)
528 self.plots[c].xaxes=self.config['xaxes'] #FIXME: in the future, xaxes and yaxes should be set per-plot
529 self.plots[c].yaxes=self.config['yaxes']
533 self._send_plot(self.plots)
535 def _delta(self, set=1):
537 calculates the difference between two clicked points
539 print 'Click two points'
540 points=self._measure_N_points(N=2, whatset=set)
541 dx=abs(points[0].graph_coords[0]-points[1].graph_coords[0])
542 dy=abs(points[0].graph_coords[1]-points[1].graph_coords[1])
543 unitx=self.plots[points[0].dest].units[0]
544 unity=self.plots[points[0].dest].units[1]
545 return dx,unitx,dy,unity
547 def do_delta(self,args):
551 Measures the delta X and delta Y between two points.
555 dx,unitx,dy,unity=self._delta()
556 print str(dx)+' '+unitx
557 print str(dy)+' '+unity
559 def _point(self, set=1):
560 '''calculates the coordinates of a single clicked point'''
562 print 'Click one point'
563 point=self._measure_N_points(N=1, whatset=set)
565 x=point[0].graph_coords[0]
566 y=point[0].graph_coords[1]
567 unitx=self.plots[point[0].dest].units[0]
568 unity=self.plots[point[0].dest].units[1]
569 return x,unitx,y,unity
571 def do_point(self,args):
575 Returns the coordinates of a point on the graph.
579 x,unitx,y,unity=self._point()
580 print str(x)+' '+unitx
581 print str(y)+' '+unity
582 to_dump='point '+self.current.path+' '+str(x)+' '+unitx+', '+str(y)+' '+unity
583 self.outlet.push(to_dump)
586 def do_close(self,args=None):
589 Closes one of the two plots. If no arguments are given, the bottom plot is closed.
591 Syntax: close [top,bottom]
600 close_plot=self.list_of_events['close_plot']
601 wx.PostEvent(self.frame, close_plot(to_close=to_close))
603 def do_show(self,args=None):
608 show_plots=self.list_of_events['show_plots']
609 wx.PostEvent(self.frame, show_plots())
613 #PLOT EXPORT AND MANIPULATION COMMANDS
614 def help_export(self):
617 Saves the current plot as an image file
619 Syntax: export [filename] {plot to export}
621 The supported formats are PNG and EPS; the file extension of the filename is automatically recognized
622 and correctly exported. Resolution is (for now) fixed at 150 dpi.
624 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)
626 def do_export(self,args):
627 #FIXME: the bottom plot doesn't have the title
632 #FIXME: We have to go into the libinput stuff and fix it, for now here's a dummy replacement...
633 #name=linp.safeinput('Filename?',[self.current.path+'.png'])
634 name=raw_input('Filename? ')
641 export_image=self.list_of_events['export_image']
642 wx.PostEvent(self.frame, export_image(name=name, dest=dest))
648 Saves the current curve as a text file
649 Columns are, in order:
650 X1 , Y1 , X2 , Y2 , X3 , Y3 ...
653 Syntax: txt [filename] {plot to export}
655 def do_txt(self,args):
657 def transposed2(lists, defval=0):
659 transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
661 (by Zoran Isailovski on the Python Cookbook online)
663 if not lists: return []
664 return map(lambda *row: [elem or defval for elem in row], *lists)
669 filename=linp.safeinput('Filename?',[self.current.path+'.txt'])
671 filename=linp.checkalphainput(args[0],self.current.path+'.txt',[])
673 whichplot=int(args[1])
678 for dataset in self.plots[whichplot].vectors:
679 for i in range(0,len(dataset)):
681 for value in dataset[i]:
682 columns[-1].append(str(value))
684 rows=transposed2(columns, 'nan')
685 rows=[' , '.join(item) for item in rows]
688 txtfile=open(filename,'w+')
689 #Save units of measure in header
690 txtfile.write('X:'+self.plots[whichplot].units[0]+'\n')
691 txtfile.write('Y:'+self.plots[whichplot].units[1]+'\n')
696 #LOGGING, REPORTING, NOTETAKING
699 def do_note_old(self,args):
702 **deprecated**: Use note instead. Will be removed in 0.9
704 Writes or displays a note about the current curve.
705 If [anything] is empty, it displays the note, otherwise it adds a note.
706 The note is then saved in the playlist if you issue a savelist command
708 Syntax: note_old [anything]
712 print self.current_list[self.pointer].notes
714 #bypass UnicodeDecodeError troubles
716 args=args.decode('ascii')
718 args=args.decode('ascii','ignore')
722 self.current_list[self.pointer].notes=args
726 def do_note(self,args):
730 Writes or displays a note about the current curve.
731 If [anything] is empty, it displays the note, otherwise it adds a note.
732 The note is then saved in the playlist if you issue a savelist command.
734 Syntax: note_old [anything]
738 print self.current_list[self.pointer].notes
740 if self.notes_filename == None:
741 self.notes_filename=raw_input('Notebook filename? ')
742 title_line='Notes taken at '+time.asctime()+'\n'
743 f=open(self.notes_filename,'w')
747 #bypass UnicodeDecodeError troubles
749 args=args.decode('ascii')
751 args=args.decode('ascii','ignore')
754 self.current_list[self.pointer].notes=args
756 f=open(self.notes_filename,'a+')
757 note_string=(self.current.path+' | '+self.current.notes+'\n')
761 def help_notelog(self):
764 Writes a log of the notes taken during the session for the current
767 Syntax notelog [filename]
769 def do_notelog(self,args):
772 args=linp.safeinput('Notelog filename?',['notelog.txt'])
774 note_lines='Notes taken at '+time.asctime()+'\n'
775 for item in self.current_list:
776 if len(item.notes)>0:
777 #FIXME: log should be justified
778 #FIXME: file path should be truncated...
779 note_string=(item.path+' | '+item.notes+'\n')
780 note_lines+=note_string
786 except IOError, (ErrorNumber, ErrorMessage):
787 print 'Error: notes cannot be saved. Catched exception:'
792 def help_copylog(self):
795 Moves the annotated curves to another directory
797 Syntax copylog [directory]
799 def do_copylog(self,args):
802 args=linp.safeinput('Destination directory?') #TODO default
804 mydir=os.path.abspath(args)
805 if not os.path.isdir(mydir):
806 print 'Destination is not a directory.'
809 for item in self.current_list:
810 if len(item.notes)>0:
812 shutil.copy(item.path, mydir)
813 except (OSError, IOError):
814 print 'Cannot copy file. '+item.path+' Perhaps you gave me a wrong directory?'
818 def do_outlet_show(self,args):
821 Shows current content of outlet with index for reference
823 self.outlet.printbuf()
825 def do_outlet_undo(self, args):
828 Eliminates last entry in outlet
830 print 'Erasing last entry'
833 def do_outlet_delete(self, args):
835 Eliminates a particular entry from outlet
836 Syntax: outlet_delete n
839 print 'Index needed!, use outlet_show to know it'
841 self.outlet.delete(args)
843 #OS INTERACTION COMMANDS
848 Lists the files in the directory
853 def do_dir(self,args):
857 print glob.glob(args)
861 def do_ls(self,args):
867 Gives the current working directory.
871 def do_pwd(self,args):
877 Changes the current working directory
881 def do_cd(self,args):
882 mypath=os.path.abspath(args)
886 print 'I cannot access that directory.'
889 def help_system(self):
892 Executes a system command line and reports the output
894 Syntax system [command line]
897 def do_system(self,args):
898 waste=os.system(args)
900 def do_debug(self,args):
902 this is a dummy command where I put debugging things
904 print self.config['plotmanips']
907 def help_current(self):
910 Prints the current curve path.
914 def do_current(self,args):
915 print self.current.path
917 def do_info(self,args):
921 Returns informations about the current curve.
923 print 'Path: ',self.current.path
924 print 'Experiment: ',self.current.curve.experiment
925 print 'Filetype: ',self.current.curve.filetype
926 for plot in self.current.curve.default_plots():
927 for set in plot.vectors:
928 lengths=[len(item) for item in set]
929 print 'Data set size: ',lengths
931 def do_version(self,args):
935 Prints the current version and codename, plus library version. Useful for debugging.
937 print 'Hooke '+__version__+' ('+__codename__+')'
938 print 'Released on: '+__releasedate__
940 print 'Python version: '+python_version
941 print 'WxPython version: '+wx_version
942 print 'wxMPL version: '+wxmpl_version
943 print 'Matplotlib version: '+mpl_version
944 print 'SciPy version: '+scipy_version
945 print 'NumPy version: '+numpy_version
947 print 'Platform: '+str(platform.uname())
949 print 'Loaded plugins:',self.config['loaded_plugins']
954 Exits the program cleanly.
959 def do_exit(self,args):
962 if (not self.playlist_saved) or (not self.notes_saved):
963 we_exit=linp.safeinput('You did not save your playlist and/or notes. Exit?',['n'])
965 we_exit=linp.safeinput('Exit?',['y'])
967 if we_exit[0].upper()=='Y':
968 wx.CallAfter(self.frame.Close)
975 def do_quit(self,args):
979 if __name__ == '__main__':