Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retrieve GBIF IDs for species within a clade #32

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Based on the suggestion at https://keepachangelog.com/en/1.0.0/.

## [Unreleased]
- Upgraded NPM packages with `npm upgrade`.
- Added a "demo mode", which loads and reasons over the example file.
- Adds support for retrieving the list of all species within the clade
from the Open Tree Resolver, and then to retrieve the list of GBIF IDs
for those species.

## [0.2.0] - 2020-05-19
- Added support for gzipping the ontology before upload to the reasoner.
Expand Down
259 changes: 246 additions & 13 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
:reasoningResults="reasoningResults"
:nodesByID="nodesByID"
:unknownOttIdReasons="unknownOttIdReasons"
:speciesByNodeId="speciesByNodeId"
/>
</div>
<div class="card-footer">
Expand All @@ -35,9 +36,12 @@
Add phyloreferences from example
</button>
<div class="dropdown-menu" aria-labelledby="addFromExamples">
<a href="javascript:;" class="dropdown-item" v-for="example of exampleJSONLDURLs" v-bind:key="example.url" @click="loadJSONLDFromURL(example.url)">
{{example.title}}
</a>
<a
href="javascript:;"
class="dropdown-item"
v-for="example of exampleJSONLDURLs"
:key="example.url"
@click="loadJSONLDFromURL(example.url)">{{example.title}}</a>
</div>
</div>
<div class="btn-group ml-2" role="group" area-label="Actions on phyloreferences">
Expand Down Expand Up @@ -134,13 +138,89 @@
<button
class="btn btn-secondary"
href="javascript:;"
@click="downloadSpeciesForAllPhylorefs()"
>
Download species for all phylorefs
</button>

<button
class="btn btn-info"
href="javascript:;"
@click="downloadAsJSONLD()"
>
Download as ontology
</button>
</div>
</div>
</div>

<!-- Display all the species for a particular clade. -->
<div
v-for="(selectedPhyloref, phylorefIndex) of phylorefs"
:key="selectedPhyloref['@id'] || selectedPhyloref.label || ('phyloref_index_' + phylorefIndex)"
class="card border-dark mt-2"
>
<h5 :id="'species_in_' + selectedPhyloref.label" class="card-header">
Species in clade in {{selectedPhyloref.label || 'unlabeled phyloref'}}
</h5>
<div class="card-body">
<form>
<div class="form-group row" v-if="getNodeIdForPhyloref(selectedPhyloref)" >
<div class="col-md-3 control-label">
Resolved to:
</div>
<div class="col-md-9 input-group">
<a target="_blank" :href="'https://tree.opentreeoflife.org/opentree/@' + getNodeIdForPhyloref(selectedPhyloref)">{{getNodeIdForPhyloref(selectedPhyloref)}}</a>
</div>
</div>

<div class="form-group row" v-if="getNodeIdForPhyloref(selectedPhyloref)">
<div class="col-md-3 control-label">
Included species:
</div>
<div class="col-md-9 input-group">
<table border="1">
<thead>
<th>Node ID</th>
<th>Name</th>
<th>GBIF Species ID</th>
<th>GBIF occurrence count</th>
</thead>
<tbody>
<tr
v-for="nodeId in selectedPhyloref.species"
:key="(selectedPhyloref['@id'] || selectedPhyloref.label || '') + '_' + nodeId"
>
<td>{{nodeId}}</td>
<td>{{speciesByNodeId[nodeId].name}}</td>
<td v-if="gbifBySpeciesName && speciesByNodeId[nodeId] && speciesByNodeId[nodeId].name && gbifBySpeciesName[speciesByNodeId[nodeId].name]">
<a
v-for="speciesId in gbifBySpeciesName[speciesByNodeId[nodeId].name].speciesKey"
:key="'species_id_' + speciesId"
target="_blank"
:href="'http://gbif.org/species/' + speciesId"
>{{speciesId}}</a>
</td>
<td v-if="gbifBySpeciesName && speciesByNodeId[nodeId] && speciesByNodeId[nodeId].name && gbifBySpeciesName[speciesByNodeId[nodeId].name]">{{gbifBySpeciesName[speciesByNodeId[nodeId].name].count}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</form>
</div>
<div class="card-footer" v-if="getNodeIdForPhyloref(selectedPhyloref)">
<div class="btn-group" role="group" area-label="Query biodiversity databases">
<button
class="btn btn-primary"
href="javascript:;"
@click="downloadFromGBIF(selectedPhyloref)"
>
Query information from GBIF
</button>
</div>
</div>
</div>
</div>

<!-- All modals are included here -->
Expand Down Expand Up @@ -208,6 +288,17 @@ export default {
unknownOttIds: [],
unknownOttIdReasons: {},

// Currently selected phyloref.
selectedPhyloref: undefined,

// Contains information on the species inside a node ID.
// Structure:
// speciesByNodeId[nodeId] = [ { "nodeId": "12345", "name": "Homo sapiens", "rank": "species", ... } ]
speciesByNodeId: {},

// Contains information on GBIF by species name.
gbifBySpeciesName: {},

// URL to Phyx context JSON.
PHYX_CONTEXT_JSON: "http://www.phyloref.org/phyx.js/context/v0.2.0/phyx.json",

Expand Down Expand Up @@ -272,6 +363,17 @@ export default {
},
]},
},
mounted() {
/**
* If provided with a special query (#demo), start a "demo" of the
* functionality of the Open Tree Resolver by loading an example
* file, looking it up on the Open Tree of Life, and starting reasoning.
*/
if (window.location.hash == "#demo") {
this.demo()
}

},
methods: {
/*
* Methods for accessing specifiers on Phylorefs
Expand Down Expand Up @@ -308,18 +410,22 @@ export default {
* Open Tree synthetic tree methods
*/

downloadInducedSubtreeFromOToL(ottIds) {
downloadInducedSubtreeFromOToL(ottIds, callback) {
// Given a set of OTT ids, download the induced subtree from the Open Tree API.

if(ottIds.length === 0) return;

// Reset caches.
Vue.set(this, 'speciesByNodeId', {});
Vue.set(this, 'gbifBySpeciesName', {});

// Reset the unknown OTT Ids.
this.unknownOttIds = [];
this.unknownOttIdReasons = {};
Vue.set(this, 'unknownOttIds', []);
Vue.set(this, 'unknownOttIdReasons', {});

// Query the induced subtree, i.e. a tree showing the relationships between all
// these OTT ids.
jQuery.ajax({
return jQuery.ajax({
type: 'POST',
url: this.$config.OTT_API_INDUCED_SUBTREE,
data: JSON.stringify({
Expand All @@ -329,6 +435,7 @@ export default {
dataType: 'json',
success: (data) => {
this.newick = data.newick;
if (callback) callback(data.newick);
},
})
.fail(x => {
Expand Down Expand Up @@ -357,6 +464,7 @@ export default {
dataType: 'json',
success: (data) => {
this.newick = data.newick;
if(callback) callback(data.newick);
},
}).fail(x => console.log("Error accessing Open Tree induced_subtree", x));
} else {
Expand All @@ -372,7 +480,7 @@ export default {
queryOTTIds() {
// Calculate names from currently loaded specifiers.
const names = this.allSpecifiers.map(specifier => this.getScinameForSpecifier(specifier));
this.queryOTTIdsForNames({names});
return this.queryOTTIdsForNames({names});
},

queryOTTIdsForNames(options) {
Expand Down Expand Up @@ -406,12 +514,14 @@ export default {
matches: [],
};
}));
// OToL TNRS match_names has a limit of 1,000 names.
chunk(names, 999).forEach(chunk => {
// OToL TNRS match_names has a limit of 1,000 names, so we need to chunk
// our requests to it. Because of that chunking, we will create a single
// promise that is done when all the individual queries are done.
return Promise.all(chunk(names, 999).flatMap(chunk => {
options.names = chunk;
const data = JSON.stringify(options);
// Step 2. Spawn queries to OTT asking for the names.
jQuery.ajax({
return jQuery.ajax({
type: 'POST',
url: this.$config.OTT_API_TNRS_MATCH_NAMES,
data,
Expand All @@ -422,7 +532,7 @@ export default {
},
})
.fail(x => console.log("Error accessing Open Tree Taxonomy", x));
});
}));
},

/*
Expand Down Expand Up @@ -600,7 +710,7 @@ export default {
loadJSONLDFromURL(url) {
// Load phylorefs from a JSON-LD file from a given URL.

jQuery.getJSON(url)
return jQuery.getJSON(url)
.done((data) => {
this.extractPhylorefsFromJSONLD(data);
})
Expand Down Expand Up @@ -699,6 +809,129 @@ export default {
addPhyloref(jsonld);
}
},

/* Retrieving lists of species from Open Tree of Life */

getNodeIdForPhyloref(phyloref) {
// Return the URL for the Open Tree resolved node for a particular phyloreference.

if (!phyloref) return undefined;

const phylorefId = phyloref['@id'];
if(phylorefId && has(this.reasoningResults, phylorefId)) {
const node = this.nodesByID[this.reasoningResults[phylorefId][0]];
if(!node) return undefined;

const label = this.getNodeLabel(node);
if(!label) return undefined;

console.log("Finding node ID in label: ", label);

// TODO: ignore change; works the other way around too.
const matchMRCA = /^mrca.*$/.exec(label);
if(matchMRCA == null) {
const match = /^.*[_\s](.*?ott.*)$/.exec(label);
if (match == null) return undefined;
return match[1].toString();
}
console.log("Found MRCA: ", matchMRCA[0].toString());
return matchMRCA[0].toString();
}
return undefined;
},

getNodeLabel(node) {
// Return the label for a particular node.
const labels = node.labels || [];
if(labels.length == 0) return undefined;
return labels[0]; // Ignore other labels.
},

downloadSpeciesForPhyloref(phyloref) {
console.log("downloadSpeciesForPhyloref", phyloref);
const ottNodeId = this.getNodeIdForPhyloref(phyloref);
if (!ottNodeId) return;
if (has(this.speciesByNodeId, ottNodeId)) return;
if (has(phyloref, 'species')) return;
Vue.set(phyloref, 'species', []);

jQuery.post({
url: this.$config.OTT_API_SUBTREE,
contentType: 'application/json; charset=utf-8',
data: JSON.stringify({
node_id: ottNodeId,
format: 'arguson',
height_limit: -1,
}),
}).done((data) => {
console.log('Data retrieved: ', data);

const recordTaxa = (node) => {
if (!node) return;
if (node.taxon && node.taxon.rank && node.taxon.rank == "species") {
// Record this species!
this.speciesByNodeId[node.node_id] = {
node_id: node.node_id,
...node.taxon,
};
phyloref.species.push(node.node_id);
// console.log(`Setting ${node.node_id} to`, node.taxon);
}
if (node.children) node.children.forEach(recordTaxa);
};
recordTaxa(data.arguson);
console.log("Found species for phyloref: ", phyloref.species);
});

return [];
},

/* Code for querying GBIF */
downloadFromGBIF(phyloref) {
if (!phyloref.species) return;
const speciesNames = phyloref.species.map(nodeId => this.speciesByNodeId[nodeId].name);
speciesNames.forEach(speciesName => {
console.log("Querying GBIF for species name: ", speciesName);
jQuery.get({
url: this.$config.GBIF_API_OCCURRENCE,
data: {
scientificName: speciesName,
rank: 'SPECIES'
},
}).done((data) => {
console.log('Data retrieved: ', data);
Vue.set(this.gbifBySpeciesName, speciesName, {
count: data.count,
speciesKey: Array.from(new Set(data.results.map(result => result.speciesKey))),
});
});
});
},

downloadSpeciesForAllPhylorefs() {
// Helper function for downloading species on all phyloreferences
// that have been resolved on the Open Tree of Life.
this.phylorefs.forEach(phyloref => {
const phylorefId = phyloref['@id'];
if(phylorefId && has(this.reasoningResults, phylorefId)) {
this.downloadSpeciesForPhyloref(phyloref);
}
});
},

demo() {
// This demo is designed to demonstrate all the functionality of
// the Open Tree Resolver.

// TODO: add UI element to show demo loading processing.
this.loadJSONLDFromURL(this.exampleJSONLDURLs[0].url).done(() => {
this.queryOTTIds().then(() => {
this.downloadInducedSubtreeFromOToL(this.ottIdsForAllSpecifiers, () => {
this.reasonOverPhylogeny();
});
});
});
},
},
};
</script>
Expand Down
Loading