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