src/wrappers/PhylorefWrapper.js
/** Used to parse timestamps for phyloref statuses. */
const moment = require('moment');
const { has, cloneDeep, uniq } = require('lodash');
const owlterms = require('../utils/owlterms');
const { TaxonomicUnitWrapper } = require('./TaxonomicUnitWrapper');
const { TaxonConceptWrapper } = require('./TaxonConceptWrapper');
const { PhylogenyWrapper } = require('./PhylogenyWrapper');
const { CitationWrapper } = require('./CitationWrapper');
/**
* PhylorefWrapper
*
*/
class PhylorefWrapper {
// Wraps a phyloreference in a PHYX model.
constructor(phyloref, phyxDefaultNomenCode = owlterms.UNKNOWN_CODE) {
// Wraps the provided phyloreference
this.phyloref = phyloref;
this.phyxDefaultNomenCode = phyxDefaultNomenCode;
}
/** Return the internal specifiers of this phyloref (if any). */
get internalSpecifiers() {
if (!has(this.phyloref, 'internalSpecifiers')) {
// If there isn't one, create an empty list so that the caller can do
// `wrappedPhyloref.internalSpecifiers.push({...})`.
this.phyloref.internalSpecifiers = [];
}
return this.phyloref.internalSpecifiers;
}
/**
* Normalize a phyloreference.
*
* @param phyloref
*/
static normalize(phyloref) {
const normalizedPhyloref = cloneDeep(phyloref);
normalizedPhyloref.internalSpecifiers = (phyloref.internalSpecifiers || [])
.map(TaxonomicUnitWrapper.normalize);
normalizedPhyloref.externalSpecifiers = (phyloref.externalSpecifiers || [])
.map(TaxonomicUnitWrapper.normalize);
return normalizedPhyloref;
}
/** Return the external specifiers of this phyloref (if any). */
get externalSpecifiers() {
if (!has(this.phyloref, 'externalSpecifiers')) {
// If there isn't one, create an empty list so that the caller can do
// `wrappedPhyloref.externalSpecifiers.push({...})`.
this.phyloref.externalSpecifiers = [];
}
return this.phyloref.externalSpecifiers;
}
get label() {
// Return a label for this phyloreference.
if (has(this.phyloref, 'label')) return this.phyloref.label;
if (has(this.phyloref, 'labels') && this.phyloref.labels.length > 0) return this.phyloref.labels[0];
if (has(this.phyloref, 'title')) return this.phyloref.title;
return undefined;
}
set label(newLabel) {
// Set a label for this phyloreference.
if (has(this.phyloref, 'label')) {
this.phyloref.label = newLabel;
} else {
// Vue.set(this.phyloref, 'label', newLabel);
this.phyloref.label = newLabel;
}
}
/** Return all the specifiers of this phyloref (if any). */
get specifiers() {
// Returns a list of all specifiers by combining the internal and external
// specifiers into a single list, with internal specifiers before
// external specifiers.
let specifiers = this.internalSpecifiers;
specifiers = specifiers.concat(this.externalSpecifiers);
return specifiers;
}
getSpecifierType(specifier) {
// For a given specifier, return a string indicating whether it is
// an 'Internal' or 'External' specifier.
if (this.internalSpecifiers.includes(specifier)) return 'Internal';
if (this.externalSpecifiers.includes(specifier)) return 'External';
return 'Specifier';
}
setSpecifierType(specifier, specifierType) {
// Change the type of a given specifier. To do this, we first need
// to determine if it was originally an internal or external
// specifier, then move it into the other list.
let index;
if (specifierType === 'Internal') {
// To set a specifier to 'Internal', we might need to delete it from the
// list of external specifiers first.
index = this.externalSpecifiers.indexOf(specifier);
if (index !== -1) this.externalSpecifiers.splice(index, 1);
// Don't add it to the list of internal specifiers if it's already there.
if (!this.internalSpecifiers.includes(specifier)) {
this.phyloref.internalSpecifiers.unshift(specifier);
}
} else if (specifierType === 'External') {
// To set a specifier to 'External', we might need to delete it from the
// list of internal specifiers first.
index = this.phyloref.internalSpecifiers.indexOf(specifier);
if (index !== -1) this.phyloref.internalSpecifiers.splice(index, 1);
// Don't add it to the list of internal specifiers if it's already there.
if (!this.phyloref.externalSpecifiers.includes(specifier)) {
this.phyloref.externalSpecifiers.unshift(specifier);
}
} else {
// Neither internal nor external? Ignore.
}
}
deleteSpecifier(specifier) {
// Since the user interface combines specifiers into a single list,
// it doesn't remember if the specifier to be deleted is internal
// or external. We delete the intended specifier from both arrays.
if (has(this.phyloref, 'internalSpecifiers') && this.phyloref.internalSpecifiers.length > 0) {
const index = this.phyloref.internalSpecifiers.indexOf(specifier);
if (index !== -1) this.phyloref.internalSpecifiers.splice(index, 1);
}
if (has(this.phyloref, 'externalSpecifiers') && this.phyloref.externalSpecifiers.length > 0) {
const index = this.phyloref.externalSpecifiers.indexOf(specifier);
if (index !== -1) this.phyloref.externalSpecifiers.splice(index, 1);
}
}
getExpectedNodeLabels(phylogeny) {
// Given a phylogeny, determine which node labels we expect this phyloref to
// resolve to. To do this, we:
// 1. Find all node labels that are case-sensitively identical
// to the phyloreference.
// 2. Find all node labels that have additionalNodeProperties with
// expectedPhyloreferenceNamed case-sensitively identical to
// the phyloreference.
const phylorefLabel = this.label;
const nodeLabels = new Set();
new PhylogenyWrapper(
phylogeny,
this.defaultNomenCode
).getNodeLabels().forEach((nodeLabel) => {
// Is this node label identical to the phyloreference name?
if (nodeLabel === phylorefLabel) {
nodeLabels.add(nodeLabel);
} else if (
has(phylogeny, 'additionalNodeProperties')
&& has(phylogeny.additionalNodeProperties, nodeLabel)
&& has(phylogeny.additionalNodeProperties[nodeLabel], 'expectedPhyloreferenceNamed')
) {
// Does this node label have an expectedPhyloreferenceNamed that
// includes this phyloreference name?
const expectedPhylorefs = phylogeny
.additionalNodeProperties[nodeLabel]
.expectedPhyloreferenceNamed;
if (expectedPhylorefs.includes(phylorefLabel)) {
nodeLabels.add(nodeLabel);
}
}
});
// Return node labels sorted alphabetically.
return Array.from(nodeLabels).sort();
}
static getStatusCURIEsInEnglish() {
// Return dictionary of all phyloref statuses in English
return {
'pso:draft': 'Draft',
'pso:final-draft': 'Final draft',
'pso:under-review': 'Under review',
'pso:submitted': 'Tested',
'pso:published': 'Published',
'pso:retracted-from-publication': 'Retracted',
};
}
getCurrentStatus() {
// Return a result object that contains:
// - status: phyloreference status as a short URI (CURIE)
// - statusInEnglish: an English representation of the phyloref status
// - intervalStart: the start of the interval
// - intervalEnd: the end of the interval
if (
has(this.phyloref, 'pso:holdsStatusInTime')
&& Array.isArray(this.phyloref['pso:holdsStatusInTime'])
&& this.phyloref['pso:holdsStatusInTime'].length > 0
) {
// If we have any pso:holdsStatusInTime entries, pick the first one and
// extract the CURIE and time interval information from it.
const lastStatusInTime = this.phyloref['pso:holdsStatusInTime'][this.phyloref['pso:holdsStatusInTime'].length - 1];
const statusCURIE = lastStatusInTime['pso:withStatus']['@id'];
// Look for time interval information
let intervalStart;
let intervalEnd;
if (has(lastStatusInTime, 'tvc:atTime')) {
const atTime = lastStatusInTime['tvc:atTime'];
if (has(atTime, 'timeinterval:hasIntervalStartDate')) intervalStart = atTime['timeinterval:hasIntervalStartDate'];
if (has(atTime, 'timeinterval:hasIntervalEndDate')) intervalEnd = atTime['timeinterval:hasIntervalEndDate'];
}
// Return result object
return {
statusCURIE,
statusInEnglish: PhylorefWrapper.getStatusCURIEsInEnglish()[statusCURIE],
intervalStart,
intervalEnd,
};
}
// If we couldn't figure out a status for this phyloref, assume it's a draft.
return {
statusCURIE: 'pso:draft',
statusInEnglish: PhylorefWrapper.getStatusCURIEsInEnglish()['pso:draft'],
};
}
getStatusChanges() {
// Return a list of status changes for a particular phyloreference
if (has(this.phyloref, 'pso:holdsStatusInTime')) {
return this.phyloref['pso:holdsStatusInTime'].map((entry) => {
const result = {};
// Create a statusCURIE convenience field.
if (has(entry, 'pso:withStatus')) {
result.statusCURIE = entry['pso:withStatus']['@id'];
result.statusInEnglish = PhylorefWrapper.getStatusCURIEsInEnglish()[result.statusCURIE];
}
// Create intervalStart/intervalEnd convenient fields
if (has(entry, 'tvc:atTime')) {
const atTime = entry['tvc:atTime'];
if (has(atTime, 'timeinterval:hasIntervalStartDate')) {
result.intervalStart = atTime['timeinterval:hasIntervalStartDate'];
result.intervalStartAsCalendar = moment(result.intervalStart).calendar();
}
if (has(atTime, 'timeinterval:hasIntervalEndDate')) {
result.intervalEnd = atTime['timeinterval:hasIntervalEndDate'];
result.intervalEndAsCalendar = moment(result.intervalEnd).calendar();
}
}
return result;
});
}
// No changes? Return an empty list.
return [];
}
setStatus(status) {
// Set the status of a phyloreference
//
// Check whether we have a valid status CURIE.
if (!has(PhylorefWrapper.getStatusCURIEsInEnglish(), status)) {
throw new TypeError(`setStatus() called with invalid status CURIE '${status}'`);
}
// See if we can end the previous interval.
const currentTime = new Date(Date.now()).toISOString();
if (!has(this.phyloref, 'pso:holdsStatusInTime')) {
// Vue.set(this.phyloref, 'pso:holdsStatusInTime', []);
this.phyloref['pso:holdsStatusInTime'] = [];
}
// Check to see if there's a previous time interval we should end.
if (
Array.isArray(this.phyloref['pso:holdsStatusInTime'])
&& this.phyloref['pso:holdsStatusInTime'].length > 0
) {
const lastStatusInTime = this.phyloref['pso:holdsStatusInTime'][this.phyloref['pso:holdsStatusInTime'].length - 1];
// if (!has(lastStatusInTime, 'tvc:atTime'))
// Vue.set(lastStatusInTime, 'tvc:atTime', {});
if (!has(lastStatusInTime, 'tvc:atTime')) {
lastStatusInTime['tvc:atTime'] = {};
}
if (!has(lastStatusInTime['tvc:atTime'], 'timeinterval:hasIntervalEndDate')) {
// If the last time entry doesn't already have an interval end date, set it to now.
lastStatusInTime['tvc:atTime']['timeinterval:hasIntervalEndDate'] = currentTime;
}
}
// Create new entry.
this.phyloref['pso:holdsStatusInTime'].push({
'@type': 'http://purl.org/spar/pso/StatusInTime',
'pso:withStatus': { '@id': status },
'tvc:atTime': {
'timeinterval:hasIntervalStartDate': currentTime,
},
});
}
/**
* Return a list of all the unique nomenclatural codes used by this phyloreference.
* The default nomenclatural code used in creating the PhylorefWrapper will be used
* for any taxonomic units that don't have any nomenclatural code set. If any
* specifiers are not taxon concepts, they will be represented in the returned
* list as owlterms.UNKNOWN_CODE.
*/
get uniqNomenCodes() {
return uniq(this.specifiers.map((specifier) => {
const taxonConcept = new TaxonomicUnitWrapper(
specifier,
this.phyxDefaultNomenCode
).taxonConcept;
if (!taxonConcept) return owlterms.UNKNOWN_CODE;
const nomenCode = new TaxonConceptWrapper(
taxonConcept,
this.phyxDefaultNomenCode
).nomenCode;
if (!nomenCode) return owlterms.UNKNOWN_CODE;
return nomenCode;
}));
}
/**
* Returns a summarized nomenclatural code for this phyloref. If all of the
* specifiers have either the same nomenclatural code or `undefined`,
* this getter will return that nomenclatural code. Otherwise, this method
* will return owlterms.UNKNOWN_CODE.
*/
get defaultNomenCode() {
// Check to see if we have a single nomenclatural code to use.
if (this.uniqNomenCodes.length === 1) return this.uniqNomenCodes[0];
// If one or more of our specifiers have no nomenclatural code (e.g. if
// they are specimens), they will show up as owlterms.UNKNOWN_CODE.
// If we have a single nomenclatural code *apart* from all the
// owlterms.UNKNOWN_CODEs, then that is still usable as a default
// nomenclatural code for this phyloreference.
const uniqNomenCodesNoUnknowns = this.uniqNomenCodes
.filter(code => code !== owlterms.UNKNOWN_CODE);
if (uniqNomenCodesNoUnknowns.length === 1) return uniqNomenCodesNoUnknowns[0];
return owlterms.UNKNOWN_CODE;
}
/**
* Create a component class for the set of internal and external specifiers provided.
* We turn this into a label (in the form `A & B ~ C V D`), which we use to ensure that
* we don't create more than one class for a particular set of internal and external
* specifiers.
* - jsonld: The JSON-LD representation of the Phyloreference this is an component class
* for. We mainly use this to retrieve its '@id'.
* - internalSpecifiers: The set of internal specifiers for this component class.
* - externalSpecifiers: The set of external specifiers for this component class.
* - equivClass: The equivalent class expression for this component class as a function
* that returns the expression as a string.
* - reusePrevious (default: true): If true, we reuse previous expressions with the
* same set of included and excluded specifiers. If false, we always generate a new
* component class for this expression.
* - parentClass: If not undefined, provides a JSON-LD definition of the class to set as the
* parent class of this component class. We only use the ['@id'].
*/
createComponentClass(
jsonld,
internalSpecifiers,
externalSpecifiers,
equivClass,
reusePrevious = true,
parentClass = undefined
) {
if (internalSpecifiers.length === 0) throw new Error('Cannot create component class without any internal specifiers');
if (internalSpecifiers.length === 1 && externalSpecifiers.length === 0) throw new Error('Cannot create component class with a single internal specifiers and no external specifiers');
/* Generate a label that represents this component class. */
// By default, taxonomic unit labels don't include the nomenclatural code.
// However, we should include that here in order to distinguish between
// taxonomic names in different taxonomic codes. This method generates that
// name for a specifier.
const outerThis = this;
function generateSpecifierName(specifier) {
const wrapped = new TaxonomicUnitWrapper(specifier, outerThis.defaultNomenCode);
if (!wrapped) return '(error)';
if (wrapped.taxonConcept) {
const nomenCodeDetails = new TaxonConceptWrapper(wrapped.taxonConcept).nomenCodeDetails;
if (nomenCodeDetails) return `${wrapped.label} (${nomenCodeDetails.shortName})`;
}
return wrapped.label;
}
// Start with the internal specifiers, concatenated with '&'.
const internalSpecifierLabel = internalSpecifiers
.map(generateSpecifierName)
.sort()
.join(' & ');
let componentClassLabel = `(${internalSpecifierLabel}`;
if (externalSpecifiers.length === 0) {
componentClassLabel += ')';
} else {
// Add the external specifiers, concatenated with 'V'.
const externalSpecifierLabel = externalSpecifiers
.map(generateSpecifierName)
.sort()
.join(' V ');
componentClassLabel += ` ~ ${externalSpecifierLabel})`;
}
// process.stderr.write(`component class label: ${componentClassLabel}\n`);
// TODO We need to replace this with an actual object-based comparison,
// rather than trusting the labels to tell us everything.
if (reusePrevious && has(this.componentClassesByLabel, componentClassLabel)) {
// If we see the same label again, return the previously defined component class.
return { '@id': this.componentClassesByLabel[componentClassLabel]['@id'] };
}
// Create a new component class for this set of internal and external specifiers.
this.componentClassCount += 1;
const componentClass = {};
componentClass['@id'] = `${jsonld['@id']}_component${this.componentClassCount}`;
// process.stderr.write(`Creating new componentClass with id: ${componentClass['@id']}`);
componentClass['@type'] = 'owl:Class';
componentClass.label = componentClassLabel;
componentClass.equivalentClass = equivClass;
if (externalSpecifiers.length > 0) componentClass.subClassOf = ['phyloref:PhyloreferenceUsingMaximumClade'];
else componentClass.subClassOf = ['phyloref:PhyloreferenceUsingMinimumClade'];
if (parentClass) {
componentClass.subClassOf.push({
'@id': parentClass['@id'],
});
}
// Save it in the cache for later usage.
this.componentClassesByLabel[componentClassLabel] = componentClass;
// The first time we create a componentClass, we include it into the logical
// expression directly. On subsequent calls, we'll only return the `@id`
// (see above).
return componentClass;
}
getIncludesRestrictionForTU(tu) {
return {
'@type': 'owl:Restriction',
onProperty: 'phyloref:includes_TU',
someValuesFrom: new TaxonomicUnitWrapper(tu, this.defaultNomenCode).asOWLEquivClass,
};
}
/**
* Return an OWL restriction for the most recent common ancestor (MRCA)
* of two taxonomic units.
*/
getMRCARestrictionOfTwoTUs(tu1, tu2) {
return {
'@type': 'owl:Restriction',
onProperty: 'obo:CDAO_0000149', // cdao:has_Child
someValuesFrom: {
'@type': 'owl:Class',
intersectionOf: [
{
'@type': 'owl:Restriction',
onProperty: 'phyloref:excludes_TU',
someValuesFrom: new TaxonomicUnitWrapper(tu1, this.defaultNomenCode).asOWLEquivClass,
},
this.getIncludesRestrictionForTU(tu2),
],
},
};
}
/*
* Create an OWL restriction for a phyloreference made up entirely of internal
* specifiers.
* - jsonld: the JSON-LD representation of this phyloreference in model 1.0.
* We mainly use this to access the '@id' and internal and external specifiers.
* - remainingInternals: all internal specifiers that have not yet been selected.
* - selected: internal specifiers have been seen selected. This should initially
* be [], and will be filled in when this method calls itself recursively.
*
* This method works like this:
* 1. We have several special cases: we fail if 0 or 1 specifiers are
* provided, and we have a special representation for 2 specifiers.
* 2. Create an expression for the currently selected specifiers. This expression
* is in the form:
* has_Child some (
* excludes_lineage_to some [remaining specifiers]
* and [selected specifiers]
* )
* We generate the expressions for remaining specifiers and selected specifiers by calling
* this method recursively.
* 3. Finally, we select another internal from the remainingInternals and generate an
* expression for that selection by calling this method recursively. Note that we
* only process cases where there are more remainingInternals than selected
* internals -- when there are fewer, we'll just end up with the inverses of the
* previous comparisons, which we'll already have covered.
*/
createClassExpressionsForInternals(jsonld, remainingInternals, selected) {
// process.stderr.write(`@id [${jsonld['@id']}] Remaining internals:
// ${remainingInternals.length}, selected: ${selected.length}\n`);
// Quick special case: if we have two 'remainingInternals' and zero selecteds,
// we can just return the MRCA for two internal specifiers.
if (selected.length === 0) {
if (remainingInternals.length === 2) {
return [
this.getMRCARestrictionOfTwoTUs(remainingInternals[0], remainingInternals[1]),
];
} if (remainingInternals.length === 1) {
throw new Error('Cannot determine class expression for a single specifier');
} else if (remainingInternals.length === 0) {
throw new Error('Cannot determine class expression for zero specifiers');
}
}
// Step 1. If we've already selected something, create an expression for it.
const classExprs = [];
if (selected.length > 0) {
let remainingInternalsExpr = [];
if (remainingInternals.length === 1) {
remainingInternalsExpr = this.getIncludesRestrictionForTU(remainingInternals[0]);
} else if (remainingInternals.length === 2) {
remainingInternalsExpr = this.getMRCARestrictionOfTwoTUs(
remainingInternals[0],
remainingInternals[1]
);
} else {
remainingInternalsExpr = this.createComponentClass(
jsonld,
remainingInternals,
[],
this.createClassExpressionsForInternals(jsonld, remainingInternals, [])
);
}
let selectedExpr = [];
if (selected.length === 1) {
selectedExpr = this.getIncludesRestrictionForTU(selected[0]);
} else if (selected.length === 2) {
selectedExpr = this.getMRCARestrictionOfTwoTUs(selected[0], selected[1]);
} else {
selectedExpr = this.createComponentClass(
jsonld,
selected,
[],
this.createClassExpressionsForInternals(jsonld, selected, [])
);
}
classExprs.push({
'@type': 'owl:Restriction',
onProperty: 'obo:CDAO_0000149', // cdao:has_Child
someValuesFrom: {
'@type': 'owl:Class',
intersectionOf: [{
'@type': 'owl:Restriction',
onProperty: 'phyloref:excludes_lineage_to',
someValuesFrom: remainingInternalsExpr,
}, selectedExpr],
},
});
}
// Step 2. Now select everything from remaining once, and start recursing through
// every possibility.
// Note that we only process cases where there are more remainingInternals than
// selected internals -- when there are fewer, we'll just end up with the inverses
// of the previous comparisons, which we'll already have covered.
if (remainingInternals.length > 1 && selected.length <= remainingInternals.length) {
remainingInternals.map(newlySelected => this.createClassExpressionsForInternals(
jsonld,
// The new remaining is the old remaining minus the selected TU.
remainingInternals.filter(i => i !== newlySelected),
// The new selected is the old selected plus the selected TU.
selected.concat([newlySelected])
))
.reduce((acc, val) => acc.concat(val), [])
.forEach(expr => classExprs.push(expr));
}
return classExprs;
}
/*
* Phyloref.asJSONLD(fallbackIRI)
*
* Export this phylogeny as JSON-LD.
*
* Arguments:
* - fallbackIRI: The base IRI to use for this phyloref if it does not have
* an '@id'.
*/
asJSONLD(fallbackIRI) {
// Keep all currently extant data.
const phylorefAsJSONLD = cloneDeep(this.phyloref);
// Set the @id and @type. If we don't already have an '@id', use the
// fallbackIRI.
if (!has(phylorefAsJSONLD, '@id')) phylorefAsJSONLD['@id'] = fallbackIRI;
phylorefAsJSONLD['@type'] = 'owl:Class';
// If we don't have a bibliographicCitation but we do have a definition source,
// then generate a bibliographicCitation for the source.
if (has(phylorefAsJSONLD, 'definitionSource')) {
const definitionSource = phylorefAsJSONLD.definitionSource;
if (!has(definitionSource, 'bibliographicCitation')) {
definitionSource.bibliographicCitation = new CitationWrapper(definitionSource).toString();
}
}
// Construct a class expression for this phyloreference.
const internalSpecifiers = phylorefAsJSONLD.internalSpecifiers || [];
const externalSpecifiers = phylorefAsJSONLD.externalSpecifiers || [];
// If it is an apomorphy-based class expression, we should generate a
// logical expression that describes the apomorphy.
const phylorefType = phylorefAsJSONLD.phylorefType;
if (
(phylorefType && phylorefType === 'phyloref:PhyloreferenceUsingApomorphy')
|| (has(phylorefAsJSONLD, 'apomorphy'))
) {
// This is an apomorphy-based definition!
phylorefAsJSONLD.subClassOf = [
'phyloref:Phyloreference',
'phyloref:PhyloreferenceUsingApomorphy',
];
// Someday, we will probably want to turn this apomorphy into a
// logical expression so that it can be computed alongside other
// OWL ontologies. This is outside our scope for the moment, so
// we will simply pass on the phyloreference as-is.
return phylorefAsJSONLD;
}
// We might need to make component classes.
// So we reset our component class counts and records.
this.componentClassCount = 0;
this.componentClassesByLabel = {};
// The type of this phyloreference.
let calculatedPhylorefType;
// The list of logical expressions generated for this phyloref.
let logicalExpressions = [];
if (internalSpecifiers.length === 0) {
// We can't handle phyloreferences without at least one internal specifier.
calculatedPhylorefType = 'phyloref:MalformedPhyloreference';
phylorefAsJSONLD.malformedPhyloreference = 'No internal specifiers provided';
} else if (externalSpecifiers.length > 0) {
calculatedPhylorefType = 'phyloref:PhyloreferenceUsingMaximumClade';
// If the phyloreference has at least one external specifier, we
// can provide a simplified expression for the internal specifier,
// in the form:
// phyloref:includes_TU some [internal1] and
// phyloref:includes_TU some [internal2] and ...
// phyloref:excludes_TU some [exclusion1] and
// has_Ancestor some (phyloref:excludesTU some [exclusion2]) ...
//
// Since we don't know which of the external specifiers will actually
// be the one that will be used, we need to generate logical expressions
// for every possibility.
logicalExpressions = externalSpecifiers.map((selectedExternal) => {
// Add the internal specifiers.
const intersectionExprs = internalSpecifiers.map(
sp => this.getIncludesRestrictionForTU(sp)
);
// Add the selected external specifier.
intersectionExprs.push({
'@type': 'owl:Restriction',
onProperty: 'phyloref:excludes_TU',
someValuesFrom: new TaxonomicUnitWrapper(
selectedExternal,
this.defaultNomenCode
).asOWLEquivClass,
});
// Collect all of the externals that are not selected.
const remainingExternals = externalSpecifiers.filter(ex => ex !== selectedExternal);
// Add the remaining externals, which we assume will resolve outside of
// this clade.
remainingExternals.forEach((externalTU) => {
intersectionExprs.push({
'@type': 'owl:Restriction',
onProperty: 'obo:CDAO_0000144', // has_Ancestor
someValuesFrom: {
'@type': 'owl:Restriction',
onProperty: 'phyloref:excludes_TU',
someValuesFrom: new TaxonomicUnitWrapper(
externalTU,
this.defaultNomenCode
).asOWLEquivClass,
},
});
});
return {
'@type': 'owl:Class',
intersectionOf: intersectionExprs,
};
});
} else {
calculatedPhylorefType = 'phyloref:PhyloreferenceUsingMinimumClade';
// We only have internal specifiers. We therefore need to use the algorithm in
// this.createClassExpressionsForInternals() to create this expression.
logicalExpressions = this.createClassExpressionsForInternals(
phylorefAsJSONLD, internalSpecifiers, []
);
}
// If we have a single logical expression, we set that as an equivalentClass
// expression. If we have more than one, we produce multiple component classes
// to represent it.
if (logicalExpressions.length === 0) {
// This is fine, as long as there is an explanation in
// phyloref.malformedPhyloreference explaining why no logical expressions
// could be generated. Otherwise, throw an error.
if (!has(phylorefAsJSONLD, 'malformedPhyloreference')) {
throw new Error(
`Phyloref ${this.label} was generated by Phyx.js with neither logical expressions nor an explanation for the lack of logical expressions. `
+ 'This indicates an error in the Phyx.js library. Please report this bug at https://github.com/phyloref/phyx.js/issues.'
);
}
} else if (logicalExpressions.length === 1) {
// If we have a single logical expression, then that is what this phyloref
// is equivalent to.
phylorefAsJSONLD.equivalentClass = logicalExpressions[0];
} else {
// If we have multiple logical expressions, the phyloreference can be
// represented by any of them. We model this by creating subclasses of
// the phyloreference for each logical expression -- that way, it's clear
// that these expressions aren't equivalent to each other (which is what
// caused https://github.com/phyloref/phyx.js/issues/57), but nodes
// resolved by any of those expressions will also be included in the
// phyloreference itself.
//
// Note that there are two differences from the way in which we usually call
// this.createComponentClass():
// 1. Usually, createComponentClass() reuses logical expressions with the
// same sets of internal and external specifiers. That won't work here,
// since *all* these logical expressions have the same specifiers. So,
// we turn off caching.
// 2. We need to set each of these component classes to be a subclass of
// this phyloreference so that it can include instances from each of the
// logical expressions.
phylorefAsJSONLD.subClasses = logicalExpressions.map(classExpr => this.createComponentClass(
phylorefAsJSONLD,
internalSpecifiers,
externalSpecifiers,
classExpr,
// False in order to turn off caching by internal and external specifiers.
false,
// Make the new component class a subclass of this phyloreference.
phylorefAsJSONLD
));
}
// Every phyloreference is a subclass of phyloref:Phyloreference.
if (!phylorefAsJSONLD.subClassOf) phylorefAsJSONLD.subClassOf = [];
if (!Array.isArray(phylorefAsJSONLD.subClassOf)) {
phylorefAsJSONLD.subClassOf = [phylorefAsJSONLD.subClassOf];
}
phylorefAsJSONLD.subClassOf.push('phyloref:Phyloreference');
// If the this Phyloref has a phylorefType that differs from the calculated
// phyloref type, throw an error.
if (has(phylorefAsJSONLD, 'phylorefType') && phylorefAsJSONLD.phylorefType !== calculatedPhylorefType) {
throw new Error(
`Phyloref ${this.label} has phylorefType set to '${phylorefAsJSONLD.phylorefType}', but it appears to be a '${calculatedPhylorefType}'.`
);
}
phylorefAsJSONLD.subClassOf.push(calculatedPhylorefType);
return phylorefAsJSONLD;
}
}
module.exports = {
PhylorefWrapper,
};