pdbe-molstar
Version:
Molstar implementation for PDBe
311 lines (310 loc) • 16.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StateGalleryManager = exports.ImageCategory = void 0;
const tslib_1 = require("tslib");
const camera_1 = require("molstar/lib/mol-canvas3d/camera");
const linear_algebra_1 = require("molstar/lib/mol-math/linear-algebra");
const commands_1 = require("molstar/lib/mol-plugin/commands");
const object_1 = require("molstar/lib/mol-util/object");
const sleep_1 = require("molstar/lib/mol-util/sleep");
const rxjs_1 = require("rxjs");
const helpers_1 = require("../../helpers");
const behavior_1 = require("./behavior");
const config_1 = require("./config");
const titles_1 = require("./titles");
const string_like_1 = require("molstar/lib/mol-io/common/string-like");
/** Categories of images/states */
exports.ImageCategory = ['Entry', 'Assemblies', 'Entities', 'Ligands', 'Modified residues', 'Domains', 'Miscellaneous'];
/** Provides functionality to get list of images (3D states) for an entry, load individual images, keeps track of the currently loaded image.
* Use async `StateGalleryManager.create()` to create an instance. */
class StateGalleryManager {
constructor(plugin,
/** Entry identifier, i.e. '1cbs' */
entryId,
/** Data retrieved from API */
data,
/** Config values */
options) {
this.plugin = plugin;
this.entryId = entryId;
this.data = data;
this.options = options;
/** BehaviorSubjects for current state of the manager */
this.events = {
/** Image that has been requested to load most recently. */
requestedImage: new rxjs_1.BehaviorSubject(undefined),
/** Image that has been successfully loaded most recently. Undefined if another state has been requested since. */
loadedImage: new rxjs_1.BehaviorSubject(undefined),
/** Loading status. */
status: new rxjs_1.BehaviorSubject('ready'),
};
/** True if at least one image has been loaded (this is to skip animation on the first load) */
this.firstLoaded = false;
this.loader = new helpers_1.PreemptiveQueue((filename) => this._load(filename));
/** Cache for MOLJ states from API */
this.cache = {};
const allImages = listImages(data, true);
this.images = removeWithSuffixes(allImages, ['_side', '_top']); // removing images in different orientation than 'front'
this.filenameIndex = (0, helpers_1.createIndex)(this.images.map(img => img.filename));
this.events.status.subscribe(status => {
var _a, _b, _c;
const customState = (0, behavior_1.StateGalleryCustomState)(this.plugin);
(_a = customState.status) === null || _a === void 0 ? void 0 : _a.next(status);
if (((_b = customState.manager) === null || _b === void 0 ? void 0 : _b.value) !== this)
(_c = customState.manager) === null || _c === void 0 ? void 0 : _c.next(this);
});
this.events.requestedImage.subscribe(img => {
var _a, _b, _c;
const customState = (0, behavior_1.StateGalleryCustomState)(this.plugin);
(_a = customState.requestedImage) === null || _a === void 0 ? void 0 : _a.next(img);
if (((_b = customState.manager) === null || _b === void 0 ? void 0 : _b.value) !== this)
(_c = customState.manager) === null || _c === void 0 ? void 0 : _c.next(this);
});
}
/** Create an instance of `StateGalleryManager` and retrieve list of images from API.
* Options that are not provided will use values from plugin config. */
static create(plugin, entryId, options) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const fullOptions = Object.assign(Object.assign({}, (0, config_1.getStateGalleryConfig)(plugin)), options);
const data = yield getData(plugin, fullOptions.ServerUrl, entryId);
if (data === undefined) {
console.error(`StateGalleryManager failed to get data for entry ${entryId}`);
}
return new this(plugin, entryId, data, fullOptions);
});
}
/** Load an image (3D state). Do not call directly; use `load` instead, which handles concurrent requests. */
_load(filename) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
if (!this.plugin.canvas3d)
throw new Error('plugin.canvas3d is not defined');
let snapshot = yield this.getSnapshot(filename);
const oldCamera = getCurrentCamera(this.plugin);
const incomingCamera = getCameraFromSnapshot(snapshot); // Camera position from the MOLJ file, which may be incorrectly zoomed if viewport width < height
const newCamera = Object.assign(Object.assign({}, oldCamera), refocusCameraSnapshot(this.plugin.canvas3d.camera, incomingCamera));
snapshot = modifySnapshot(snapshot, {
removeCanvasProps: !this.options.LoadCanvasProps,
replaceCamera: {
camera: (this.options.LoadCameraOrientation && !this.firstLoaded) ? newCamera : oldCamera,
transitionDurationInMs: 0,
},
});
yield this.plugin.managers.snapshot.setStateSnapshot(JSON.parse(snapshot));
yield (0, sleep_1.sleep)(this.firstLoaded ? this.options.CameraPreTransitionMs : 0); // it is necessary to sleep even for 0 ms here, to get animation
yield commands_1.PluginCommands.Camera.Reset(this.plugin, {
snapshot: this.options.LoadCameraOrientation ? newCamera : undefined,
durationMs: this.firstLoaded ? this.options.CameraTransitionMs : 0,
});
this.firstLoaded = true;
});
}
/** Request to load an image (3D state). When there are multiple concurrent requests, some requests may be skipped (will resolve to `{ status: 'cancelled' }` or `{ status: 'skipped' }`) as only the last request is really important. */
load(img) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
var _a;
if (typeof img === 'string') {
img = (_a = this.getImageByFilename(img)) !== null && _a !== void 0 ? _a : { filename: img };
}
this.events.requestedImage.next(img);
this.events.loadedImage.next(undefined);
this.events.status.next('loading');
let result;
try {
result = yield this.loader.requestRun(img.filename);
return result;
}
finally {
if ((result === null || result === void 0 ? void 0 : result.status) === 'completed') {
this.events.loadedImage.next(img);
this.events.status.next('ready');
}
// if resolves with result.status 'cancelled' or 'skipped', keep current state
if (!result) {
this.events.status.next('error');
}
}
});
}
/** Move to next/previous image in the list. */
shift(shift) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const current = this.events.requestedImage.value;
const iCurrent = (current !== undefined) ? this.filenameIndex.get(current.filename) : undefined;
let iNew = (iCurrent !== undefined) ? (iCurrent + shift) : (shift > 0) ? (shift - 1) : shift;
iNew = (0, helpers_1.nonnegativeModulo)(iNew, this.images.length);
return yield this.load(this.images[iNew]);
});
}
/** Request to load the previous image in the list */
loadPrevious() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
return yield this.shift(-1);
});
}
/** Request to load the next image in the list */
loadNext() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
return yield this.shift(1);
});
}
/** Fetch a MOLJ state from API */
fetchSnapshot(filename) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const url = (0, helpers_1.combineUrl)(this.options.ServerUrl, `${filename}.molj`);
const data = yield this.plugin.runTask(this.plugin.fetch({ url, type: 'string' }));
return string_like_1.StringLike.toString(data);
});
}
/** Get MOLJ state for the image (get from cache or fetch from API) */
getSnapshot(filename) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
var _a;
var _b;
return (_a = (_b = this.cache)[filename]) !== null && _a !== void 0 ? _a : (_b[filename] = yield this.fetchSnapshot(filename));
});
}
/** Get full image information based on filename. Return `undefined` if image with given filename is not in the list. */
getImageByFilename(filename) {
const index = this.filenameIndex.get(filename);
if (index === undefined)
return undefined;
return this.images[index];
}
}
exports.StateGalleryManager = StateGalleryManager;
/** Get the list of images, captions etc. for an entry from API */
function getData(plugin, serverUrl, entryId) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const url = (0, helpers_1.combineUrl)(serverUrl, entryId + '.json');
try {
const text = yield plugin.runTask(plugin.fetch(url));
const data = JSON.parse(text);
return data[entryId];
}
catch (_a) {
return undefined;
}
});
}
function listImages(data, byCategory = false) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
if (byCategory) {
const out = [];
// Entry
for (const img of (_c = (_b = (_a = data === null || data === void 0 ? void 0 : data.entry) === null || _a === void 0 ? void 0 : _a.all) === null || _b === void 0 ? void 0 : _b.image) !== null && _c !== void 0 ? _c : []) {
out.push(Object.assign(Object.assign(Object.assign({}, img), { category: 'Entry' }), titles_1.ImageTitles.entry(img)));
}
// Validation
for (const img of (_g = (_f = (_e = (_d = data === null || data === void 0 ? void 0 : data.validation) === null || _d === void 0 ? void 0 : _d.geometry) === null || _e === void 0 ? void 0 : _e.deposited) === null || _f === void 0 ? void 0 : _f.image) !== null && _g !== void 0 ? _g : []) {
out.push(Object.assign(Object.assign(Object.assign({}, img), { category: 'Entry' }), titles_1.ImageTitles.validation(img)));
}
// Bfactor
for (const img of (_k = (_j = (_h = data === null || data === void 0 ? void 0 : data.entry) === null || _h === void 0 ? void 0 : _h.bfactor) === null || _j === void 0 ? void 0 : _j.image) !== null && _k !== void 0 ? _k : []) {
out.push(Object.assign(Object.assign(Object.assign({}, img), { category: 'Entry' }), titles_1.ImageTitles.bfactor(img)));
}
// Assembly
const assemblies = data === null || data === void 0 ? void 0 : data.assembly;
for (const assemblyId in assemblies) {
for (const img of (_l = assemblies[assemblyId].image) !== null && _l !== void 0 ? _l : []) {
out.push(Object.assign(Object.assign(Object.assign({}, img), { category: 'Assemblies' }), titles_1.ImageTitles.assembly(img, { assemblyId })));
}
}
// Entity
const entities = data === null || data === void 0 ? void 0 : data.entity;
for (const entityId in entities) {
for (const img of (_m = entities[entityId].image) !== null && _m !== void 0 ? _m : []) {
out.push(Object.assign(Object.assign(Object.assign({}, img), { category: 'Entities' }), titles_1.ImageTitles.entity(img, { entityId })));
}
}
// Ligand
const ligands = (_o = data === null || data === void 0 ? void 0 : data.entry) === null || _o === void 0 ? void 0 : _o.ligands;
for (const compId in ligands) {
for (const img of (_p = ligands[compId].image) !== null && _p !== void 0 ? _p : []) {
out.push(Object.assign(Object.assign(Object.assign({}, img), { category: 'Ligands' }), titles_1.ImageTitles.ligand(img, { compId })));
}
}
// Modres
const modres = (_q = data === null || data === void 0 ? void 0 : data.entry) === null || _q === void 0 ? void 0 : _q.mod_res;
for (const compId in modres) {
for (const img of (_r = modres[compId].image) !== null && _r !== void 0 ? _r : []) {
out.push(Object.assign(Object.assign(Object.assign({}, img), { category: 'Modified residues' }), titles_1.ImageTitles.modres(img, { compId })));
}
}
// Domain
for (const entityId in entities) {
const dbs = entities[entityId].database;
for (const db in dbs) {
const domains = dbs[db];
for (const familyId in domains) {
for (const img of (_s = domains[familyId].image) !== null && _s !== void 0 ? _s : []) {
out.push(Object.assign(Object.assign(Object.assign({}, img), { category: 'Domains' }), titles_1.ImageTitles.domain(img, { db, familyId, entityId })));
}
}
}
}
// Any other potential images not caught in categories above
pushImages(out, data);
return (0, helpers_1.distinct)(out, img => img.filename);
}
else {
return pushImages([], data);
}
}
function pushImages(out, data) {
if ((0, object_1.isPlainObject)(data)) {
for (const key in data) {
const value = data[key];
if (key === 'image' && Array.isArray(value)) {
out.push(...value);
}
else {
pushImages(out, value);
}
}
}
return out;
}
/** Return a filtered list of images, removing all images with filename ending in one of `suffixes` */
function removeWithSuffixes(images, suffixes) {
return images.filter(img => !suffixes.some(suffix => img.filename.endsWith(suffix)));
}
function modifySnapshot(snapshot, options) {
var _a;
const json = JSON.parse(snapshot);
for (const entry of (_a = json.entries) !== null && _a !== void 0 ? _a : []) {
if (entry.snapshot) {
if (options.removeCanvasProps && entry.snapshot.canvas3d) {
delete entry.snapshot.canvas3d.props;
}
if (options.replaceCamera) {
const { camera, transitionDurationInMs } = options.replaceCamera;
entry.snapshot.camera = {
current: camera,
transitionStyle: transitionDurationInMs > 0 ? 'animate' : 'instant',
transitionDurationInMs: transitionDurationInMs > 0 ? transitionDurationInMs : undefined,
};
}
}
}
return JSON.stringify(json);
}
function getCameraFromSnapshot(snapshot) {
var _a, _b, _c, _d;
const json = JSON.parse(snapshot);
return (_d = (_c = (_b = (_a = json === null || json === void 0 ? void 0 : json.entries) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.snapshot) === null || _c === void 0 ? void 0 : _c.camera) === null || _d === void 0 ? void 0 : _d.current;
}
/** Recalculate camera distance from target in `snapshot` based on `snapshot.radius`,
* keeping target, direction, and up from snapshot but using camera mode and FOV from `camera`. */
function refocusCameraSnapshot(camera, snapshot) {
if (snapshot === undefined)
return undefined;
const dir = linear_algebra_1.Vec3.sub((0, linear_algebra_1.Vec3)(), snapshot.target, snapshot.position);
return camera.getInvariantFocus(snapshot.target, snapshot.radius, snapshot.up, dir);
}
/** Get current camera positioning */
function getCurrentCamera(plugin) {
if (!plugin.canvas3d)
return camera_1.Camera.createDefaultSnapshot();
plugin.canvas3d.commit();
return plugin.canvas3d.camera.getSnapshot();
}