src/wrappers/TaxonomicUnitWrapper.js
/** Utility functions. */
const {
has,
isArray,
cloneDeep,
assign,
} = require('lodash');
/** List of OWL/RDF terms we use. */
const owlterms = require('../utils/owlterms');
/** We store the taxonomic units we extract from phylogeny labels in the Phyx Cache Manager. */
const { PhyxCacheManager } = require('../utils/PhyxCacheManager');
/** For parsing specimen identifiers. */
const { SpecimenWrapper } = require('./SpecimenWrapper');
/** For parsing scientific names. */
const { TaxonConceptWrapper } = require('./TaxonConceptWrapper');
/**
* The TaxonomicUnitWrapper wraps taxonomic units, whether on a node or being used
* as a specifier on a phyloreference. Every taxonomic unit can additionally be
* wrapped by more specific classes, such as {@link TaxonConceptWrapper} or
* {@link SpecimenWrapper}. We can determine which type it is based on its
* '@type' and whether it includes:
* - TaxonomicUnitWrapper.TYPE_TAXON_CONCEPT => {@link TaxonConceptWrapper}
* - TaxonomicUnitWrapper.TYPE_SPECIMEN => {@link SpecimenWrapper}
* - TaxonomicUnitWrapper.TYPE_APOMORPHY => reserved for future use
* - TaxonomicUnitWrapper.TYPE_PHYLOREF => reserved for future use
*
* It also contains static methods for extracting
* taxonomic units from arbitrary strings, such as phylogeny labels.
*
* Every taxonomic unit SHOULD have an rdfs:label and MAY include a dcterm:description
* to describe it in human-readable terms. It MUST include a '@type' that specifies
* what type of taxonomic unit it is.
*
* Taxonomic units may be specified with only an '@id' or a set of '@id's, which
* indicate external references.
*/
class TaxonomicUnitWrapper {
/* Types of taxonomic units we support (see documentation above). */
/** A taxon or taxon concept. */
static get TYPE_TAXON_CONCEPT() {
return TaxonConceptWrapper.TYPE_TAXON_CONCEPT;
}
/** A specimen. */
static get TYPE_SPECIMEN() {
return SpecimenWrapper.TYPE_SPECIMEN;
}
/** Wrap a taxonomic unit. */
constructor(tunit, defaultNomenCode = owlterms.NAME_IN_UNKNOWN_CODE) {
this.tunit = tunit;
this.defaultNomenCode = defaultNomenCode;
}
/**
* Normalize the specified taxonomic unit.
* @param tunit A taxonomic unit to be normalized.
*/
static normalize(tunit) {
const wrapped = new TaxonomicUnitWrapper(tunit);
if (wrapped.taxonConcept) {
return TaxonConceptWrapper.normalize(tunit);
}
if (wrapped.specimen) {
return SpecimenWrapper.normalize(tunit);
}
if (wrapped.externalReferences) {
// External references should only have an `@id`.
return tunit;
}
return tunit;
}
/**
* What type of specifier is this? This is an array that could contain multiple
* classes, but should contain one of:
* - {@link TYPE_TAXON_CONCEPT}
* - {@link TYPE_SPECIMEN}
*/
get types() {
if (!has(this.tunit, '@type')) return [];
if (isArray(this.tunit['@type'])) return this.tunit['@type'];
return [this.tunit['@type']];
}
/**
* Return this taxonomic unit if it is a taxon concept.
*/
get taxonConcept() {
if (this.types.includes(TaxonomicUnitWrapper.TYPE_TAXON_CONCEPT)) return this.tunit;
return undefined;
}
/**
* Return this taxonomic unit if it is a specimen.
*/
get specimen() {
// Only specimens have scientific names.
if (this.types.includes(TaxonomicUnitWrapper.TYPE_SPECIMEN)) return this.tunit;
return undefined;
}
/**
* Return the list of external references for this taxonomic unit.
* This is just all the '@ids' of this object.
*/
get externalReferences() {
if (!has(this.tunit, '@id')) return [];
if (isArray(this.tunit['@id'])) return this.tunit['@id'];
return [this.tunit['@id']];
}
/**
* Return the label of this taxonomic unit.
*/
get label() {
// A label or description for this TU?
if (has(this.tunit, 'label')) return this.tunit.label;
if (has(this.tunit, 'description')) return this.tunit.description;
// Is this a specimen?
if (this.specimen) {
return new SpecimenWrapper(this.specimen).label;
}
// Is this a taxon concept?
if (this.taxonConcept) {
return new TaxonConceptWrapper(this.taxonConcept).label;
}
// If its neither a specimen nor a taxon concept, just list the
// external references.
const externalReferences = this.externalReferences;
if (externalReferences.length > 0) {
return externalReferences
.map(externalRef => `<${externalRef}>`)
.join(' and ');
}
// If we don't have any properties of a taxonomic unit, return undefined.
return undefined;
}
/**
* Given a label, attempt to parse it into a taxonomic unit, whether a scientific
* name or a specimen identifier. The provided nomenclatural code is used.
*
* @return A taxonomic unit that this label could be parsed as.
*/
static fromLabel(nodeLabel, nomenCode = owlterms.NAME_IN_UNKNOWN_CODE) {
if (nodeLabel === undefined || nodeLabel === null || nodeLabel.trim() === '') return undefined;
// Rather than figuring out with this label, check to see if we've parsed
// this before.
if (PhyxCacheManager.has(`TaxonomicUnitWrapper.taxonomicUnitsFromNodeLabelCache.${nomenCode}`, nodeLabel)) {
return PhyxCacheManager.get(`TaxonomicUnitWrapper.taxonomicUnitsFromNodeLabelCache.${nomenCode}`, nodeLabel);
}
// Look for taxon concept.
const taxonConcept = TaxonConceptWrapper.fromLabel(nodeLabel, nomenCode);
// Look for specimen information.
let specimen;
if (nodeLabel.toLowerCase().startsWith('specimen ')) {
// Eliminate a 'Specimen ' prefix if it exists.
specimen = SpecimenWrapper.fromOccurrenceID(nodeLabel.substr(9));
}
let tunit;
if (taxonConcept && specimen) {
// If we have both, then treat it as a specimen that has been identified
// to a particular taxonomic name.
tunit = assign({}, taxonConcept, specimen);
tunit['@type'] = TaxonomicUnitWrapper.TYPE_SPECIMEN;
} else if (taxonConcept) {
tunit = taxonConcept;
} else if (specimen) {
tunit = specimen;
}
// Look for external references. For now, we only check to see if the entire
// nodeLabel starts with URL/URNs, but we should eventually just look for
// them inside the label.
const URL_URN_PREFIXES = [
'http://',
'https://',
'ftp://',
'sftp://',
'file://',
'urn:',
];
if (URL_URN_PREFIXES.filter(prefix => nodeLabel.startsWith(prefix)).length > 0) {
// The node label starts with something that looks like a URL!
// Treat it as an external reference.
if (tunit === undefined) tunit = {};
tunit['@id'] = nodeLabel;
}
// Finally, let's record the label we parsed to get to this tunit!
if (tunit) {
tunit.label = nodeLabel;
}
// Record in the cache
PhyxCacheManager.put(`TaxonomicUnitWrapper.taxonomicUnitsFromNodeLabelCache.${nomenCode}`, nodeLabel, tunit);
return tunit;
}
/**
* Return the JSON representation of this taxonomic unit, i.e. the object we're wrapping.
*/
get asJSON() {
return this.tunit;
}
/**
* Return this taxonomic unit as an OWL/JSON-LD object.
*/
get asJSONLD() {
const jsonld = cloneDeep(this.tunit);
// Add CDAO_TU as a type to the existing types.
if (has(this.tunit, '@type')) {
if (isArray(this.tunit['@type'])) this.tunit['@type'].push(owlterms.CDAO_TU);
}
const equivClass = this.asOWLEquivClass;
if (equivClass) {
jsonld.equivalentClass = equivClass;
}
return jsonld;
}
/**
* Return the equivalent class expression for this taxonomic unit.
*/
get asOWLEquivClass() {
if (this.types.includes(TaxonomicUnitWrapper.TYPE_TAXON_CONCEPT)) {
return new TaxonConceptWrapper(this.tunit, this.defaultNomenCode).asOWLEquivClass;
}
if (this.types.includes(TaxonomicUnitWrapper.TYPE_SPECIMEN)) {
return new SpecimenWrapper(this.specimen).asOWLEquivClass;
}
// Nothing we can do, so just ignore it.
return undefined;
}
}
module.exports = {
TaxonomicUnitWrapper,
};