UNPKG

hra-api

Version:

The Human Reference Atlas (HRA) API deployed to https://apps.humanatlas.io/api/

237 lines (216 loc) 8.2 kB
import { Euler, Matrix4, toDegrees, toRadians } from '@math.gl/core'; import { OrientedBoundingBox } from '@math.gl/culling'; import graphology from 'graphology'; import reverse from 'graphology-operators/reverse.js'; import shortestPath from 'graphology-shortest-path/unweighted.js'; import { LRUCache } from 'lru-cache'; import { v4 as uuidV4 } from 'uuid'; import dimensionsQuery from '../../v1/queries/spatial-entity-dimensions.rq'; import hraPlacementsQuery from '../../v1/queries/spatial-placements-hra.rq'; import placementsQuery from '../../v1/queries/spatial-placements.rq'; import { ensureArray } from '../../v1/utils/jsonld-compat.js'; import { select } from '../utils/sparql.js'; /** @type {LRUCache<string | undefined, Promise<SpatialGraph>>} */ const CACHED_GRAPH = new LRUCache({ max: 20, ttl: 1000 * 60 * 60 * 8, // 8 hours ttlAutopurge: true, }); /** * Get an initialized spatial graph for spatial queries * * @param {string} endpoint the sparql endpoint to use for queries * @param {boolean} useCache whether to create/use a cached SpatialGraph * @returns {Promise<SpatialGraph>} a promise for an initialized SpatialGraph */ export async function getSpatialGraph(endpoint, useCache = false, token = undefined) { if (!useCache) { return new SpatialGraph(endpoint, token).initialize(); } else { if (!CACHED_GRAPH.has(token)) { const graph = getSpatialGraph(endpoint, false, token); CACHED_GRAPH.set(token, graph); return graph; } else { return CACHED_GRAPH.get(token); } } } function getScaleFactor(units) { switch (units) { case 'centimeter': return 0.01; default: case 'millimeter': return 0.001; case 'meter': return 1; } } export class SpatialGraph { constructor(endpoint, token = undefined) { this.endpoint = endpoint; this.token = token; } async initialize() { const graph = (this.graph = new graphology.DirectedGraph()); const from = this.token ? `FROM <urn:hra-api:${this.token}:ds-graph>` : ''; const [placements, hraPlacements, dimensions] = await Promise.all([ select(placementsQuery.replace('#{{FROM}}', from), this.endpoint), select(hraPlacementsQuery, this.endpoint), select(dimensionsQuery.replace('#{{FROM}}', from), this.endpoint), ]); for (const placement of placements.concat(hraPlacements)) { graph.mergeDirectedEdge(placement.source, placement.target, { placement }); } const halfSizeLookup = (this.halfSizeLookup = {}); for (const { rui_location, x, y, z, units } of dimensions) { const factor = getScaleFactor(units) * 0.5; halfSizeLookup[rui_location] = [x, y, z].map((n) => Number(n) * factor); } return this; } getTransformationMatrix(sourceIRI, targetIRI) { if (sourceIRI === targetIRI) { return new Matrix4(Matrix4.IDENTITY); // identity } if (!this.graph.hasNode(sourceIRI) || !this.graph.hasNode(targetIRI)) { return undefined; } const tx = new Matrix4(Matrix4.IDENTITY); const path = shortestPath.bidirectional(this.graph, sourceIRI, targetIRI); if (path && path.length > 0) { path.reverse(); let target = ''; for (const source of path) { if (target) { const placement = this.graph.getEdgeAttribute(source, target, 'placement'); this.applySpatialPlacement(tx, placement); } target = source; } return tx; } else { return undefined; } } applySpatialPlacement(tx, placement) { const p = placement; const factor = getScaleFactor(p.translation_units); const T = [p.x_translation * factor, p.y_translation * factor, p.z_translation * factor]; const R = [p.x_rotation, p.y_rotation, p.z_rotation].map(toRadians); const S = [p.x_scaling, p.y_scaling, p.z_scaling]; return tx.translate(T).rotateXYZ(R).scale(S); } getSpatialPlacement(source, targetIri) { const sourceIri = this.graph.hasNode(source['@id']) ? source['@id'] : undefined; const placement = ensureArray(source.placement)[0]; let matrix; if (placement && this.graph.hasNode(placement.target)) { matrix = this.getTransformationMatrix(placement.target, targetIri); if (matrix) { matrix = this.applySpatialPlacement(matrix, placement); } } else if (sourceIri) { matrix = this.getTransformationMatrix(sourceIri, targetIri); } if (matrix) { return this.matrixToSpatialPlacement(matrix, source['@id'], targetIri); } else { return undefined; } } matrixToSpatialPlacement(matrix, sourceIri, targetIri) { const euler = new Euler().fromRotationMatrix(matrix, Euler.XYZ); const T = matrix.getTranslation().map((n) => n * 1000); const R = euler.toVector3().map(toDegrees); const S = matrix.getScale().map((n) => (n < 1 && n > 0.999999 ? 1 : n)); return { '@context': 'https://hubmapconsortium.github.io/hubmap-ontology/ccf-context.jsonld', '@id': `http://purl.org/ccf/1.5/${uuidV4()}_placement`, '@type': 'SpatialPlacement', source: sourceIri, target: targetIri, placement_date: new Date().toISOString().split('T')[0], x_scaling: S[0], y_scaling: S[1], z_scaling: S[2], scaling_units: 'ratio', x_rotation: R[0], y_rotation: R[1], z_rotation: R[2], rotation_order: 'XYZ', rotation_units: 'degree', x_translation: T[0], y_translation: T[1], z_translation: T[2], translation_units: 'millimeter', }; } getSpatialPlacementsToTarget(targetIri) { const graph = reverse(this.graph); const sources = shortestPath.singleSource(graph, targetIri); const results = []; for (const [sourceIri, path] of Object.entries(sources)) { if (sourceIri !== targetIri) { const tx = new Matrix4(Matrix4.IDENTITY); let target = ''; for (const source of path) { if (target) { const placement = graph.getEdgeAttribute(target, source, 'placement'); this.applySpatialPlacement(tx, placement); } target = source; } results.push(this.matrixToSpatialPlacement(tx, sourceIri, targetIri)); } } return results; } get3DObjectTransform(_sourceIri, targetIri, objectRefIri) { let transform = this.getTransformationMatrix(objectRefIri, targetIri); if (transform) { transform = new Matrix4(Matrix4.IDENTITY).rotateX(toRadians(90)).multiplyLeft(transform); } return transform; } getExtractionSiteTransform(sourceIri, targetIri, bounds) { const transform = this.getTransformationMatrix(sourceIri, targetIri); if (transform) { // Scale visible bounding boxes to the desired dimensions if (bounds) { const factor = getScaleFactor(bounds.dimension_units) * 0.5; const scale = [bounds.x_dimension * factor, bounds.y_dimension * factor, bounds.z_dimension * factor]; transform.scale(scale); } } return transform; } getOrientedBoundingBox(sourceIri, targetIri) { const matrix = this.getTransformationMatrix(sourceIri, targetIri); const halfSize = this.halfSizeLookup[sourceIri]; if (matrix && halfSize) { const center = matrix.getTranslation(); if (center.findIndex(isNaN) === -1) { const quaternion = new Euler().fromRotationMatrix(matrix, Euler.XYZ).toQuaternion().normalize().calculateW(); return new OrientedBoundingBox().fromCenterHalfSizeQuaternion(center, halfSize, quaternion); } } return undefined; } probeExtractionSites(search, results = new Set()) { const { x, y, z, radius, target } = search; const radiusSquared = (radius / 1000) * (radius / 1000); const center = [x, y, z].map((n) => n / 1000); for (const sourceIri of Object.keys(this.halfSizeLookup)) { const boundingBox = this.getOrientedBoundingBox(sourceIri, target); if (boundingBox) { const distanceSquared = boundingBox.distanceSquaredTo(center); if (distanceSquared <= radiusSquared) { results.add(sourceIri); } } } return results; } }