b2833535f6819e5929778126247f93dc4c678e3d
[hooke.git] / hooke / hooke_cli.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 '''
5 hooke_cli.py
6
7 Command line module of Hooke.
8
9 Copyright (C) 2006 Massimo Sandal (University of Bologna, Italy).
10
11 This program is released under the GNU General Public License version 2.
12 '''
13
14 from .libhooke import HOOKE_VERSION, WX_GOOD
15
16 import wxversion
17 wxversion.select(WX_GOOD)
18 import wx
19
20 from wx.lib.newevent import NewEvent
21 from matplotlib.numerix import * #FIXME
22
23 import xml.dom.minidom
24 import sys, os, os.path, glob, shutil
25 import Queue
26 import cmd
27 import time
28
29 global __version__
30 global __codename__
31 global __releasedate__
32 __version__ = HOOKE_VERSION[0]
33 __codename__ = HOOKE_VERSION[1]
34 __releasedate__ = HOOKE_VERSION[2]
35
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
42 import platform
43
44 from .libhooke import PlaylistXML
45 from . import curve as lhc
46 from . import libinput as linp
47 from . import liboutlet as lout
48
49
50 class HookeCli(cmd.Cmd, object):
51
52     def __init__(self,frame,list_of_events,events_from_gui,config,drivers):
53         cmd.Cmd.__init__(self)
54
55         self.prompt = 'hooke: '
56         self.current_list=[] #the playlist we're using
57         self.current=None    #the current curve under analysis.
58         self.plots=None
59         '''
60         The actual hierarchy of the "current curve" is a bit complex:
61
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.
65
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.
69         '''
70         self.pointer=0       #a pointer to navigate the current list
71
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
78
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
91             else:
92                 pass
93
94
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
99
100         #create outlet
101         self.outlet=lout.Outlet()
102
103         #Data that must be saved in the playlist, related to the whole playlist (not individual curves)
104         self.playlist_generics={}
105
106         #make sure we execute _plug_init() for every command line plugin we import
107         for plugin_name in self.config['plugins']:
108             try:
109                 plugin=__import__(plugin_name)
110                 try:
111                     eval('plugin.'+plugin_name+'Commands._plug_init(self)')
112                 except AttributeError:
113                     pass
114             except ImportError:
115                 pass
116
117         #load default list, if possible
118         self.do_loadlist(self.config['defaultlist'])
119
120 #HELPER FUNCTIONS
121 #Everything sending an event should be here
122     def _measure_N_points(self, N, whatset=1):
123         '''
124         general helper function for N-points measures
125         '''
126         wx.PostEvent(self.frame,self.list_of_events['measure_points'](num_of_points=N, set=whatset))
127         while 1:
128             try:
129                 points=self.frame.events_from_gui.get()
130                 break
131             except Empty:
132                 pass
133         return points
134
135     def _get_displayed_plot(self,dest=0):
136         '''
137         returns the currently displayed plot.
138         '''
139         wx.PostEvent(self.frame, self.list_of_events['get_displayed_plot'](dest=dest))
140         while 1:
141             try:
142                 displayed_plot=self.events_from_gui.get()
143             except Empty:
144                 pass
145             if displayed_plot:
146                 break
147         return displayed_plot
148
149     def _send_plot(self,plots):
150         '''
151         sends a plot to the GUI
152         '''
153         wx.PostEvent(self.frame, self.list_of_events['plot_graph'](plots=plots))
154         return
155
156     def _find_plotmanip(self, name):
157         '''
158         returns a plot manipulator function from its name
159         '''
160         return self.plotmanip[self.config['plotmanips'].index(name)]
161
162     def _clickize(self, xvector, yvector, index):
163         '''
164         returns a ClickedPoint() object from an index and vectors of x, y coordinates
165         '''
166         point=ClickedPoint()
167         point.index=index
168         point.absolute_coords=xvector[index],yvector[index]
169         point.find_graph_coords(xvector,yvector)
170         return point
171
172 #HERE COMMANDS BEGIN
173
174     def help_set(self):
175         print '''
176 SET
177 Sets a local configuration variable
178 -------------
179 Syntax: set [variable] [value]
180         '''
181     def do_set(self,args):
182         #FIXME: some variables in self.config should be hidden or intelligently configurated...
183         args=args.split()
184         if len(args)==0:
185             print 'You must specify a variable and a value'
186             print 'Available variables:'
187             print self.config.keys()
188             return
189         if args[0] not in self.config.keys():
190             print 'This is not an internal Hooke variable!'
191             return
192         if len(args)==1:
193             #FIXME:we should reload the config file and reset the config value
194             print self.config[args[0]]
195             return
196         key=args[0]
197         try: #try to have a numeric value
198             value=float(args[1])
199         except ValueError: #if it cannot be converted to float, it's None, or a string...
200             value=args[1]
201             if value.lower()=='none':
202                 value=None
203             else:
204                 value=args[1]
205
206         self.config[key]=value
207         self.do_plot(0)
208
209     def help_printlist(self):
210         print '''
211 PRINTLIST
212 Prints the list of curves in the current playlist
213 -------------
214 Syntax: printlist
215 '''
216     def do_printlist(self,args):
217         for item in self.current_list:
218             print item.path
219
220
221     def help_jump(self):
222         print '''
223 JUMP
224 Jumps to a given curve.
225 ------
226 Syntax: jump {$curve}
227
228 If the curve is not in the current playlist, it politely asks if we want to add it.
229         '''
230     def do_jump(self,filename):
231         '''
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.
234         '''
235
236         if filename=='':
237             filename=linp.safeinput('Jump to?')
238
239         filepath=os.path.abspath(filename)
240         print filepath
241
242         c=0
243         item_not_found=1
244         while item_not_found:
245             try:
246
247                 if self.current_list[c].path == filepath:
248                     self.pointer=c
249                     self.current=self.current_list[self.pointer]
250                     item_not_found=0
251                     self.do_plot(0)
252                 else:
253                     c+=1
254             except IndexError:
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':
258                     try:
259                         self.do_addtolist(filepath)
260                     except:
261                         print 'Curve file not found.'
262                         return
263                     self.current=self.current_list[-1]
264                     self.pointer=(len(current_list)-1)
265                     self.do_plot(0)
266
267                 item_not_found=0
268
269
270     def do_index(self,args):
271         '''
272         INDEX
273         Prints the index of the current curve in the list
274         -----
275         Syntax: index
276         '''
277         print self.pointer+1, 'of', len(self.current_list)
278
279
280     def help_next(self):
281         print '''
282 NEXT
283 Go the next curve in the playlist.
284 If we are at the last curve, we come back to the first.
285 -----
286 Syntax: next, n
287         '''
288     def do_next(self,args):
289         try:
290             self.current.curve.close_all()
291         except:
292             print 'No curve file loaded, currently!'
293             print 'This should not happen, report to http://code.google.com/p/hooke'
294             return
295
296         if self.pointer == (len(self.current_list)-1):
297             self.pointer=0
298             print 'Playlist finished; back to first curve.'
299         else:
300             self.pointer+=1
301
302         self.current=self.current_list[self.pointer]
303         self.do_plot(0)
304
305
306     def help_n(self):
307         self.help_next()
308     def do_n(self,args):
309         self.do_next(args)
310
311     def help_previous(self,args):
312         print '''
313 PREVIOUS
314 Go to the previous curve in the playlist.
315 If we are at the first curve, we jump to the last.
316 -------
317 Syntax: previous, p
318     '''
319     def do_previous(self,args):
320         try:
321             self.current.curve.close_all()
322         except:
323             print 'No curve file loaded, currently!'
324             print 'This should not happen, report to http://code.google.com/p/hooke'
325             return
326         if self.pointer == 0:
327             self.pointer=(len(self.current_list)-1)
328             print 'Start of playlist; jump to last curve.'
329         else:
330             self.pointer-=1
331
332         self.current=self.current_list[self.pointer]
333         self.do_plot(args)
334
335
336     def help_p(self):
337         self.help_previous()
338     def do_p(self,args):
339         self.do_previous(args)
340
341
342 #PLOT INTERACTION COMMANDS
343 #-------------------------------
344     def help_plot(self):
345         print '''
346 PLOT
347 Plots the current force curve
348 -------
349 Syntax: plot
350         '''
351     def do_plot(self,args):
352         if self.current.identify(self.drivers) == False:
353             return
354         self.plots=self.current.curve.default_plots()
355         try:
356             self.plots=self.current.curve.default_plots()
357         except Exception, e:
358             print 'Unexpected error occurred in do_plot().'
359             print e
360             return
361
362         #apply the plotmanip functions eventually present
363         nplots=len(self.plots)
364         c=0
365         while c<nplots:
366             for function in self.plotmanip: #FIXME: something strange happens about self.plotmanip[0]
367                 self.plots[c]=function(self.plots[c], self.current)
368
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']
371
372             c+=1
373
374         self._send_plot(self.plots)
375
376     def _delta(self, set=1):
377         '''
378         calculates the difference between two clicked points
379         '''
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
387
388     def do_delta(self,args):
389         '''
390         DELTA
391
392         Measures the delta X and delta Y between two points.
393         ----
394         Syntax: delta
395         '''
396         dx,unitx,dy,unity=self._delta()
397         print str(dx)+' '+unitx
398         print str(dy)+' '+unity
399
400     def _point(self, set=1):
401         '''calculates the coordinates of a single clicked point'''
402
403         print 'Click one point'
404         point=self._measure_N_points(N=1, whatset=set)
405
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
411
412     def do_point(self,args):
413         '''
414         POINT
415
416         Returns the coordinates of a point on the graph.
417         ----
418         Syntax: point
419         '''
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)
425
426
427     def do_close(self,args=None):
428         '''
429         CLOSE
430         Closes one of the two plots. If no arguments are given, the bottom plot is closed.
431         ------
432         Syntax: close [top,bottom]
433         '''
434         if args=='top':
435             to_close=0
436         elif args=='bottom':
437             to_close=1
438         else:
439             to_close=1
440
441         close_plot=self.list_of_events['close_plot']
442         wx.PostEvent(self.frame, close_plot(to_close=to_close))
443
444     def do_show(self,args=None):
445         '''
446         SHOW
447         Shows both plots.
448         '''
449         show_plots=self.list_of_events['show_plots']
450         wx.PostEvent(self.frame, show_plots())
451
452
453
454     #PLOT EXPORT AND MANIPULATION COMMANDS
455     def help_export(self):
456         print '''
457 EXPORT
458 Saves the current plot as an image file
459 ---------------
460 Syntax: export [filename] {plot to export}
461
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.
464
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)
466         '''
467     def do_export(self,args):
468         #FIXME: the bottom plot doesn't have the title
469
470         dest=0
471
472         if len(args)==0:
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? ')
476         else:
477             args=args.split()
478             name=args[0]
479             if len(args) > 1:
480                 dest=int(args[1])
481
482         export_image=self.list_of_events['export_image']
483         wx.PostEvent(self.frame, export_image(name=name, dest=dest))
484
485
486     def help_txt(self):
487         print '''
488 TXT
489 Saves the current curve as a text file
490 Columns are, in order:
491 X1 , Y1 , X2 , Y2 , X3 , Y3 ...
492
493 -------------
494 Syntax: txt [filename] {plot to export}
495         '''
496     def do_txt(self,args):
497
498         def transposed2(lists, defval=0):
499             '''
500             transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
501             elements
502             (by Zoran Isailovski on the Python Cookbook online)
503             '''
504             if not lists: return []
505             return map(lambda *row: [elem or defval for elem in row], *lists)
506
507         whichplot=0
508         args=args.split()
509         if len(args)==0:
510             filename=linp.safeinput('Filename?',[self.current.path+'.txt'])
511         else:
512             filename=linp.checkalphainput(args[0],self.current.path+'.txt',[])
513             try:
514                 whichplot=int(args[1])
515             except:
516                 pass
517
518         try:
519             outofplot=self.plots[whichplot].vectors
520         except:
521             print "Plot index out of range."
522             return 0
523
524         columns=[]     
525         for dataset in self.plots[whichplot].vectors:
526             for i in range(0,len(dataset)):
527                 columns.append([])
528                 for value in dataset[i]:
529                     columns[-1].append(str(value))
530
531         rows=transposed2(columns, 'nan')
532         rows=[' , '.join(item) for item in rows]
533         text='\n'.join(rows)
534
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')
539         txtfile.write(text)
540         txtfile.close()
541
542
543     #LOGGING, REPORTING, NOTETAKING
544
545
546     def do_note_old(self,args):
547         '''
548         NOTE_OLD
549         **deprecated**: Use note instead. Will be removed in 0.9
550
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
554         ---------------
555         Syntax: note_old [anything]
556
557         '''
558         if args=='':
559             print self.current_list[self.pointer].notes
560         else:
561             #bypass UnicodeDecodeError troubles
562             try:
563                 args=args.decode('ascii')
564             except:
565                 args=args.decode('ascii','ignore')
566                 if len(args)==0:
567                     args='?'
568
569             self.current_list[self.pointer].notes=args
570         self.notes_saved=0
571
572
573     def do_note(self,args):
574         '''
575         NOTE
576
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.
580         ---------------
581         Syntax: note_old [anything]
582
583         '''
584         if args=='':
585             print self.current_list[self.pointer].notes
586         else:
587             if self.notes_filename == None:
588                 if not os.path.exists(os.path.realpath('output')):
589                     os.mkdir('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')
594                 f.write(title_line)
595                 f.close()
596
597             #bypass UnicodeDecodeError troubles
598             try:
599                args=args.decode('ascii')
600             except:
601                args=args.decode('ascii','ignore')
602                if len(args)==0:
603                    args='?'
604             self.current_list[self.pointer].notes=args
605
606             f=open(self.notes_filename,'a+')
607             note_string=(self.current.path+'  |  '+self.current.notes+'\n')
608             f.write(note_string)
609             f.close()
610
611     def help_notelog(self):
612         print '''
613 NOTELOG
614 Writes a log of the notes taken during the session for the current
615 playlist
616 --------------
617 Syntax notelog [filename]
618 '''
619     def do_notelog(self,args):
620
621         if len(args)==0:
622             args=linp.safeinput('Notelog filename?',['notelog.txt'])
623
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
631
632         try:
633             f=open(args,'a+')
634             f.write(note_lines)
635             f.close()
636         except IOError, (ErrorNumber, ErrorMessage):
637             print 'Error: notes cannot be saved. Catched exception:'
638             print ErrorMessage
639
640         self.notes_saved=1
641
642     def help_copylog(self):
643         print '''
644 COPYLOG
645 Moves the annotated curves to another directory
646 -----------
647 Syntax copylog [directory]
648         '''
649     def do_copylog(self,args):
650
651         if len(args)==0:
652             args=linp.safeinput('Destination directory?')  #TODO default
653
654         mydir=os.path.abspath(args)
655         if not os.path.isdir(mydir):
656             print 'Destination is not a directory.'
657             return
658
659         for item in self.current_list:
660             if len(item.notes)>0:
661                 try:
662                     shutil.copy(item.path, mydir)
663                 except (OSError, IOError):
664                     print 'Cannot copy file. '+item.path+' Perhaps you gave me a wrong directory?'
665
666 #OUTLET management
667 #-----------------
668     def do_outlet_show(self,args):
669         '''OUTLET_SHOW
670         ---------
671         Shows current content of outlet with index for reference
672         '''
673         self.outlet.printbuf()
674
675     def do_outlet_undo(self, args):
676         '''OUTLET_UNDO
677         ---------
678         Eliminates last entry in outlet
679         '''
680         print 'Erasing last entry'
681         self.outlet.pop()
682
683     def do_outlet_delete(self, args):
684         '''OUTLET_DELETE
685         Eliminates a particular entry from outlet
686         Syntax: outlet_delete n
687         '''
688         if len(args)==0:
689             print 'Index needed!, use outlet_show to know it'
690         else:
691             self.outlet.delete(args)
692
693 if __name__ == '__main__':
694     mycli=HookeCli(0)
695     mycli.cmdloop()