UNPKG

molstar

Version:

A comprehensive macromolecular library.

441 lines (440 loc) 21.6 kB
"use strict"; /** * Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Adam Midlik <midlik@gmail.com> */ Object.defineProperty(exports, "__esModule", { value: true }); exports.MVSAnnotation = exports.MVSAnnotations = exports.MVSAnnotationsProvider = exports.MVSAnnotationsParams = void 0; exports.getMVSAnnotationForStructure = getMVSAnnotationForStructure; const db_1 = require("../../../mol-data/db.js"); const cif_1 = require("../../../mol-io/reader/cif.js"); const schema_1 = require("../../../mol-io/reader/cif/schema.js"); const mmcif_1 = require("../../../mol-model-formats/structure/mmcif.js"); const custom_model_property_1 = require("../../../mol-model-props/common/custom-model-property.js"); const custom_property_1 = require("../../../mol-model/custom-property.js"); const structure_1 = require("../../../mol-model/structure/structure.js"); const assets_1 = require("../../../mol-util/assets.js"); const json_1 = require("../../../mol-util/json.js"); const object_1 = require("../../../mol-util/object.js"); const param_choice_1 = require("../../../mol-util/param-choice.js"); const param_definition_1 = require("../../../mol-util/param-definition.js"); const element_ranges_1 = require("../helpers/element-ranges.js"); const indexing_1 = require("../helpers/indexing.js"); const param_definition_2 = require("../helpers/param-definition.js"); const schemas_1 = require("../helpers/schemas.js"); const selections_1 = require("../helpers/selections.js"); const utils_1 = require("../helpers/utils.js"); /** Allowed values for the annotation format parameter */ const MVSAnnotationFormat = new param_choice_1.Choice({ json: 'json', cif: 'cif', bcif: 'bcif' }, 'json'); const MVSAnnotationFormatTypes = { json: 'string', cif: 'string', bcif: 'binary' }; exports.MVSAnnotationsParams = { annotations: param_definition_1.ParamDefinition.ObjectList({ source: param_definition_1.ParamDefinition.MappedStatic('source-cif', { 'source-cif': param_definition_1.ParamDefinition.EmptyGroup(), 'url': param_definition_1.ParamDefinition.Group({ url: param_definition_1.ParamDefinition.Text(''), format: MVSAnnotationFormat.PDSelect(), }), }), schema: schemas_1.MVSAnnotationSchema.PDSelect(), cifBlock: param_definition_1.ParamDefinition.MappedStatic('index', { index: param_definition_1.ParamDefinition.Group({ index: param_definition_1.ParamDefinition.Numeric(0, { min: 0, step: 1 }, { description: '0-based index of the block' }) }), header: param_definition_1.ParamDefinition.Group({ header: param_definition_1.ParamDefinition.Text(undefined, { description: 'Block header' }) }), }, { description: 'Specify which CIF block contains annotation data (only relevant when format=cif or format=bcif)' }), cifCategory: (0, param_definition_2.MaybeStringParamDefinition)({ placeholder: 'Take first category', description: 'Specify which CIF category contains annotation data (only relevant when format=cif or format=bcif)' }), fieldRemapping: param_definition_1.ParamDefinition.ObjectList({ standardName: param_definition_1.ParamDefinition.Text('', { placeholder: ' ', description: 'Standard name of the selector field (e.g. label_asym_id)' }), actualName: (0, param_definition_2.MaybeStringParamDefinition)({ placeholder: 'Ignore field', description: 'Actual name of the field in the annotation data (e.g. spam_chain_id), null to ignore the field with standard name' }), }, e => `"${e.standardName}": ${e.actualName === null ? 'null' : `"${e.actualName}"`}`, { description: 'Optional remapping of annotation field names { standardName1: actualName1, ... }. Use { "label_asym_id": "X" } to load actual field "X" as "label_asym_id". Use { "label_asym_id": null } to ignore actual field "label_asym_id". Fields not mentioned here are mapped implicitely (i.e. actual name = standard name).' }), id: param_definition_1.ParamDefinition.Text('', { description: 'Arbitrary identifier that can be referenced by MVSAnnotationColorTheme' }), }, obj => obj.id), }; /** Provider for custom model property "Annotations" */ exports.MVSAnnotationsProvider = custom_model_property_1.CustomModelProperty.createProvider({ label: 'MVS Annotations', descriptor: (0, custom_property_1.CustomPropertyDescriptor)({ name: 'mvs-annotations', }), type: 'static', defaultParams: exports.MVSAnnotationsParams, getParams: (data) => exports.MVSAnnotationsParams, isApplicable: (data) => true, obtain: async (ctx, data, props) => { var _a; props = { ...param_definition_1.ParamDefinition.getDefaultValues(exports.MVSAnnotationsParams), ...props }; const specs = (_a = props.annotations) !== null && _a !== void 0 ? _a : []; const annots = await MVSAnnotations.fromSpecs(ctx, specs, data); return { value: annots }; } }); /** Represents multiple annotations retrievable by their ID */ class MVSAnnotations { constructor(dict) { this.dict = dict; } static async fromSpecs(ctx, specs, model) { var _a; const sources = specs.map(annotationSourceFromSpec); const files = await getFilesFromSources(ctx, sources, model); const annots = {}; for (let i = 0; i < specs.length; i++) { const spec = specs[i]; try { const file = files[i]; if (!file.ok) throw file.error; annots[spec.id] = await MVSAnnotation.fromSpec(ctx, spec, file.value); } catch (err) { (_a = ctx.errorContext) === null || _a === void 0 ? void 0 : _a.add('mvs', `Failed to obtain annotation (${err}).\nAnnotation specification source params: ${JSON.stringify(spec.source.params)}`); console.error(`Failed to obtain annotation (${err}).\nAnnotation specification:`, spec); annots[spec.id] = MVSAnnotation.createEmpty(spec.schema); } } return new MVSAnnotations(annots); } getAnnotation(id) { return this.dict[id]; } getAllAnnotations() { return Object.values(this.dict); } } exports.MVSAnnotations = MVSAnnotations; /** Retrieve annotation with given `annotationId` from custom model property "MVS Annotations" and the model from which it comes */ function getMVSAnnotationForStructure(structure, annotationId) { const models = structure.isEmpty ? [] : structure.models; for (const model of models) { if (model.customProperties.has(exports.MVSAnnotationsProvider.descriptor)) { const annots = exports.MVSAnnotationsProvider.get(model).value; const annotation = annots === null || annots === void 0 ? void 0 : annots.getAnnotation(annotationId); if (annotation) { return { annotation, model }; } } } return { annotation: undefined, model: undefined }; } function getIndexedElementsForUnitKind(indexedModel, unitKind) { if (unitKind === structure_1.Unit.Kind.Atomic) return indexedModel.atoms; if (unitKind === structure_1.Unit.Kind.Spheres) return indexedModel.spheres; if (unitKind === structure_1.Unit.Kind.Gaussians) return indexedModel.gaussians; console.warn(`Unknown Unit.Kind value: ${unitKind}`); return null; } /** Main class for processing MVS annotation */ class MVSAnnotation { constructor(data, schema, fieldRemapping) { this.data = data; this.schema = schema; this.fieldRemapping = fieldRemapping; /** Cached `IndexedModel` per `Model.id` (if annotation contains no instanceIds) * or per `Model.id:instanceId` combination (if at least one row contains instanceId). */ this._indexedModels = new Map(); /** Cached annotation rows. Do not use directly, use `getRows` instead. */ this._rows = undefined; this._hasInstanceIds = undefined; this.nRows = getRowCount(data); } /** Create a new `MVSAnnotation` based on specification `spec`. Use `file` if provided, otherwise download the file. * Throw error if download fails or problem with data. */ static async fromSpec(ctx, spec, file) { var _a; file !== null && file !== void 0 ? file : (file = await getFileFromSource(ctx, annotationSourceFromSpec(spec))); let data; switch (file.format) { case 'json': data = file; break; case 'cif': if (file.data.blocks.length === 0) throw new Error('No block in CIF'); const blockSpec = spec.cifBlock; let block; switch (blockSpec.name) { case 'header': const foundBlock = file.data.blocks.find(b => b.header === blockSpec.params.header); if (!foundBlock) throw new Error(`CIF block with header "${blockSpec.params.header}" not found`); block = foundBlock; break; case 'index': block = file.data.blocks[blockSpec.params.index]; if (!block) throw new Error(`CIF block with index ${blockSpec.params.index} not found`); break; } const categoryName = (_a = spec.cifCategory) !== null && _a !== void 0 ? _a : Object.keys(block.categories)[0]; if (!categoryName) throw new Error('There are no categories in CIF block'); const category = block.categories[categoryName]; if (!category) throw new Error(`CIF category "${categoryName}" not found`); data = { format: 'cif', data: category }; break; } return new MVSAnnotation(data, spec.schema, Object.fromEntries(spec.fieldRemapping.map(e => [e.standardName, e.actualName]))); } static createEmpty(schema) { return new MVSAnnotation({ format: 'json', data: [] }, schema, {}); } /** Return value of field `fieldName` assigned to location `loc`, if any */ getValueForLocation(loc, fieldName) { const indexedModel = this.getIndexedModel(loc.unit.model, loc.unit.conformation.operator.instanceId); const indexedElements = getIndexedElementsForUnitKind(indexedModel, loc.unit.kind); const iRow = indexedElements ? indexedElements[loc.element] : -1; return this.getValueForRow(iRow, fieldName); } /** Return value of field `fieldName` assigned to `i`-th annotation row, if any */ getValueForRow(i, fieldName) { if (i < 0) return undefined; switch (this.data.format) { case 'json': const value = getValueFromJson(i, fieldName, this.data.data); if (value === undefined || typeof value === 'string') return value; else return `${value}`; case 'cif': return getValueFromCif(i, fieldName, this.data.data); } } /** Return cached `ElementIndex` -> `MVSAnnotationRow` mapping for `Model` (or create it if not cached yet) */ getIndexedModel(model, instanceId) { const key = this.hasInstanceIds() ? `${model.id}:${instanceId}` : model.id; if (!this._indexedModels.has(key)) { const result = this.getRowForEachAtom(model, instanceId); this._indexedModels.set(key, result); } return this._indexedModels.get(key); } /** Create `ElementIndex` -> `MVSAnnotationRow` mapping for `Model` */ getRowForEachAtom(model, instanceId) { const indices = indexing_1.IndicesAndSortings.get(model); const nAtoms = model.atomicHierarchy.atoms._rowCount; const nSpheres = model.coarseHierarchy.spheres.count; const nGaussians = model.coarseHierarchy.gaussians.count; let indexedAtoms = null; let indexedSpheres = null; let indexedGaussians = null; const rows = this.getRows(); for (let iRow = 0, nRows = rows.length; iRow < nRows; iRow++) { const row = rows[iRow]; const atomRanges = (0, selections_1.getAtomRangesForRow)(row, model, instanceId, indices); indexedAtoms = fillValueOnRanges(indexedAtoms, nAtoms, atomRanges, iRow); const sphereRanges = (0, selections_1.getSphereRangesForRow)(row, model, instanceId, indices); indexedSpheres = fillValueOnRanges(indexedSpheres, nSpheres, sphereRanges, iRow); const gaussianRanges = (0, selections_1.getGaussianRangesForRow)(row, model, instanceId, indices); indexedGaussians = fillValueOnRanges(indexedGaussians, nGaussians, gaussianRanges, iRow); } return { atoms: indexedAtoms, spheres: indexedSpheres, gaussians: indexedGaussians }; } /** Parse and return all annotation rows in this annotation, or return cached result if available */ getRows() { var _a; return (_a = this._rows) !== null && _a !== void 0 ? _a : (this._rows = this._getRows()); } /** Parse and return all annotation rows in this annotation */ _getRows() { switch (this.data.format) { case 'json': return getRowsFromJson(this.data.data, this.schema, this.fieldRemapping); case 'cif': return getRowsFromCif(this.data.data, this.schema, this.fieldRemapping); } } /** Return `true` if some rows in the annotation contain `instance_id` field. */ hasInstanceIds() { var _a; return (_a = this._hasInstanceIds) !== null && _a !== void 0 ? _a : (this._hasInstanceIds = this.getRows().some(row => (0, utils_1.isDefined)(row.instance_id))); } /** Return list of all distinct values appearing in field `fieldName`, in order of first occurrence. Ignores special values `.` and `?`. If `caseInsensitive`, make all values uppercase. */ getDistinctValuesInField(fieldName, caseInsensitive) { const seen = new Set(); const out = []; for (let i = 0; i < this.nRows; i++) { let value = this.getValueForRow(i, fieldName); if (caseInsensitive) value = value === null || value === void 0 ? void 0 : value.toUpperCase(); if (value !== undefined && !seen.has(value)) { seen.add(value); out.push(value); } } return out; } } exports.MVSAnnotation = MVSAnnotation; function getValueFromJson(rowIndex, fieldName, data) { var _a, _b; const js = data; if (Array.isArray(js)) { const row = (_a = js[rowIndex]) !== null && _a !== void 0 ? _a : {}; return row[fieldName]; } else { const column = (_b = js[fieldName]) !== null && _b !== void 0 ? _b : []; return column[rowIndex]; } } function getValueFromCif(rowIndex, fieldName, data) { const column = data.getField(fieldName); if (!column) return undefined; if (column.valueKind(rowIndex) !== db_1.Column.ValueKind.Present) return undefined; return column.str(rowIndex); } /** Return number of rows in this annotation (without parsing all the data) */ function getRowCount(data) { switch (data.format) { case 'json': return getRowCountFromJson(data.data); case 'cif': return getRowCountFromCif(data.data); } } function getRowCountFromJson(data) { const js = data; if (Array.isArray(js)) { // array of objects return js.length; } else { // object of arrays const keys = Object.keys(js); if (keys.length > 0) { return js[keys[0]].length; } else { return 0; } } } function getRowCountFromCif(data) { return data.rowCount; } function getRowsFromJson(data, schema, fieldRemapping) { const js = data; const cifSchema = (0, schemas_1.getCifAnnotationSchema)(schema); const cifSchemaKeys = Object.keys(cifSchema); if (Array.isArray(js)) { // array of objects return js.map(row => (0, object_1.pickObjectKeysWithRemapping)(row, cifSchemaKeys, fieldRemapping)); } else { // object of arrays const selectedFields = (0, object_1.pickObjectKeysWithRemapping)(js, cifSchemaKeys, fieldRemapping); return (0, object_1.objectOfArraysToArrayOfObjects)(selectedFields); } } function getRowsFromCif(data, schema, fieldRemapping) { const cifSchema = (0, schemas_1.getCifAnnotationSchema)(schema); const cifSchemaKeys = Object.keys(cifSchema); const columns = {}; for (const key of cifSchemaKeys) { let srcKey = fieldRemapping[key]; if (srcKey === null) continue; // Ignore key if (srcKey === undefined) srcKey = key; // Implicit key mapping const columnArray = getArrayFromCifCategory(data, srcKey, cifSchema[key]); // Avoiding `column.toArray` as it replaces . and ? fields by 0 or '' if (columnArray) columns[key] = columnArray; } if (Object.keys(columns).length === 0) return new Array(data.rowCount).fill({}); return (0, object_1.objectOfArraysToArrayOfObjects)(columns); } /** Load data from a specific column in a CIF category into an array. Load `.` and `?` as undefined. */ function getArrayFromCifCategory(data, columnName, columnSchema) { if (data.getField(columnName) === undefined) return undefined; const table = (0, schema_1.toTable)({ [columnName]: columnSchema }, data); // a bit dumb, I don't know how to make column directly const column = table[columnName]; return getArrayFromCifColumn(column); // Avoiding `column.toArray` as it replaces . and ? fields by 0 or '' } /** Same as `column.toArray` but reads `.` and `?` as undefined (instead of using type defaults) */ function getArrayFromCifColumn(column) { const nRows = column.rowCount; const Present = db_1.Column.ValueKind.Present; const out = new Array(nRows); for (let iRow = 0; iRow < nRows; iRow++) { out[iRow] = column.valueKind(iRow) === Present ? column.value(iRow) : undefined; } return out; } async function getFileFromSource(ctx, source, model) { switch (source.kind) { case 'source-cif': return { format: 'cif', data: getSourceFileFromModel(model) }; case 'url': const url = assets_1.Asset.getUrlAsset(ctx.assetManager, source.url); const dataType = MVSAnnotationFormatTypes[source.format]; const dataWrapper = await ctx.assetManager.resolve(url, dataType).runInContext(ctx.runtime); const rawData = dataWrapper.data; if (!rawData) throw new Error('Missing data'); switch (source.format) { case 'json': const json = JSON.parse(rawData); return { format: 'json', data: json }; case 'cif': case 'bcif': const parsed = await cif_1.CIF.parse(rawData).run(); if (parsed.isError) throw new Error(`Failed to parse ${source.format}`); return { format: 'cif', data: parsed.result }; } } } /** Like `sources.map(s => safePromise(getFileFromSource(ctx, s)))` * but downloads a repeating source only once. */ async function getFilesFromSources(ctx, sources, model) { var _a; const promises = {}; for (const src of sources) { const key = (0, json_1.canonicalJsonString)(src); (_a = promises[key]) !== null && _a !== void 0 ? _a : (promises[key] = (0, utils_1.safePromise)(getFileFromSource(ctx, src, model))); } const files = await (0, object_1.promiseAllObj)(promises); return sources.map(src => files[(0, json_1.canonicalJsonString)(src)]); } function getSourceFileFromModel(model) { if (model && mmcif_1.MmcifFormat.is(model.sourceData)) { if (model.sourceData.data.file) { return model.sourceData.data.file; } else { const frame = model.sourceData.data.frame; const block = (0, cif_1.CifBlock)(Array.from(frame.categoryNames), frame.categories, frame.header); const file = (0, cif_1.CifFile)([block]); return file; } } else { console.warn('Could not get CifFile from Model, returning empty CifFile'); return (0, cif_1.CifFile)([]); } } function annotationSourceFromSpec(s) { switch (s.source.name) { case 'url': return { kind: 'url', ...s.source.params }; case 'source-cif': return { kind: 'source-cif' }; } } /** In `array`, set value `fillValue` to all positions described by `fillRanges`. In case `array` is `null`, initialize it with length `n` prefilled with -1. */ function fillValueOnRanges(array, n, fillRanges, fillValue) { if (!fillRanges || element_ranges_1.ElementRanges.count(fillRanges) === 0) return array; const out = array !== null && array !== void 0 ? array : Array(n).fill(-1); element_ranges_1.ElementRanges.foreach(fillRanges, (from, to) => out.fill(fillValue, from, to)); return out; }