hra-api
Version:
The Human Reference Atlas (HRA) API deployed to https://apps.humanatlas.io/api/
205 lines (187 loc) • 7.16 kB
JavaScript
import { Matrix4 } from '@math.gl/core';
import { simplifyScene } from '../../shared/spatial/simplify-scene.js';
import { getSpatialGraph } from '../../shared/spatial/spatial-graph.js';
import { select } from '../../shared/utils/sparql.js';
import landmarksQuery from '../../v1/queries/reference-landmarks.rq';
import hraPlacementsQuery from '../../v1/queries/reference-organ-placement-patches.rq';
import landmarksFrame from '../frames/reference-landmarks.jsonld';
import frame from '../frames/reference-organs.jsonld';
import query from '../queries/reference-organ-as.rq';
import { executeFilteredConstructQuery } from '../utils/execute-sparql.js';
import { ensureArray, ensureGraphArray, normalizeJsonLd } from '../utils/jsonld-compat.js';
import { getReferenceOrgans } from './reference-organs.js';
function reformatResponse(results) {
return normalizeJsonLd(ensureGraphArray(results));
}
function getLabel(spatialEntity) {
return spatialEntity.label
.replace('Spatial entity of female ', '')
.replace('Spatial entity of male ', '')
.replace('left ', '')
.replace('right ', '')
.replace('Left ', '')
.replace('Right ', '')
.replace(' full term', '');
}
async function getSpatialPlacements(endpoint = 'https://lod.humanatlas.io/sparql') {
const placements = await select(hraPlacementsQuery, endpoint);
return placements;
}
function getPatchPlacements(refOrgans, placements, graph) {
const placementPatches = {};
for (const sourcePlacement of placements) {
const source = sourcePlacement.source;
// Ignore placements from spatial object references and the VH Male/Female entities
if (
!source.endsWith('_obj') &&
!source.endsWith('Obj') &&
!source.endsWith('#VHFemale') &&
!source.endsWith('#VHMale')
) {
let placement = {
'@id': sourcePlacement.id,
'@type': 'SpatialPlacement',
...sourcePlacement,
scaling_units: 'ratio',
rotation_units: 'degree',
id: undefined,
};
if (!refOrgans.some((refOrgan) => refOrgan['@id'] === placement.target)) {
for (const refOrgan of refOrgans) {
const directPlacement = graph.getSpatialPlacement({ '@id': source }, refOrgan['@id']);
if (directPlacement) {
placement = directPlacement;
break;
}
}
}
placementPatches[source] = placement;
}
}
return placementPatches;
}
async function getReferenceOrganAnatomicalStructures(filter, endpoint = 'https://lod.humanatlas.io/sparql') {
return reformatResponse(await executeFilteredConstructQuery(query, filter, frame, endpoint)).sort(
(a, b) => a.rui_rank - b.rui_rank
);
}
async function getReferenceLandmarks(filter, endpoint = 'https://lod.humanatlas.io/sparql') {
return reformatResponse(await executeFilteredConstructQuery(landmarksQuery, filter, landmarksFrame, endpoint)).map(
(landmarkSet) => {
landmarkSet.extractionSites = ensureArray(landmarkSet.extractionSites);
return landmarkSet;
}
);
}
export async function getRuiReferenceData(filter, endpoint = 'https://lod.humanatlas.io/sparql') {
const [refOrgans, refOrganAs, landmarkSets, graph, allPlacementPatches] = await Promise.all([
getReferenceOrgans(filter, endpoint),
getReferenceOrganAnatomicalStructures(filter, endpoint),
getReferenceLandmarks(filter, endpoint),
getSpatialGraph(endpoint),
getSpatialPlacements(endpoint),
]);
const placementPatches = getPatchPlacements(refOrgans, allPlacementPatches, graph);
const organIRILookup = {};
const organSpatialEntities = {};
const anatomicalStructures = {};
for (const organ of refOrgans) {
const key = [getLabel(organ), organ.sex, organ.side ?? ''].join('|');
organIRILookup[key] = organ['@id'];
organSpatialEntities[organ['@id']] = organ;
}
for (const organAs of refOrganAs) {
const organ = organAs.reference_organ;
const structures = (anatomicalStructures[organ] = anatomicalStructures[organ] ?? []);
structures.push(organAs);
}
for (const organ of refOrgans) {
if (!anatomicalStructures[organ['@id']]) {
anatomicalStructures[organ['@id']] = [organ];
}
}
const sceneNodeLookup = {};
const asNodes = [
...Object.entries(organSpatialEntities),
...refOrganAs.map((organAs) => [organAs.reference_organ, organAs]),
...landmarkSets.reduce(
(acc, landmarkSet) =>
acc.concat(landmarkSet.extractionSites.map((site) => [landmarkSet.extraction_set_for, site])),
[]
),
];
for (const [organ, organAs] of asNodes) {
const organSpatialEntity = organSpatialEntities[organ];
const body = 'https://purl.humanatlas.io/graph/hra-ccf-body#VH' + organSpatialEntity.sex;
const organPlacement = graph.getSpatialPlacement(
{
'@id': organ,
placement: organSpatialEntity.placement,
},
body
);
const dimensions = [
organSpatialEntity.x_dimension,
organSpatialEntity.y_dimension,
organSpatialEntity.z_dimension,
].map((n) => -n / 1000 / 2);
const tx = [organPlacement.x_translation, organPlacement.y_translation, organPlacement.z_translation].map(
(n) => -n / 1000
);
let transformMatrix = graph.get3DObjectTransform(organAs['@id'], body, organAs.object['@id']);
if (transformMatrix) {
transformMatrix = new Matrix4(Matrix4.IDENTITY)
.translate(dimensions)
.translate(tx)
.multiplyRight(transformMatrix);
}
const sceneNode = {
'@id': organAs['@id'],
'@type': 'SpatialSceneNode',
representation_of: organAs.representation_of,
reference_organ: organ,
scenegraph: organAs.object.file,
scenegraphNode: organAs.object.file_subpath,
transformMatrix,
tooltip: organAs.label,
unpickable: true,
_lighting: 'pbr',
zoomBasedOpacity: false,
color: [255, 255, 255, 255],
};
sceneNodeLookup[organAs['@id']] = sceneNode;
}
const extractionSets = {};
for (const refOrgan of refOrgans) {
extractionSets[refOrgan['@id']] = extractionSets[refOrgan['@id']] || [];
}
for (const landmarkSet of landmarkSets) {
const refOrgan = landmarkSet.extraction_set_for;
const sets = (extractionSets[refOrgan] = extractionSets[refOrgan] || []);
const sameSet = sets.find((set) => set.label === landmarkSet.label);
if (sameSet) {
sameSet.extractionSites = sameSet.extractionSites.concat(landmarkSet.extractionSites);
} else {
if (landmarkSet.label === 'Landmarks') {
sets.unshift(landmarkSet);
} else {
sets.push(landmarkSet);
}
}
}
// Y3 TODO: get simplified scene nodes into the KG so we don't have to load/compute this on the fly
const simpleSceneNodes = await simplifyScene(Object.values(sceneNodeLookup));
const simpleSceneNodeLookup = simpleSceneNodes.reduce((acc, node) => {
acc[node['@id']] = node;
return acc;
}, {});
return {
organIRILookup,
organSpatialEntities,
anatomicalStructures,
extractionSets,
sceneNodeLookup,
simpleSceneNodeLookup,
placementPatches,
};
}