UNPKG

molstar

Version:

A comprehensive macromolecular library.

366 lines (365 loc) 16.4 kB
"use strict"; /** * Copyright (c) 2023-2024 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"); const cif_1 = require("../../../mol-io/reader/cif"); const schema_1 = require("../../../mol-io/reader/cif/schema"); const mmcif_1 = require("../../../mol-model-formats/structure/mmcif"); const custom_model_property_1 = require("../../../mol-model-props/common/custom-model-property"); const custom_property_1 = require("../../../mol-model/custom-property"); const array_1 = require("../../../mol-util/array"); const assets_1 = require("../../../mol-util/assets"); const json_1 = require("../../../mol-util/json"); const object_1 = require("../../../mol-util/object"); const param_choice_1 = require("../../../mol-util/param-choice"); const param_definition_1 = require("../../../mol-util/param-definition"); const atom_ranges_1 = require("../helpers/atom-ranges"); const indexing_1 = require("../helpers/indexing"); const param_definition_2 = require("../helpers/param-definition"); const schemas_1 = require("../helpers/schemas"); const selections_1 = require("../helpers/selections"); const utils_1 = require("../helpers/utils"); /** 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)(undefined, { description: 'Specify which CIF category contains annotation data (only relevant when format=cif or format=bcif)' }), 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 }; } /** Main class for processing MVS annotation */ class MVSAnnotation { constructor(data, schema) { this.data = data; this.schema = schema; /** Store mapping `ElementIndex` -> annotation row index for each `Model`, -1 means no row applies */ this.indexedModels = new Map(); this.rows = undefined; } /** 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); } static createEmpty(schema) { return new MVSAnnotation({ format: 'json', data: [] }, schema); } /** Reference implementation of `getAnnotationForLocation`, just for checking, DO NOT USE DIRECTLY */ getAnnotationForLocation_Reference(loc) { const model = loc.unit.model; const iAtom = loc.element; let result = undefined; for (const row of this.getRows()) { if ((0, selections_1.atomQualifies)(model, iAtom, row)) result = row; } return result; } /** Return value of field `fieldName` assigned to location `loc`, if any */ getValueForLocation(loc, fieldName) { const indexedModel = this.getIndexedModel(loc.unit.model); const iRow = indexedModel[loc.element]; 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) { const key = model.id; if (!this.indexedModels.has(key)) { const result = this.getRowForEachAtom(model); this.indexedModels.set(key, result); } return this.indexedModels.get(key); } /** Create `ElementIndex` -> `MVSAnnotationRow` mapping for `Model` */ getRowForEachAtom(model) { const indices = indexing_1.IndicesAndSortings.get(model); const nAtoms = model.atomicHierarchy.atoms._rowCount; const result = Array(nAtoms).fill(-1); const rows = this.getRows(); for (let i = 0, nRows = rows.length; i < nRows; i++) { const atomRanges = (0, selections_1.getAtomRangesForRow)(model, rows[i], indices); atom_ranges_1.AtomRanges.foreach(atomRanges, (from, to) => result.fill(i, from, to)); } return result; } /** Parse and return all annotation rows in this annotation */ _getRows() { switch (this.data.format) { case 'json': return getRowsFromJson(this.data.data, this.schema); case 'cif': return getRowsFromCif(this.data.data, this.schema); } } /** 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()); } } 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); } function getRowsFromJson(data, schema) { const js = data; const cifSchema = (0, schemas_1.getCifAnnotationSchema)(schema); if (Array.isArray(js)) { // array of objects return js.map(row => (0, object_1.pickObjectKeys)(row, Object.keys(cifSchema))); } else { // object of arrays const rows = []; const keys = Object.keys(js).filter(key => Object.hasOwn(cifSchema, key)); if (keys.length > 0) { const n = js[keys[0]].length; if (keys.some(key => js[key].length !== n)) throw new Error('FormatError: arrays must have the same length.'); for (let i = 0; i < n; i++) { const item = {}; for (const key of keys) { item[key] = js[key][i]; } rows.push(item); } } return rows; } } function getRowsFromCif(data, schema) { const rows = []; const cifSchema = (0, schemas_1.getCifAnnotationSchema)(schema); const table = (0, schema_1.toTable)(cifSchema, data); (0, array_1.arrayExtend)(rows, getRowsFromTable(table)); // Avoiding Table.getRows(table) as it replaces . and ? fields by 0 or '' return rows; } /** Same as `Table.getRows` but omits `.` and `?` fields (instead of using type defaults) */ function getRowsFromTable(table) { const rows = []; const columns = table._columns; const nRows = table._rowCount; const Present = db_1.Column.ValueKind.Present; for (let iRow = 0; iRow < nRows; iRow++) { const row = {}; for (const col of columns) { if (table[col].valueKind(iRow) === Present) { row[col] = table[col].value(iRow); } } rows[iRow] = row; } return rows; } 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' }; } }