Added modular directory structure.
[hooke.git] / hooke / libhooke.py
1 #!/usr/bin/env python
2 '''
3 libhooke.py
4
5 General library of internal objects and utilities for Hooke.
6
7 Copyright (C) 2006 Massimo Sandal (University of Bologna, Italy).
8 With algorithms contributed by Francesco Musiani (University of Bologna, Italy)
9
10 This program is released under the GNU General Public License version 2.
11 '''
12
13 import scipy
14 import numpy
15 import xml.dom.minidom
16 import os
17 import os.path
18 import string
19 import csv
20
21 from . import libhookecurve as lhc
22
23 HOOKE_VERSION=['0.8.3_devel', 'Seinei', '2008-04-16']
24 WX_GOOD=['2.6','2.8']
25
26 class PlaylistXML(object):
27         '''
28         This module allows for import/export of an XML playlist into/out of a list of HookeCurve objects
29         '''
30
31         def __init__(self):
32
33             self.playlist=None #the DOM object representing the playlist data structure
34             self.playpath=None #the path of the playlist XML file
35             self.plaything=None
36             self.hidden_attributes=['curve'] #This list contains hidden attributes that we don't want to go into the playlist.
37
38         def export(self, list_of_hooke_curves, generics):
39             '''
40             Creates an initial playlist from a list of files.
41             A playlist is an XML document with the following syntaxis:
42             <playlist>
43             <element path="/my/file/path/"/ attribute="attribute">
44             <element path="...">
45             </playlist>
46             '''
47
48             #create the output playlist, a simple XML document
49             impl=xml.dom.minidom.getDOMImplementation()
50             #create the document DOM object and the root element
51             newdoc=impl.createDocument(None, "playlist",None)
52             top_element=newdoc.documentElement
53
54             #save generics variables
55             playlist_generics=newdoc.createElement("generics")
56             top_element.appendChild(playlist_generics)
57             for key in generics.keys():
58                 newdoc.createAttribute(key)
59                 playlist_generics.setAttribute(key,str(generics[key]))
60
61             #save curves and their attributes
62             for item in list_of_hooke_curves:
63                 #playlist_element=newdoc.createElement("curve")
64                 playlist_element=newdoc.createElement("element")
65                 top_element.appendChild(playlist_element)
66                 for key in item.__dict__:
67                     if not (key in self.hidden_attributes):
68                         newdoc.createAttribute(key)
69                         playlist_element.setAttribute(key,str(item.__dict__[key]))
70
71             self.playlist=newdoc
72
73         def load(self,filename):
74             '''
75             loads a playlist file
76             '''
77             myplay=file(filename)
78             self.playpath=filename
79
80             #the following 3 lines are needed to strip newlines. otherwise, since newlines
81             #are XML elements too (why?), the parser would read them (and re-save them, multiplying
82             #newlines...)
83             #yes, I'm an XML n00b
84             the_file=myplay.read()
85             the_file_lines=the_file.split('\n')
86             the_file=''.join(the_file_lines)
87
88             self.playlist=xml.dom.minidom.parseString(the_file)
89
90             #inner parsing functions
91             def handlePlaylist(playlist):
92                 list_of_files=playlist.getElementsByTagName("element")
93                 generics=playlist.getElementsByTagName("generics")
94                 return handleFiles(list_of_files), handleGenerics(generics)
95
96             def handleGenerics(generics):
97                 generics_dict={}
98                 if len(generics)==0:
99                     return generics_dict
100
101                 for attribute in generics[0].attributes.keys():
102                     generics_dict[attribute]=generics[0].getAttribute(attribute)
103                 return generics_dict
104
105             def handleFiles(list_of_files):
106                 new_playlist=[]
107                 for myfile in list_of_files:
108                     #rebuild a data structure from the xml attributes
109                     the_curve=lhc.HookeCurve(
110                         os.path.join(os.path.dirname(self.playpath),
111                                      myfile.getAttribute('path')))
112                     for attribute in myfile.attributes.keys():
113                         #extract attributes for the single curve
114                         if attribute == 'path':
115                             continue # we already added this attribute
116                         the_curve.__dict__[attribute]=myfile.getAttribute(attribute)
117                     new_playlist.append(the_curve)
118
119                 return new_playlist #this is the true thing returned at the end of this function...(FIXME: clarity)
120
121             return handlePlaylist(self.playlist)
122
123
124         def save(self,output_filename):
125             '''
126             saves the playlist in a XML file.
127             '''
128             try:
129                 outfile=file(output_filename,'w')
130             except IOError:
131                 print 'libhooke.py : Cannot save playlist. Wrong path or filename'
132                 return
133
134             self.playlist.writexml(outfile,indent='\n')
135             outfile.close()
136
137 def config_file_path(filename, config_dir=None):
138     if config_dir == None:
139         config_dir = os.path.abspath(
140             os.path.join(os.path.dirname(os.path.dirname(__file__)), 'conf'))
141     return os.path.join(config_dir, filename)
142
143 class HookeConfig(object):
144     '''
145     Handling of Hooke configuration file
146
147     Mostly based on the simple-yet-useful examples of the Python Library Reference
148     about xml.dom.minidom
149
150     FIXME: starting to look a mess, should require refactoring
151     '''
152
153     def __init__(self, config_dir=None):
154         self.config={}
155         self.config['install']={}
156         self.config['plugins']=[]
157         self.config['drivers']=[]
158         self.config['plotmanips']=[]
159         self.config_dir = config_dir
160
161     def load_config(self, filename):
162         myconfig=file(config_file_path(filename, config_dir=self.config_dir))
163
164         #the following 3 lines are needed to strip newlines. otherwise, since newlines
165         #are XML elements too, the parser would read them (and re-save them, multiplying
166         #newlines...)
167         #yes, I'm an XML n00b
168         the_file=myconfig.read()
169         the_file_lines=the_file.split('\n')
170         the_file=''.join(the_file_lines)
171
172         self.config_tree=xml.dom.minidom.parseString(the_file)
173
174         def getText(nodelist):
175             #take the text from a nodelist
176             #from Python Library Reference 13.7.2
177             rc = ''
178             for node in nodelist:
179                 if node.nodeType == node.TEXT_NODE:
180                     rc += node.data
181             return rc
182
183         def handleConfig(config):
184             install_elements=config.getElementsByTagName("install")
185             display_elements=config.getElementsByTagName("display")
186             plugins_elements=config.getElementsByTagName("plugins")
187             drivers_elements=config.getElementsByTagName("drivers")
188             defaultlist_elements=config.getElementsByTagName("defaultlist")
189             plotmanip_elements=config.getElementsByTagName("plotmanips")
190             handleInstall(install_elements)
191             handleDisplay(display_elements)
192             handlePlugins(plugins_elements)
193             handleDrivers(drivers_elements)
194             handleDefaultlist(defaultlist_elements)
195             handlePlotmanip(plotmanip_elements)
196
197         def handleInstall(install_elements):
198             for install in install_elements:
199                 for node in install.childNodes:
200                     if node.nodeType == node.TEXT_NODE:
201                         continue
202                     path = os.path.abspath(getText(node.childNodes).strip())
203                     self.config['install'][str(node.tagName)] = path
204
205         def handleDisplay(display_elements):
206             for element in display_elements:
207                 for attribute in element.attributes.keys():
208                     self.config[attribute]=element.getAttribute(attribute)
209
210         def handlePlugins(plugins):
211             for plugin in plugins[0].childNodes:
212                 try:
213                     self.config['plugins'].append(str(plugin.tagName))
214                 except: #if we allow fancy formatting of xml, there is a text node, so tagName fails for it...
215                     pass
216         #FIXME: code duplication
217         def handleDrivers(drivers):
218             for driver in drivers[0].childNodes:
219                 try:
220                     self.config['drivers'].append(str(driver.tagName))
221                 except: #if we allow fancy formatting of xml, there is a text node, so tagName fails for it...
222                     pass
223
224         def handlePlotmanip(plotmanips):
225             for plotmanip in plotmanips[0].childNodes:
226                 try:
227                     self.config['plotmanips'].append(str(plotmanip.tagName))
228                 except: #if we allow fancy formatting of xml, there is a text node, so tagName fails for it...
229                     pass
230
231         def handleDefaultlist(defaultlist):
232             '''
233             default playlist
234             '''
235             dflist=getText(defaultlist[0].childNodes)
236             self.config['defaultlist']=dflist.strip()
237
238         handleConfig(self.config_tree)
239         #making items in the dictionary more machine-readable
240         for item in self.config.keys():
241             try:
242                 self.config[item]=float(self.config[item])
243             except TypeError: #we are dealing with a list, probably. keep it this way.
244                 try:
245                     self.config[item]=eval(self.config[item])
246                 except: #not a list, not a tuple, probably a string?
247                     pass
248             except ValueError: #if we can't get it to a number, it must be None or a string
249                 if string.lower(self.config[item])=='none':
250                     self.config[item]=None
251                 else:
252                     pass
253
254         return self.config
255
256
257     def save_config(self, config_filename):
258         print 'Not Implemented.'
259         pass
260
261
262 class ClickedPoint(object):
263     '''
264     this class defines what a clicked point on the curve plot is
265     '''
266     def __init__(self):
267
268         self.is_marker=None #boolean ; decides if it is a marker
269         self.is_line_edge=None #boolean ; decides if it is the edge of a line (unused)
270         self.absolute_coords=(None,None) #(float,float) ; the absolute coordinates of the clicked point on the graph
271         self.graph_coords=(None,None) #(float,float) ; the coordinates of the plot that are nearest in X to the clicked point
272         self.index=None #integer ; the index of the clicked point with respect to the vector selected
273         self.dest=None #0 or 1 ; 0=top plot 1=bottom plot
274
275
276     def find_graph_coords_old(self, xvector, yvector):
277         '''
278         Given a clicked point on the plot, finds the nearest point in the dataset (in X) that
279         corresponds to the clicked point.
280         OLD & DEPRECATED - to be removed
281         '''
282
283         #FIXME: a general algorithm using min() is needed!
284         print '---DEPRECATED FIND_GRAPH_COORDS_OLD---'
285         best_index=0
286         best_dist=10**9 #should be more than enough given the scale
287
288         for index in scipy.arange(1,len(xvector),1):
289             dist=((self.absolute_coords[0]-xvector[index])**2)+(100*((self.absolute_coords[1]-yvector[index])))**2
290                         #TODO, generalize? y coordinate is multiplied by 100 due to scale differences in the plot
291             if dist<best_dist:
292                 best_index=index
293                 best_dist=dist
294
295         self.index=best_index
296         self.graph_coords=(xvector[best_index],yvector[best_index])
297         return
298
299     def find_graph_coords(self,xvector,yvector):
300         '''
301         Given a clicked point on the plot, finds the nearest point in the dataset that
302         corresponds to the clicked point.
303         '''
304         dists=[]
305         for index in scipy.arange(1,len(xvector),1):
306             dists.append(((self.absolute_coords[0]-xvector[index])**2)+((self.absolute_coords[1]-yvector[index])**2))
307
308         self.index=dists.index(min(dists))
309         self.graph_coords=(xvector[self.index],yvector[self.index])
310 #-----------------------------------------
311 #CSV-HELPING FUNCTIONS
312
313 def transposed2(lists, defval=0):
314     '''
315     transposes a list of lists, i.e. from [[a,b,c],[x,y,z]] to [[a,x],[b,y],[c,z]] without losing
316     elements
317     (by Zoran Isailovski on the Python Cookbook online)
318     '''
319     if not lists: return []
320     return map(lambda *row: [elem or defval for elem in row], *lists)
321
322 def csv_write_dictionary(f, data, sorting='COLUMNS'):
323     '''
324     Writes a CSV file from a dictionary, with keys as first column or row
325     Keys are in "random" order.
326
327     Keys should be strings
328     Values should be lists or other iterables
329     '''
330     keys=data.keys()
331     values=data.values()
332     t_values=transposed2(values)
333     writer=csv.writer(f)
334
335     if sorting=='COLUMNS':
336         writer.writerow(keys)
337         for item in t_values:
338             writer.writerow(item)
339
340     if sorting=='ROWS':
341         print 'Not implemented!' #FIXME: implement it.
342
343
344 #-----------------------------------------
345
346 def debug():
347     '''
348     debug stuff from latest rewrite of hooke_playlist.py
349     should be removed sooner or later (or substituted with new debug code!)
350     '''
351     confo=HookeConfig()
352     print confo.load_config('hooke.conf')
353
354 if __name__ == '__main__':
355     debug()