UNPKG

pdbe-molstar

Version:
311 lines (310 loc) 16.4 kB
"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(); }