molstar
Version:
A comprehensive macromolecular library.
181 lines (180 loc) • 8.18 kB
JavaScript
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { hashFnv32a } from '../../../mol-data/util';
import { StringLike } from '../../../mol-io/common/string-like';
import { DataFormatProvider } from '../../../mol-plugin-state/formats/provider';
import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
import { Download } from '../../../mol-plugin-state/transforms/data';
import { StateAction, StateObjectRef } from '../../../mol-state';
import { Task } from '../../../mol-task';
import { Asset } from '../../../mol-util/assets';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { unzip } from '../../../mol-util/zip/zip';
import { loadMVS } from '../load';
import { MVSData } from '../mvs-data';
import { MVSTransform } from './annotation-structure-component';
/** Plugin state object storing `MVSData` */
export class Mvs extends SO.Create({ name: 'MVS Data', typeClass: 'Data' }) {
}
/** Transformer for parsing data in MVSJ format */
export const ParseMVSJ = MVSTransform({
name: 'mvs-parse-mvsj',
display: { name: 'MolViewSpec from MVSJ', description: 'Create MolViewSpec view from MVSJ data' },
from: SO.Data.String,
to: Mvs,
})({
apply({ a }, plugin) {
const mvsData = MVSData.fromMVSJ(StringLike.toString(a.data));
const sourceUrl = tryGetDownloadUrl(a, plugin);
return new Mvs({ mvsData, sourceUrl });
},
});
/** Transformer for parsing data in MVSX format (= zipped MVSJ + referenced files like structures and annotations) */
export const ParseMVSX = MVSTransform({
name: 'mvs-parse-mvsx',
display: { name: 'MolViewSpec from MVSX', description: 'Create MolViewSpec view from MVSX data' },
from: SO.Data.Binary,
to: Mvs,
params: {
mainFilePath: PD.Text('index.mvsj'),
},
})({
apply({ a, params }, plugin) {
return Task.create('Parse MVSX file', async (ctx) => {
const data = await loadMVSX(plugin, ctx, a.data, params.mainFilePath);
return new Mvs(data);
});
},
});
/** Params for the `LoadMvsData` action */
export const LoadMvsDataParams = {
appendSnapshots: PD.Boolean(false, { description: 'If true, add snapshots from MVS into current snapshot list; if false, replace the snapshot list.' }),
keepCamera: PD.Boolean(false, { description: 'If true, any camera positioning from the MVS state will be ignored and the current camera position will be kept.' }),
applyExtensions: PD.Boolean(true, { description: 'If true, apply builtin MVS-loading extensions (not a part of standard MVS specification).' }),
};
/** State action which loads a MVS view into Mol* */
export const LoadMvsData = StateAction.build({
display: { name: 'Load MVS Data' },
from: Mvs,
params: LoadMvsDataParams,
})(({ a, params }, plugin) => Task.create('Load MVS Data', async () => {
const { mvsData, sourceUrl } = a.data;
await loadMVS(plugin, mvsData, { appendSnapshots: params.appendSnapshots, keepCamera: params.keepCamera, sourceUrl: sourceUrl, extensions: params.applyExtensions ? undefined : [] });
}));
/** Data format provider for MVSJ format.
* If Visuals:On, it will load the parsed MVS view;
* otherwise it will just create a plugin state object with parsed data. */
export const MVSJFormatProvider = DataFormatProvider({
label: 'MVSJ',
description: 'MVSJ',
category: 'Miscellaneous',
stringExtensions: ['mvsj'],
parse: async (plugin, data) => {
return plugin.state.data.build().to(data).apply(ParseMVSJ).commit();
},
visuals: async (plugin, data) => {
const ref = StateObjectRef.resolveRef(data);
const params = PD.getDefaultValues(LoadMvsDataParams);
return await plugin.state.data.applyAction(LoadMvsData, params, ref).run();
},
});
/** Data format provider for MVSX format.
* If Visuals:On, it will load the parsed MVS view;
* otherwise it will just create a plugin state object with parsed data. */
export const MVSXFormatProvider = DataFormatProvider({
label: 'MVSX',
description: 'MVSX',
category: 'Miscellaneous',
binaryExtensions: ['mvsx'],
parse: async (plugin, data) => {
return plugin.state.data.build().to(data).apply(ParseMVSX).commit();
},
visuals: MVSJFormatProvider.visuals,
});
/** Parse binary data `data` as MVSX archive,
* add all contained files to `plugin`'s asset manager,
* and parse the main file in the archive as MVSJ.
* Return parsed MVS data and `sourceUrl` for resolution of relative URIs. */
export async function loadMVSX(plugin, runtimeCtx, data, mainFilePath = 'index.mvsj') {
const archiveId = `ni,fnv1a;${hashFnv32a(data)}`;
let files;
try {
files = await unzip(runtimeCtx, data);
}
catch (err) {
plugin.log.error('Invalid MVSX file');
throw err;
}
for (const path in files) {
const url = arcpUri(archiveId, path);
ensureUrlAsset(plugin.managers.asset, url, files[path]);
}
const mainFile = files[mainFilePath];
if (!mainFile)
throw new Error(`File ${mainFilePath} not found in the MVSX archive`);
const mvsData = MVSData.fromMVSJ(decodeUtf8(mainFile));
const sourceUrl = arcpUri(archiveId, mainFilePath);
return { mvsData, sourceUrl };
}
export async function loadMVSData(plugin, data, format, options) {
if (typeof data === 'string' && data.startsWith('base64')) {
data = Uint8Array.from(atob(data.substring(7)), c => c.charCodeAt(0)); // Decode base64 string to Uint8Array
}
if (format === 'mvsj') {
if (data.BYTES_PER_ELEMENT && data.buffer) {
data = new TextDecoder().decode(data); // Decode Uint8Array to string using UTF8
}
let mvsData;
if (typeof data === 'string') {
mvsData = MVSData.fromMVSJ(data);
}
else {
mvsData = data;
}
await loadMVS(plugin, mvsData, { sanityChecks: true, sourceUrl: undefined, ...options });
}
else if (format === 'mvsx') {
if (typeof data === 'string') {
throw new Error("loadMvsData: if `format` is 'mvsx', then `data` must be a Uint8Array or a base64-encoded string prefixed with 'base64,'.");
}
await plugin.runTask(Task.create('Load MVSX file', async (ctx) => {
const parsed = await loadMVSX(plugin, ctx, data);
await loadMVS(plugin, parsed.mvsData, { sanityChecks: true, sourceUrl: parsed.sourceUrl, ...options });
}));
}
else {
throw new Error(`Unknown MolViewSpec format: ${format}`);
}
}
/** If the PluginStateObject `pso` comes from a Download transform, try to get its `url` parameter. */
function tryGetDownloadUrl(pso, plugin) {
var _a;
const theCell = plugin.state.data.selectQ(q => q.ofTransformer(Download)).find(cell => cell.obj === pso);
const urlParam = (_a = theCell === null || theCell === void 0 ? void 0 : theCell.transform.params) === null || _a === void 0 ? void 0 : _a.url;
return urlParam ? Asset.getUrl(urlParam) : undefined;
}
/** Return a URI referencing a file within an archive, using ARCP scheme (https://arxiv.org/pdf/1809.06935.pdf).
* `archiveId` corresponds to the `authority` part of URI (e.g. 'uuid,EYVwjDiZhM20PWbF1OWWvQ' or 'ni,fnv1a;938511930')
* `path` corresponds to the path to a file within the archive */
function arcpUri(archiveId, path) {
return new URL(path, `arcp://${archiveId}/`).href;
}
/** Add a URL asset to asset manager.
* Skip if an asset with the same URL already exists. */
function ensureUrlAsset(manager, url, data) {
var _a;
const asset = Asset.getUrlAsset(manager, url);
if (!manager.has(asset)) {
const filename = (_a = url.split('/').pop()) !== null && _a !== void 0 ? _a : 'file';
manager.set(asset, new File([data], filename));
}
}
/** Decode bytes to text using UTF-8 encoding */
function decodeUtf8(bytes) {
_decoder !== null && _decoder !== void 0 ? _decoder : (_decoder = new TextDecoder());
return _decoder.decode(bytes);
}
let _decoder;