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 curve 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 def help_printlist(self):
212 Prints the list of curves in the current playlist
216 def do_printlist(self,args):
217 for item in self.current_list:
224 Jumps to a given curve.
226 Syntax: jump {$curve}
228 If the curve is not in the current playlist, it politely asks if we want to add it.
230 def do_jump(self,filename):
232 jumps to the curve with the given filename.
233 if the filename is not in the playlist, it asks if we must add it or not.
237 filename=linp.safeinput('Jump to?')
239 filepath=os.path.abspath(filename)
244 while item_not_found:
247 if self.current_list[c].path == filepath:
249 self.current=self.current_list[self.pointer]
255 #We've found the end of the list.
256 answer=linp.safeinput('Curve not found in playlist. Add it to list?',['y'])
257 if answer.lower()[0]=='y':
259 self.do_addtolist(filepath)
261 print 'Curve file not found.'
263 self.current=self.current_list[-1]
264 self.pointer=(len(current_list)-1)
270 def do_index(self,args):
273 Prints the index of the current curve in the list
277 print self.pointer+1, 'of', len(self.current_list)
283 Go the next curve in the playlist.
284 If we are at the last curve, we come back to the first.
288 def do_next(self,args):
290 self.current.curve.close_all()
292 print 'No curve file loaded, currently!'
293 print 'This should not happen, report to http://code.google.com/p/hooke'
296 if self.pointer == (len(self.current_list)-1):
298 print 'Playlist finished; back to first curve.'
302 self.current=self.current_list[self.pointer]
311 def help_previous(self,args):
314 Go to the previous curve in the playlist.
315 If we are at the first curve, we jump to the last.
319 def do_previous(self,args):
321 self.current.curve.close_all()
323 print 'No curve file loaded, currently!'
324 print 'This should not happen, report to http://code.google.com/p/hooke'
326 if self.pointer == 0:
327 self.pointer=(len(self.current_list)-1)
328 print 'Start of playlist; jump to last curve.'
332 self.current=self.current_list[self.pointer]
339 self.do_previous(args)
342 #PLOT INTERACTION COMMANDS
343 #-------------------------------
347 Plots the current force curve
351 def do_plot(self,args):
352 if self.current.identify(self.drivers) == False:
354 self.plots=self.current.curve.default_plots()
356 self.plots=self.current.curve.default_plots()
358 print 'Unexpected error occurred in do_plot().'
362 #apply the plotmanip functions eventually present
363 nplots=len(self.plots)
366 for function in self.plotmanip: #FIXME: something strange happens about self.plotmanip[0]
367 self.plots[c]=function(self.plots[c], self.current)
369 self.plots[c].xaxes=self.config['xaxes'] #FIXME: in the future, xaxes and yaxes should be set per-plot
370 self.plots[c].yaxes=self.config['yaxes']
374 self._send_plot(self.plots)
376 def _delta(self, set=1):
378 calculates the difference between two clicked points
380 print 'Click two points'
381 points=self._measure_N_points(N=2, whatset=set)
382 dx=abs(points[0].graph_coords[0]-points[1].graph_coords[0])
383 dy=abs(points[0].graph_coords[1]-points[1].graph_coords[1])
384 unitx=self.plots[points[0].dest].units[0]
385 unity=self.plots[points[0].dest].units[1]
386 return dx,unitx,dy,unity
388 def do_delta(self,args):
392 Measures the delta X and delta Y between two points.
396 dx,unitx,dy,unity=self._delta()
397 print str(dx)+' '+unitx
398 print str(dy)+' '+unity
400 def _point(self, set=1):
401 '''calculates the coordinates of a single clicked point'''
403 print 'Click one point'
404 point=self._measure_N_points(N=1, whatset=set)
406 x=point[0].graph_coords[0]
407 y=point[0].graph_coords[1]
408 unitx=self.plots[point[0].dest].units[0]
409 unity=self.plots[point[0].dest].units[1]
410 return x,unitx,y,unity
412 def do_point(self,args):
416 Returns the coordinates of a point on the graph.
420 x,unitx,y,unity=self._point()
421 print str(x)+' '+unitx
422 print str(y)+' '+unity
423 to_dump='point '+self.current.path+' '+str(x)+' '+unitx+', '+str(y)+' '+unity
424 self.outlet.push(to_dump)
427 def do_close(self,args=None):
430 Closes one of the two plots. If no arguments are given, the bottom plot is closed.
432 Syntax: close [top,bottom]
441 close_plot=self.list_of_events['close_plot']
442 wx.PostEvent(self.frame, close_plot(to_close=to_close))
444 def do_show(self,args=None):
449 show_plots=self.list_of_events['show_plots']
450 wx.PostEvent(self.frame, show_plots())
454 #PLOT EXPORT AND MANIPULATION COMMANDS
455 def help_export(self):
458 Saves the current plot as an image file
460 Syntax: export [filename] {plot to export}
462 The supported formats are PNG and EPS; the file extension of the filename is automatically recognized
463 and correctly exported. Resolution is (for now) fixed at 150 dpi.
465 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)
467 def do_export(self,args):
468 #FIXME: the bottom plot doesn't have the title
473 #FIXME: We have to go into the libinput stuff and fix it, for now here's a dummy replacement...
474 #name=linp.safeinput('Filename?',[self.current.path+'.png'])
475 name=raw_input('Filename? ')
482 export_image=self.list_of_events['export_image']
483 wx.PostEvent(self.frame, export_image(name=name, dest=dest))
489 Saves the current curve as a text file
490 Columns are, in order:
491 X1 , Y1 , X2 , Y2 , X3 , Y3 ...
494 Syntax: txt [filename] {plot to export}
496 def do_txt(self,args):
498 def transposed2(lists, defval=0):
500 transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
502 (by Zoran Isailovski on the Python Cookbook online)
504 if not lists: return []
505 return map(lambda *row: [elem or defval for elem in row], *lists)
510 filename=linp.safeinput('Filename?',[self.current.path+'.txt'])
512 filename=linp.checkalphainput(args[0],self.current.path+'.txt',[])
514 whichplot=int(args[1])
519 outofplot=self.plots[whichplot].vectors
521 print "Plot index out of range."
525 for dataset in self.plots[whichplot].vectors:
526 for i in range(0,len(dataset)):
528 for value in dataset[i]:
529 columns[-1].append(str(value))
531 rows=transposed2(columns, 'nan')
532 rows=[' , '.join(item) for item in rows]
535 txtfile=open(filename,'w+')
536 #Save units of measure in header
537 txtfile.write('X:'+self.plots[whichplot].units[0]+'\n')
538 txtfile.write('Y:'+self.plots[whichplot].units[1]+'\n')
543 #LOGGING, REPORTING, NOTETAKING
546 def do_note_old(self,args):
549 **deprecated**: Use note instead. Will be removed in 0.9
551 Writes or displays a note about the current curve.
552 If [anything] is empty, it displays the note, otherwise it adds a note.
553 The note is then saved in the playlist if you issue a savelist command
555 Syntax: note_old [anything]
559 print self.current_list[self.pointer].notes
561 #bypass UnicodeDecodeError troubles
563 args=args.decode('ascii')
565 args=args.decode('ascii','ignore')
569 self.current_list[self.pointer].notes=args
573 def do_note(self,args):
577 Writes or displays a note about the current curve.
578 If [anything] is empty, it displays the note, otherwise it adds a note.
579 The note is then saved in the playlist if you issue a savelist command.
581 Syntax: note_old [anything]
585 print self.current_list[self.pointer].notes
587 if self.notes_filename == None:
588 if not os.path.exists(os.path.realpath('output')):
590 self.notes_filename=raw_input('Notebook filename? ')
591 self.notes_filename=os.path.join(os.path.realpath('output'),self.notes_filename)
592 title_line='Notes taken at '+time.asctime()+'\n'
593 f=open(self.notes_filename,'a')
597 #bypass UnicodeDecodeError troubles
599 args=args.decode('ascii')
601 args=args.decode('ascii','ignore')
604 self.current_list[self.pointer].notes=args
606 f=open(self.notes_filename,'a+')
607 note_string=(self.current.path+' | '+self.current.notes+'\n')
611 def help_notelog(self):
614 Writes a log of the notes taken during the session for the current
617 Syntax notelog [filename]
619 def do_notelog(self,args):
622 args=linp.safeinput('Notelog filename?',['notelog.txt'])
624 note_lines='Notes taken at '+time.asctime()+'\n'
625 for item in self.current_list:
626 if len(item.notes)>0:
627 #FIXME: log should be justified
628 #FIXME: file path should be truncated...
629 note_string=(item.path+' | '+item.notes+'\n')
630 note_lines+=note_string
636 except IOError, (ErrorNumber, ErrorMessage):
637 print 'Error: notes cannot be saved. Catched exception:'
642 def help_copylog(self):
645 Moves the annotated curves to another directory
647 Syntax copylog [directory]
649 def do_copylog(self,args):
652 args=linp.safeinput('Destination directory?') #TODO default
654 mydir=os.path.abspath(args)
655 if not os.path.isdir(mydir):
656 print 'Destination is not a directory.'
659 for item in self.current_list:
660 if len(item.notes)>0:
662 shutil.copy(item.path, mydir)
663 except (OSError, IOError):
664 print 'Cannot copy file. '+item.path+' Perhaps you gave me a wrong directory?'
668 def do_outlet_show(self,args):
671 Shows current content of outlet with index for reference
673 self.outlet.printbuf()
675 def do_outlet_undo(self, args):
678 Eliminates last entry in outlet
680 print 'Erasing last entry'
683 def do_outlet_delete(self, args):
685 Eliminates a particular entry from outlet
686 Syntax: outlet_delete n
689 print 'Index needed!, use outlet_show to know it'
691 self.outlet.delete(args)
693 if __name__ == '__main__':