Skip to content

Commit fbd727a

Browse files
committed
Add API to get details of ancestors of a node
Something like this probably exists already in order to populate the "where am I" popup bar, but I have reimplemented something for Chris Haughton and Tiziana
1 parent 34e56ef commit fbd727a

File tree

2 files changed

+189
-1
lines changed

2 files changed

+189
-1
lines changed

controllers/API.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,195 @@ def otts2identifiers():
814814
headers=colname_map,
815815
ids=ret)
816816

817+
def ascend_ancestors():
818+
"""
819+
Used to return details of all the ancestors of a particular node, in particular
820+
- number of million years of each ancestor
821+
- the total number of species that are descendants on each side of the bifurcation
822+
- the name of the period/era/eon
823+
- a name (if it exists) for the ancestor
824+
825+
NB: finding the name for the other descendant group is a lot more tricky!
826+
827+
"""
828+
session.forget(response)
829+
lang = request.vars.lang or request.env.http_accept_language or 'en'
830+
response.headers["Access-Control-Allow-Origin"] = '*'
831+
832+
if "." not in request.env.path_info.split('/')[2]:
833+
request.extension = "json"
834+
response.view = request.controller + "/" + request.function + "." + request.extension
835+
836+
if request.vars.key is None:
837+
redirect(URL('error', vars=dict(
838+
code=400,
839+
text="Please use an API key (use 0 for the public API key)"
840+
)))
817841

842+
try:
843+
ott = int(request.vars.ott)
844+
except:
845+
redirect(URL('error', vars=dict(code=400, text="Invalid OTT")))
846+
847+
include_otts = request.vars.include_otts
848+
include_fake_nodes = request.vars.include_fake_nodes
849+
rows = db(db.ordered_leaves.ott == ott).select(
850+
db.ordered_leaves.id,
851+
db.ordered_leaves.parent,
852+
db.ordered_leaves.real_parent,
853+
db.ordered_leaves.name,
854+
db.ordered_leaves.extinction_date,
855+
)
856+
if rows:
857+
row = rows.first()
858+
date = row.extinction_date or 0
859+
prev_n_leaves = n_leaves = 1
860+
prev_leaf_lft = row.id
861+
else:
862+
rows = db(db.ordered_nodes.ott == ott).select(
863+
db.ordered_nodes.parent,
864+
db.ordered_nodes.real_parent,
865+
db.ordered_nodes.name,
866+
db.ordered_nodes.age,
867+
db.ordered_nodes.leaf_lft,
868+
db.ordered_nodes.leaf_rgt,
869+
)
870+
if not rows:
871+
redirect(URL('error', vars=dict(code=400, text="No matching OTT found")))
872+
row = rows.first()
873+
date = None if row.age is None else round(row.age, 1)
874+
prev_n_leaves = n_leaves = row.leaf_rgt - row.leaf_lft + 1
875+
prev_leaf_lft = row.leaf_lft
876+
877+
step = 0
878+
ancestors = []
879+
data_by_sib_node_id = {}
880+
data_by_sib_leaf_id = {}
881+
ret = dict(
882+
step=step,
883+
date_MYA=date,
884+
n_spp=n_leaves,
885+
name=row.name,
886+
)
887+
if include_otts:
888+
ret["input_ott"] = ott
889+
890+
data_by_ott = {ott: ret}
891+
data_by_name = {}
892+
prev_was_fake_node = row.real_parent < 0
893+
894+
while row.parent >= 0:
895+
rows = db(db.ordered_nodes.id == row.parent).select(
896+
db.ordered_nodes.id,
897+
db.ordered_nodes.parent,
898+
db.ordered_nodes.real_parent,
899+
db.ordered_nodes.name,
900+
db.ordered_nodes.ott,
901+
db.ordered_nodes.age,
902+
db.ordered_nodes.leaf_lft,
903+
db.ordered_nodes.leaf_rgt,
904+
)
905+
if not rows:
906+
break
907+
row = rows.first()
908+
n_leaves = row.leaf_rgt - row.leaf_lft + 1
909+
if row.real_parent < 0:
910+
# This is a fake node, inserted to break polytomies
911+
prev_was_fake_node = True
912+
prev_leaf_lft = row.leaf_lft
913+
if not include_fake_nodes:
914+
continue
915+
916+
step += 1
917+
row_data = dict(
918+
step=step,
919+
date_MYA=None if row.age is None else round(row.age, 1),
920+
n_spp=n_leaves,
921+
tracked_branch_n_spp=prev_n_leaves,
922+
)
923+
sib_n_leaves = n_leaves - prev_n_leaves # may include multiple sibs in a polytomy
924+
row_data["name"] = row.name if row.name and not row.name.endswith("_") else None
925+
row_data["vernacular"] = None
926+
if include_otts:
927+
row_data["ott"] = row.ott
928+
row_data["sib_branches"]={"n_spp": sib_n_leaves, "single_sib": True}
929+
930+
# If either the previous node on the tracked branch or on the sib branch
931+
# is a "fake node" it means that the current node is a polytomy
932+
if prev_was_fake_node and not include_fake_nodes:
933+
row_data["sib_branches"]["single_sib"] = False
934+
else:
935+
# Could be a valid node
936+
if prev_leaf_lft == row.leaf_lft:
937+
# the tracked branch is the left-hand branch from this node
938+
if sib_n_leaves > 1:
939+
data_by_sib_node_id[row.id + prev_n_leaves] = row_data
940+
else:
941+
data_by_sib_leaf_id[row.leaf_rgt] = row_data
942+
else:
943+
# the tracked branch is the right-hand branch from this node
944+
if sib_n_leaves > 1:
945+
data_by_sib_node_id[row.id + 1] = row_data
946+
else:
947+
data_by_sib_leaf_id[row.leaf_lft] = row_data
948+
949+
prev_n_leaves = n_leaves
950+
ancestors.append(row_data)
951+
if row.ott is not None:
952+
data_by_ott[row.ott] = row_data
953+
elif row.name is not None:
954+
data_by_name[row.name] = row_data
955+
prev_was_fake_node = False
956+
prev_leaf_lft = row.leaf_lft
957+
958+
# Get data on the siblings
959+
for sib_data_dict, db_table in zip(
960+
(data_by_sib_node_id, data_by_sib_leaf_id),
961+
(db.ordered_nodes, db.ordered_leaves)
962+
):
963+
if len(sib_data_dict) > 0:
964+
rows = db(db_table.id.belongs(sib_data_dict.keys())).select(
965+
db_table.id, db_table.real_parent, db_table.ott, db_table.name)
966+
if rows:
967+
for row in rows:
968+
parent_data = sib_data_dict[row.id]
969+
if row.real_parent >= 0 or include_fake_nodes:
970+
# The sib node should be included
971+
# if name ends in _ it's not scientific, but a OZ nickname
972+
if row.name and not row.name.endswith("_"):
973+
parent_data["sib_branches"]["name"] = row.name
974+
if row.ott is not None:
975+
data_by_ott[row.ott] = parent_data["sib_branches"]
976+
elif row.name is not None:
977+
data_by_name[row.name] = parent_data["sib_branches"]
978+
else:
979+
parent_data["sib_branches"]["single_sib"] = False
980+
981+
for ott, vernacular in OZfunc.get_common_names(
982+
data_by_ott.keys(),
983+
return_nulls=False,
984+
prefer_short_name=False,
985+
include_unpreferred=False,
986+
return_all=False,
987+
lang=lang,
988+
).items():
989+
data_by_ott[ott]["vernacular"] = vernacular
990+
991+
for name, vernacular in OZfunc.get_common_names(
992+
data_by_name.keys(),
993+
OTT=False,
994+
return_nulls=False,
995+
prefer_short_name=False,
996+
include_unpreferred=False,
997+
return_all=False,
998+
lang=lang,
999+
).items():
1000+
data_by_name[name]["vernacular"] = vernacular
1001+
ret["ancestors"] = ancestors
1002+
1003+
return ret
1004+
1005+
8181006
#PRIVATE FUNCTIONS
8191007

8201008

models/db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@
199199

200200
db.define_table('ordered_leaves',
201201
Field('parent', type='integer', notnull=True), #index number into ordered_nodes
202-
Field('real_parent', type='integer', notnull=True), #index number into ordered_nodes, negative if this is a polytomy
202+
Field('real_parent', type='integer', notnull=True), #index number into ordered_nodes, negative if this is a fake node
203203
Field('name', type='string', length=name_length_chars), #this is not genus and species because not everything conforms to that
204204
Field('extinction_date', type='double'), #in Ma
205205
Field('ott', type='integer'),

0 commit comments

Comments
 (0)