1import sys
2import re
3from string import strip
4
5#sys.path.insert(0, "/home/services/software/ete3-webplugin/")
6sys.path.insert(0, "/home/jhuerta/_Devel/ete/ete31-beta/")
7from ete3 import WebTreeApplication # Required to use the webplugin
8
9from ete3 import PhyloTree, TreeStyle, faces # Required by my custom
10                                     # application
11
12# In order to extend the default WebTreeApplication, we define our own
13# WSGI function that handles URL queries
14def example_app(environ, start_response, queries):
15    asked_method = environ['PATH_INFO'].split("/")
16    URL = environ['REQUEST_URI']
17
18    # expected arguments from the URL (POST or GET method)
19    param_seqid = queries.get("seqid", [None])[0]
20    param_browser_id_start = queries.get("browser_id_start", [None])[0]
21
22    start_response('202 OK', [('content-type', 'text/plain')])
23
24    if asked_method[1]=="draw_tree":
25        if None in set([param_seqid]):
26            return "Not enough params"
27        return html_seqid_info(param_seqid)
28
29    else:
30        return "Tachan!!"
31
32# ==============================================================================
33# TREE LOADER
34#
35# This is my tree loading functions. I want the WebTreeApplication to
36# is this method to load the trees
37# ==============================================================================
38
39# Custom Tree loader
40def extract_species_code(name):
41    return str(name).split("_")[-1].strip()
42
43def my_tree_loader(tree):
44    """ This is function is used to load trees within the
45    WebTreeApplication object. """
46    t = PhyloTree(tree, sp_naming_function=extract_species_code)
47    return t
48
49# ==============================================================================
50# CUSTOM LAYOUTS
51#
52# This are my layout functions. I want the WebTreeApplication to use
53# them for rendering trees
54# ==============================================================================
55
56LEAVE_FACES = [] # Global var that stores the faces that are rendered
57                 # by the layout function
58def main_layout(node):
59    ''' Main layout function. It controls what is shown in tree
60    images. '''
61
62    # Add faces to leaf nodes. This allows me to add the faces from
63    # the global variable LEAVE_FACES, which is set by the application
64    # controler according to the arguments passed through the URL.
65    if node.is_leaf():
66
67        for f, fkey, pos in LEAVE_FACES:
68            if hasattr(node, fkey):
69                faces.add_face_to_node(f, node, column=pos, position="branch-right")
70    else:
71        # Add special faces on collapsed nodes
72        if hasattr(node, "hide") and int(node.hide) == 1:
73            node.img_style["draw_descendants"]= False
74            collapsed_face = faces.TextFace(\
75                " %s collapsed leaves." %len(node), \
76                    fsize=10, fgcolor="#444", ftype="Arial")
77            faces.add_face_to_node(collapsed_face, node, 0)
78        else:
79            node.img_style["draw_descendants"] = True
80
81
82    # Set node aspect. This controls which node features are used to
83    # control the style of the tree. You can add or modify this
84    # features, as well as their behaviour
85    if node.is_leaf():
86        node.img_style["shape"] = "square"
87        node.img_style["size"] = 4
88    else:
89        node.img_style["size"] = 8
90        node.img_style["shape"] = "sphere"
91
92    # Evoltype: [D]uplications, [S]peciations or [L]osess.
93    if hasattr(node,"evoltype"):
94        if node.evoltype == 'D':
95            node.img_style["fgcolor"] = "#1d176e"
96            node.img_style["hz_line_color"] = "#1d176e"
97            node.img_style["vt_line_color"] = "#1d176e"
98        elif node.evoltype == 'S':
99            node.img_style["fgcolor"] = "#FF0000"
100            node.img_style["line_color"] = "#FF0000"
101        elif node.evoltype == 'L':
102            node.img_style["fgcolor"] = "#777777"
103            node.img_style["vt_line_color"] = "#777777"
104            node.img_style["hz_line_color"] = "#777777"
105            node.img_style["line_type"] = 1
106    # If no evolutionary information, set a default style
107    else:
108        node.img_style["fgcolor"] = "#000000"
109        node.img_style["vt_line_color"] = "#000000"
110        node.img_style["hz_line_color"] = "#000000"
111
112    # Parse node features features and conver them into styles. This
113    # must be done like this, since current ete version does not allow
114    # modifying style outside the layout function.
115    if hasattr(node, "bsize"):
116        node.img_style["size"]= int(node.bsize)
117
118    if hasattr(node, "shape"):
119        node.img_style["shape"]= node.shape
120
121    if hasattr(node, "bgcolor"):
122        node.img_style["bgcolor"]= node.bgcolor
123
124    if hasattr(node, "fgcolor"):
125        node.img_style["fgcolor"]= node.fgcolor
126
127
128# ==============================================================================
129# Checker function definitions:
130#
131# All checker actions must receive a node instance as unique argument
132# and return True (node passes the filters) or False (node does not
133# passes the filters).
134#
135# ==============================================================================
136
137can_expand = lambda node: not node.is_leaf() and (hasattr(node, "hide") and node.hide==True)
138can_collapse = lambda node: not node.is_leaf() and (not hasattr(node, "hide") or node.hide==False)
139is_leaf = lambda node: node.is_leaf()
140is_not_leaf = lambda node: not node.is_leaf()
141
142# ==============================================================================
143# Handler function definitions:
144#
145# All action handler functions must receive a node instance as unique
146# argument. Returns are ignored.
147#
148# Note that there is a special action handler designed for searches
149# within the tree. Handler receives node and searched term.
150#
151# ==============================================================================
152
153def collapse(node):
154    node.add_feature("hide", 1)
155    node.add_feature("bsize", 25)
156    node.add_feature("shape", "sphere")
157    node.add_feature("fgcolor", "#bbbbbb")
158
159def expand(node):
160    try:
161        node.del_feature("hide")
162        node.del_feature("bsize")
163        node.del_feature("shape")
164        node.del_feature("fgcolor")
165    except (KeyError, AttributeError):
166        pass
167
168def swap_branches(node):
169    node.children.reverse()
170
171def set_red(node):
172    node.add_feature("fgcolor", "#ff0000")
173    node.add_feature("bsize", 40)
174    node.add_feature("shape", "sphere")
175
176def set_bg(node):
177    node.add_feature("bgcolor", "#CEDBC4")
178
179def set_as_root(node):
180    node.get_tree_root().set_outgroup(node)
181
182def phylomedb_clean_layout(node):
183    phylomedb_layout(node)
184    node.img_style["size"]=0
185
186def search_by_feature(tree, search_term):
187    ''' Special action '''
188    attr, term = search_term.split("::")
189    if not term:
190        return None
191    elif attr == "clean" and term == "clean":
192        for n in tree.traverse():
193            try:
194                n.del_feature("bsize")
195                n.del_feature("shape")
196                n.del_feature("fgcolor")
197            except (KeyError, AttributeError):
198                pass
199    else:
200        for n in tree.traverse():
201            if hasattr(n, attr) and \
202                    re.search(term,  str(getattr(n, attr)), re.IGNORECASE):
203                n.add_feature("bsize", 16)
204                n.add_feature("shape", "sphere")
205                n.add_feature("fgcolor", "#BB8C2B")
206
207
208# ==============================================================================
209# HTML generators
210#
211# Actions will be automatically added to the popup menus and attached
212# to the action handler function. However, if just want to add
213# informative items to the popup menu or external actions not
214# associated to any handler, you can overwrite the default html
215# generator of each action.
216#
217# html generators receive all information attached to the node and
218# action in 5 arguments:
219#
220# * aindex: index of the action associated to this html generator
221#
222# * nodeid: id of the node to which action is attached
223#
224# * treeid: id of the tree in which node is present
225#
226# * text: the text string associated to the element that raised the
227# action (only applicable to text faces actions)
228#
229# * node: node instance in which action will be executed.
230#
231#
232# Html generator should return a text string encoding a html list
233# item:
234#
235# Example: return "<li> my text </li>"
236#
237# ==============================================================================
238
239
240def branch_info(aindex, nodeid, treeid, text, node):
241    ''' It shows some info of the node in the popup menu '''
242    return """
243           <li style="background:#eee; font-size:8pt;">
244           <div style="text-align:left;font-weight:bold;">
245            NODE ACTIONS
246           </div>
247            (<b>Branch: </b>%0.3f <b>Support:</b> %0.3f)<br>
248           </li>"""  %\
249        (node.dist, node.support)
250
251def search_in_ensmbl(aindex, nodeid, treeid, text, node):
252    return '''<li>
253              <a target="_blank" href="http://www.ensembl.org/common/Search/Results?species=all;idx=;q=%s">
254              <img src=""> Search in ensembl: %s >
255              </a>
256              </li> ''' %\
257            (node.name, node.name)
258
259def external_links_divider(aindex, nodeid, treeid, text, node):
260    ''' Used to show a separator in the popup menu'''
261    if node.is_leaf():
262        return """<li
263        style="background:#eee;font-size:8pt;"><b>External
264        links</b></li>"""
265    else:
266        return ""
267
268def topology_action_divider(aindex, nodeid, treeid, text, node):
269    return """<li style="background:#eee;"><b>Tree node actions</b></li>"""
270
271# ==============================================================================
272# TREE RENDERER
273#
274# By default, ETE will render the tree as a png image and will return
275# a simplistic HTML code to show the image and interact with
276# it. However, it is possible to wrap such functionality to preprocess
277# trees in a particular way, read extra parameters from the URL query
278# and/or produce enriched HTML applications.
279#
280# Tree renderer wrappers receive the tree object, its id, and the WSGI
281# application object. They MUST call the
282# application._get_tree_img(tree) method and return the a HTML
283# string.
284#
285# A simplistic wrapper that emulates the default WebTreeApplication
286# behaviour would be:
287#
288# def tree_renderer(tree, treeid, application):
289#    html = application._get_tree_img(treeid = treeid)
290#    return html
291#
292# ==============================================================================
293def tree_renderer(tree, treeid, application):
294    # The following part controls the features that are attched to
295    # leaf nodes and that will be shown in the tree image. Node styles
296    # are set it here, and faces are also created. The idea is the
297    # following: user can pass feature names using the URL argument
298    # "tree_features". If the feature is handled by our function and
299    # it is available in nodes, a face will be created and added to
300    # the global variable LEAVE_FACES. Remember that our layout
301    # function uses such variable to add faces to nodes during
302    # rendering.
303
304    # Extracts from URL query the features that must be drawn in the tree
305    asked_features = application.queries.get("show_features", ["name"])[0].split(",")
306    print >>sys.stderr, asked_features
307    def update_features_avail(feature_key, name, col, fsize, fcolor, prefix, suffix):
308        text_features_avail.setdefault(feature_key, [name, 0, col, fsize, fcolor, prefix, suffix])
309        text_features_avail[feature_key][1] += 1
310
311    tree.add_feature("fgcolor", "#833DB4")
312    tree.add_feature("shape", "sphere")
313    tree.add_feature("bsize", "8")
314    tree.dist = 0
315
316    # This are the features that I wanto to convert into image
317    # faces. I use an automatic function to do this. Each element in
318    # the dictionary is a list that contains the information about how
319    # to create a textFace with the feature.
320    leaves = tree.get_leaves()
321    formated_features =  {
322        # feature_name: ["Description", face column position, text_size, color, text_prefix, text_suffix ]
323        "name": ["Leaf name", len(leaves), 0, 12, "#000000", "", ""],
324        "spname": ["Species name", len(leaves), 1, 12, "#f00000", " Species:", ""],
325        }
326
327    # populates the global LEAVE_FACES variable
328    global LEAVE_FACES
329    LEAVE_FACES = []
330    unknown_faces_pos = 2
331    for fkey in asked_features:
332        if fkey in formated_features:
333            name, total, pos, size, color, prefix, suffix = formated_features[fkey]
334            f = faces.AttrFace(fkey, ftype="Arial", fsize=size, fgcolor=color, text_prefix=prefix, text_suffix=suffix)
335            LEAVE_FACES.append([f, fkey, pos])
336        else:
337            # If the feature has no associated format, let's add something standard
338            prefix = " %s: " %fkey
339            f = faces.AttrFace(fkey, ftype="Arial", fsize=10, fgcolor="#666666", text_prefix=prefix, text_suffix="")
340            LEAVE_FACES.append([f, fkey, unknown_faces_pos])
341            unknown_faces_pos += 1
342
343    text_features_avail = {}
344    for l in leaves:
345        for f in l.features:
346            if not f.startswith("_"):
347                text_features_avail.setdefault(f, 0)
348                text_features_avail[f] = text_features_avail[f] + 1
349
350    html_features = """
351      <div id="tree_features_box">
352      <div class="tree_box_header">Available tree features
353      <img src="/webplugin/close.png" onclick='$(this).closest("#tree_features_box").hide();'>
354      </div>
355      <form action='javascript: set_tree_features("", "", "");'>
356
357      """
358
359    for fkey, counter in text_features_avail.iteritems():
360        if fkey in asked_features:
361            tag = "CHECKED"
362        else:
363            tag = ""
364
365        fname = formated_features.get(fkey, [fkey])[0]
366
367        #html_features = "<tr>"
368        html_features += '<INPUT NAME="tree_feature_selector" TYPE=CHECKBOX %s VALUE="%s">%s (%s/%s leaves)</input><br> ' %\
369            (tag, fkey, fname, counter, len(leaves))
370 #       html_features += '<td><INPUT size=7 type="text"></td> <td><input size=7 type="text"></td> <td><input size=7 type="text"></td>  <td><input size=1 type="text"></td><br>'
371        #html_features += "</tr>"
372
373    html_features += """<input type="submit" value="Refresh"
374                        onclick='javascript:
375                                // This piece of js code extracts the checked features from menu and redraw the tree sending such information
376                                var allVals = [];
377                                $(this).parent().children("input[name=tree_feature_selector]").each(function(){
378                                if ($(this).is(":checked")){
379                                    allVals.push($(this).val());
380                                }});
381                                draw_tree("%s", "", "#img1", {"show_features": allVals.join(",")} );'
382                       >
383                       </form></div>""" %(treeid)
384
385    features_button = """
386     <li><a href="#" onclick='show_box(event, $(this).closest("#tree_panel").children("#tree_features_box"));'>
387     <img width=16 height=16 src="/webplugin/icon_tools.png" alt="Select Tree features">
388     </a></li>"""
389
390    download_button = """
391     <li><a href="/webplugin/tmp/%s.png" target="_blank">
392     <img width=16 height=16 src="/webplugin/icon_attachment.png" alt="Download tree image">
393     </a></li>""" %(treeid)
394
395    search_button = """
396      <li><a href="#" onclick='javascript:
397          var box = $(this).closest("#tree_panel").children("#search_in_tree_box");
398          show_box(event, box); '>
399      <img width=16 height=16 src="/webplugin/icon_search.png" alt="Search in tree">
400      </a></li>"""
401
402    clean_search_button = """
403      <li><a href="#" onclick='run_action("%s", "", %s, "clean::clean");'>
404      <img width=16 height=16 src="/webplugin/icon_cancel_search.png" alt="Clear search results">
405      </a></li>""" %\
406        (treeid, 0)
407
408    buttons = '<div id="ete_tree_buttons">' +\
409        features_button + search_button + clean_search_button + download_button +\
410        '</div>'
411
412    search_select = '<select id="ete_search_target">'
413    for fkey in text_features_avail:
414        search_select += '<option value="%s">%s</option>' %(fkey,fkey)
415    search_select += '</select>'
416
417    search_form = """
418     <div id="search_in_tree_box">
419     <div class="tree_box_header"> Search in Tree
420     <img src="/webplugin/close.png" onclick='$(this).closest("#search_in_tree_box").hide();'>
421     </div>
422     <form onsubmit='javascript:
423                     search_in_tree("%s", "%s",
424                                    $(this).closest("form").children("#ete_search_term").val(),
425                                    $(this).closest("form").children("#ete_search_target").val());'
426          action="javascript:void(0);">
427     %s
428     <input id="ete_search_term" type="text" value=""'></input>
429     <br><i>(Searches are not case sensitive and accept Perl regular expressions)</i>
430     <br>
431     </form>
432     <i> (Press ENTER to initiate the search)</i>
433     </div>
434     """ %\
435        (treeid, 0, search_select) # 0 is the action index associated
436                                   # to the search functionality. This
437                                   # means that this action is the
438                                   # first to be registered in WebApplication.
439
440
441    tree_panel_html = '<div id="tree_panel">' + search_form + html_features + buttons + '</div>'
442
443    # Now we render the tree into image and get the HTML that handles it
444    tree_html = application._get_tree_img(treeid = treeid)
445
446    # Let's return enriched HTML
447    return tree_panel_html + tree_html
448
449# ==============================================================================
450#
451# Main WSGI Application
452#
453# ==============================================================================
454
455# Create a basic ETE WebTreeApplication
456application = WebTreeApplication()
457
458# Set your temporal dir to allow web user to generate files. This two
459# paths should point to the same place, one using the absolute path in
460# your system, and the other the URL to access the same
461# directory. Note that the referred directory must be writable by the
462# webserver.
463#application.CONFIG["temp_dir"] = "/home/services/web/etetoolkit.org/webplugin/tmp/"
464application.CONFIG["temp_dir"] = "/var/www/etetoolkit.org/webplugin/tmp/"
465application.CONFIG["temp_url"] = "/webplugin/tmp/" # Relative to web site Document Root
466
467# Set the DISPLAY port that ETE should use to draw pictures. You will
468# need a X server installed in your server and allow webserver user to
469# access the display. If the X server is started by a different user
470# and www-data (usally the apache user) cannot access display, try
471# modifiying DISPLAY permisions by executing "xhost +"
472application.CONFIG["DISPLAY"] = ":0" # This is the most common
473                                     # configuration
474
475# We extend the minimum WebTreeApplication with our own WSGI
476# application
477application.set_external_app_handler(example_app)
478
479# Lets now apply our custom tree loader function to the main
480# application
481application.set_tree_loader(my_tree_loader)
482
483# And our layout as the default one to render trees
484ts = TreeStyle()
485ts.show_leaf_name = False
486ts.layout_fn.append(main_layout)
487ts.mode = "r"
488ts.branch_vertical_margin = 5
489#ts.scale = 20
490application.set_tree_style(ts)
491#application.set_default_layout_fn(main_layout)
492application.set_tree_size(None, None)
493# I want to make up how tree image in shown using a custrom tree
494# renderer that adds much more HTML code
495application.set_external_tree_renderer(tree_renderer)
496
497
498# ==============================================================================
499# ADD CUSTOM ACTIONS TO THE APPLICATION
500#
501# The function "register_action" allows to attach functionality to
502# nodes in the image. All registered accions will be shown in the
503# popup menu bound to the nodes and faces in the web image.
504#
505#
506# register_action(action_name, target_type=["node"|"face"|"layout"|"search"], \
507#                 action_handler, action_checker, html_generator_handler)
508#
509# When the Application is executed it will read your registered
510# acctions and will do the following:
511#
512# 1. Load the tree and get the image map
513#
514# 2. For each node and face in the tree, it will browse all registered
515# actions and will run the action_checker function to determine if the
516# action must be activated for such node or face
517#
518# 3. If action_checker(node) returns True, the action will be attached
519# to the context menu of that specific node or face, otherwise it will
520# be hidden.
521#
522# 4. When a click is done on a specific node, popup menus will be
523# built using their active actions. For this, ETE will use the
524# html_generator function associated to each function if
525# available. Otherwise, a popup entry will be added automatically.
526#
527# 5. When a certain action is pressed in the popup menus, the
528# action_handler function attached to the action will be executed over
529# its corresponding node, and the tree image will be refreshed.
530#
531# Special values:
532#
533#  action_checker = None : It will be interpreted as "Show allways"
534#  html_generator = None : Autogenerate html and link to action
535#  action_handler = None : Action will be ignored
536#
537# ==============================================================================
538
539# We first register the special action "search" which is attached to
540# our custom search function.
541application.register_action("", "search", search_by_feature, None, None)
542
543# Node manipulation options (bound to node items and all their faces)
544application.register_action("branch_info", "node", None, None, branch_info)
545application.register_action("<b>Collapse</b>", "node", collapse, can_collapse, None)
546application.register_action("Expand", "node", expand, can_expand, None)
547application.register_action("Highlight background", "node", set_bg, None, None)
548application.register_action("Set as root", "node", set_as_root, None, None)
549application.register_action("Swap children", "node", swap_branches, is_not_leaf, None)
550application.register_action("Pay me a compliment", "face", set_red, None, None)
551
552# Actions attached to node's content (shown as text faces)
553application.register_action("divider", "face", None, None, external_links_divider)
554
555application.register_action("Default layout", "layout", main_layout, None, None)
556application.register_action("Clean layout", "layout", main_layout, None, None)
557
558