/** * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal * @author Alexander Rose */ import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior'; import { CellPack } from '../../extensions/cellpack'; import { DnatcoConfalPyramids } from '../../extensions/dnatco'; import { G3DFormat, G3dProvider } from '../../extensions/g3d/format'; import { GeometryExport } from '../../extensions/geo-export'; import { MAQualityAssessment } from '../../extensions/model-archive/quality-assessment/behavior'; import { QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior'; import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop'; import { Mp4Export } from '../../extensions/mp4-export'; import { PDBeStructureQualityReport } from '../../extensions/pdbe'; import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb'; import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure'; import { DownloadDensity } from '../../mol-plugin-state/actions/volume'; import { PresetTrajectoryHierarchy } from '../../mol-plugin-state/builder/structure/hierarchy-preset'; import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset'; import { DataFormatProvider } from '../../mol-plugin-state/formats/provider'; import { BuildInStructureFormat } from '../../mol-plugin-state/formats/structure'; import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory'; import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume'; import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params'; import { PluginStateObject } from '../../mol-plugin-state/objects'; import { StateTransforms } from '../../mol-plugin-state/transforms'; import { TrajectoryFromModelAndCoordinates } from '../../mol-plugin-state/transforms/model'; import { createPluginUI } from '../../mol-plugin-ui'; import { PluginUIContext } from '../../mol-plugin-ui/context'; import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec'; import { PluginCommands } from '../../mol-plugin/commands'; import { PluginConfig } from '../../mol-plugin/config'; import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout'; import { PluginSpec } from '../../mol-plugin/spec'; import { PluginState } from '../../mol-plugin/state'; import { StateObjectRef, StateObjectSelector } from '../../mol-state'; import { Asset } from '../../mol-util/assets'; import { Color } from '../../mol-util/color'; import '../../mol-util/polyfill'; import { ObjectKeys } from '../../mol-util/type-helpers'; export { PLUGIN_VERSION as version } from '../../mol-plugin/version'; export { setDebugMode, setProductionMode } from '../../mol-util/debug'; const CustomFormats = [ ['g3d', G3dProvider] as const ]; const Extensions = { 'cellpack': PluginSpec.Behavior(CellPack), 'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids), 'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport), 'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry), 'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport), 'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation), 'g3d': PluginSpec.Behavior(G3DFormat), 'mp4-export': PluginSpec.Behavior(Mp4Export), 'geo-export': PluginSpec.Behavior(GeometryExport), 'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment), }; const DefaultViewerOptions = { customFormats: CustomFormats as [string, DataFormatProvider][], extensions: ObjectKeys(Extensions), layoutIsExpanded: true, layoutShowControls: true, layoutShowRemoteState: true, layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay, layoutShowSequence: true, layoutShowLog: true, layoutShowLeftPanel: true, collapseLeftPanel: false, collapseRightPanel: false, disableAntialiasing: PluginConfig.General.DisableAntialiasing.defaultValue, pixelScale: PluginConfig.General.PixelScale.defaultValue, pickScale: PluginConfig.General.PickScale.defaultValue, pickPadding: PluginConfig.General.PickPadding.defaultValue, enableWboit: PluginConfig.General.EnableWboit.defaultValue, preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue, viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue, viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue, viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue, viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue, viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue, pluginStateServer: PluginConfig.State.DefaultServer.defaultValue, volumeStreamingServer: PluginConfig.VolumeStreaming.DefaultServer.defaultValue, volumeStreamingDisabled: !PluginConfig.VolumeStreaming.Enabled.defaultValue, pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue, emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue, }; type ViewerOptions = typeof DefaultViewerOptions; export class Viewer { constructor(public plugin: PluginUIContext) { } static async create(elementOrId: string | HTMLElement, options: Partial = {}) { const o = { ...DefaultViewerOptions, ...options }; const defaultSpec = DefaultPluginUISpec(); const spec: PluginUISpec = { actions: defaultSpec.actions, behaviors: [ ...defaultSpec.behaviors, ...o.extensions.map(e => Extensions[e]), ], animations: [...defaultSpec.animations || []], customParamEditors: defaultSpec.customParamEditors, customFormats: o?.customFormats, layout: { initial: { isExpanded: o.layoutIsExpanded, showControls: o.layoutShowControls, controlsDisplay: o.layoutControlsDisplay, regionState: { bottom: 'full', left: o.collapseLeftPanel ? 'collapsed' : 'full', right: o.collapseRightPanel ? 'hidden' : 'full', top: 'full', } }, }, components: { ...defaultSpec.components, controls: { ...defaultSpec.components?.controls, top: o.layoutShowSequence ? undefined : 'none', bottom: o.layoutShowLog ? undefined : 'none', left: o.layoutShowLeftPanel ? undefined : 'none', }, remoteState: o.layoutShowRemoteState ? 'default' : 'none', }, config: [ [PluginConfig.General.DisableAntialiasing, o.disableAntialiasing], [PluginConfig.General.PixelScale, o.pixelScale], [PluginConfig.General.PickScale, o.pickScale], [PluginConfig.General.PickPadding, o.pickPadding], [PluginConfig.General.EnableWboit, o.enableWboit], [PluginConfig.General.PreferWebGl1, o.preferWebgl1], [PluginConfig.Viewport.ShowExpand, o.viewportShowExpand], [PluginConfig.Viewport.ShowControls, o.viewportShowControls], [PluginConfig.Viewport.ShowSettings, o.viewportShowSettings], [PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode], [PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation], [PluginConfig.State.DefaultServer, o.pluginStateServer], [PluginConfig.State.CurrentServer, o.pluginStateServer], [PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer], [PluginConfig.VolumeStreaming.Enabled, !o.volumeStreamingDisabled], [PluginConfig.Download.DefaultPdbProvider, o.pdbProvider], [PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider], [PluginConfig.Structure.DefaultRepresentationPreset, ViewerAutoPreset.id], ] }; const element = typeof elementOrId === 'string' ? document.getElementById(elementOrId) : elementOrId; if (!element) throw new Error(`Could not get element with id '${elementOrId}'`); const plugin = await createPluginUI(element, spec, { onBeforeUIRender: plugin => { // the preset needs to be added before the UI renders otherwise // "Download Structure" wont be able to pick it up plugin.builders.structure.representation.registerPreset(ViewerAutoPreset); } }); return new Viewer(plugin); } setRemoteSnapshot(id: string) { const url = `${this.plugin.config.get(PluginConfig.State.CurrentServer)}/get/${id}`; return PluginCommands.State.Snapshots.Fetch(this.plugin, { url }); } loadSnapshotFromUrl(url: string, type: PluginState.SnapshotType) { return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type }); } loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions) { const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin); return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, { source: { name: 'url', params: { url: Asset.Url(url), format: format as any, isBinary, options: { ...params.source.params.options, representationParams: options?.representationParams as any }, } } })); } async loadAllModelsOrAssemblyFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions) { const plugin = this.plugin; const data = await plugin.builders.data.download({ url, isBinary }, { state: { isGhost: true } }); const trajectory = await plugin.builders.structure.parseTrajectory(data, format); await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'all-models', { useDefaultIfSingleModel: true, representationPresetParams: options?.representationParams }); } async loadStructureFromData(data: string | number[], format: BuiltInTrajectoryFormat, options?: { dataLabel?: string }) { const _data = await this.plugin.builders.data.rawData({ data, label: options?.dataLabel }); const trajectory = await this.plugin.builders.structure.parseTrajectory(_data, format); await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default'); } loadPdb(pdb: string, options?: LoadStructureOptions) { const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin); const provider = this.plugin.config.get(PluginConfig.Download.DefaultPdbProvider)!; return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, { source: { name: 'pdb' as const, params: { provider: { id: pdb, server: { name: provider, params: PdbDownloadProvider[provider].defaultValue as any } }, options: { ...params.source.params.options, representationParams: options?.representationParams as any }, } } })); } loadPdbDev(pdbDev: string) { const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin); return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, { source: { name: 'pdb-dev' as const, params: { provider: { id: pdbDev, encoding: 'bcif', }, options: params.source.params.options, } } })); } loadEmdb(emdb: string, options?: { detail?: number }) { const provider = this.plugin.config.get(PluginConfig.Download.DefaultEmdbProvider)!; return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadDensity, { source: { name: 'pdb-emd-ds' as const, params: { provider: { id: emdb, server: provider, }, detail: options?.detail ?? 3, } } })); } loadAlphaFoldDb(afdb: string) { const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin); return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, { source: { name: 'alphafolddb' as const, params: { id: afdb, options: { ...params.source.params.options, representation: 'preset-structure-representation-ma-quality-assessment-plddt' }, } } })); } loadModelArchive(id: string) { const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin); return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, { source: { name: 'modelarchive' as const, params: { id, options: params.source.params.options, } } })); } /** * @example Load X-ray density from volume server viewer.loadVolumeFromUrl({ url: 'https://www.ebi.ac.uk/pdbe/densities/x-ray/1tqn/cell?detail=3', format: 'dscif', isBinary: true }, [{ type: 'relative', value: 1.5, color: 0x3362B2 }, { type: 'relative', value: 3, color: 0x33BB33, volumeIndex: 1 }, { type: 'relative', value: -3, color: 0xBB3333, volumeIndex: 1 }], { entryId: ['2FO-FC', 'FO-FC'], isLazy: true }); * ********************* * @example Load EM density from volume server viewer.loadVolumeFromUrl({ url: 'https://maps.rcsb.org/em/emd-30210/cell?detail=6', format: 'dscif', isBinary: true }, [{ type: 'relative', value: 1, color: 0x3377aa }], { entryId: 'EMD-30210', isLazy: true }); */ async loadVolumeFromUrl({ url, format, isBinary }: { url: string, format: BuildInVolumeFormat, isBinary: boolean }, isovalues: VolumeIsovalueInfo[], options?: { entryId?: string | string[], isLazy?: boolean }) { const plugin = this.plugin; if (!plugin.dataFormats.get(format)) { throw new Error(`Unknown density format: ${format}`); } if (options?.isLazy) { const update = this.plugin.build(); update.toRoot().apply(StateTransforms.Data.LazyVolume, { url, format, entryId: options?.entryId, isBinary, isovalues: isovalues.map(v => ({ alpha: 1, volumeIndex: 0, ...v })) }); return update.commit(); } return plugin.dataTransaction(async () => { const data = await plugin.builders.data.download({ url, isBinary }, { state: { isGhost: true } }); const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId: options?.entryId }); const firstVolume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector; if (!firstVolume?.isOk) throw new Error('Failed to parse any volume.'); const repr = plugin.build(); for (const iso of isovalues) { repr .to(parsed.volumes?.[iso.volumeIndex ?? 0] ?? parsed.volume) .apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, firstVolume.data!, { type: 'isosurface', typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } }, color: 'uniform', colorParams: { value: iso.color } })); } await repr.commit(); }); } /** * @example * viewer.loadTrajectory({ * model: { kind: 'model-url', url: 'villin.gro', format: 'gro' }, * coordinates: { kind: 'coordinates-url', url: 'villin.xtc', format: 'xtc', isBinary: true }, * preset: 'all-models' // or 'default' * }); */ async loadTrajectory(params: LoadTrajectoryParams) { const plugin = this.plugin; let model: StateObjectSelector, coords: StateObjectSelector; if (params.model.kind === 'model-data' || params.model.kind === 'model-url') { const data = params.model.kind === 'model-data' ? await plugin.builders.data.rawData({ data: params.model.data, label: params.modelLabel }) : await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel }); const trajectory = await plugin.builders.structure.parseTrajectory(data, params.model.format ?? 'mmcif'); model = await plugin.builders.structure.createModel(trajectory); } else { const data = params.model.kind === 'topology-data' ? await plugin.builders.data.rawData({ data: params.model.data, label: params.modelLabel }) : await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel }); const provider = plugin.dataFormats.get(params.model.format); model = await provider!.parse(plugin, data); } { const data = params.coordinates.kind === 'coordinates-data' ? await plugin.builders.data.rawData({ data: params.coordinates.data, label: params.coordinatesLabel }) : await plugin.builders.data.download({ url: params.coordinates.url, isBinary: params.coordinates.isBinary, label: params.coordinatesLabel }); const provider = plugin.dataFormats.get(params.coordinates.format); coords = await provider!.parse(plugin, data); } const trajectory = await plugin.build().toRoot() .apply(TrajectoryFromModelAndCoordinates, { modelRef: model.ref, coordinatesRef: coords.ref }, { dependsOn: [model.ref, coords.ref] }) .commit(); const preset = await plugin.builders.structure.hierarchy.applyPreset(trajectory, params.preset ?? 'default'); return { model, coords, preset }; } handleResize() { this.plugin.layout.events.updated.next(void 0); } } export interface LoadStructureOptions { representationParams?: StructureRepresentationPresetProvider.CommonParams } export interface VolumeIsovalueInfo { type: 'absolute' | 'relative', value: number, color: Color, alpha?: number, volumeIndex?: number } export interface LoadTrajectoryParams { model: { kind: 'model-url', url: string, format?: BuiltInTrajectoryFormat /* mmcif */, isBinary?: boolean } | { kind: 'model-data', data: string | number[] | ArrayBuffer | Uint8Array, format?: BuiltInTrajectoryFormat /* mmcif */ } | { kind: 'topology-url', url: string, format: BuildInStructureFormat, isBinary?: boolean } | { kind: 'topology-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuildInStructureFormat }, modelLabel?: string, coordinates: { kind: 'coordinates-url', url: string, format: BuildInStructureFormat, isBinary?: boolean } | { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuildInStructureFormat }, coordinatesLabel?: string, preset?: keyof PresetTrajectoryHierarchy } export const ViewerAutoPreset = StructureRepresentationPresetProvider({ id: 'preset-structure-representation-viewer-auto', display: { name: 'Automatic (w/ Annotation)', group: 'Annotation', description: 'Show standard automatic representation but colored by quality assessment (if available in the model).' }, isApplicable(a) { return ( !!a.data.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')) || !!a.data.models.some(m => QualityAssessment.isApplicable(m, 'qmean')) ); }, params: () => StructureRepresentationPresetProvider.CommonParams, async apply(ref, params, plugin) { const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref); const structure = structureCell?.obj?.data; if (!structureCell || !structure) return {}; if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT'))) { return await QualityAssessmentPLDDTPreset.apply(ref, params, plugin); } else if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))) { return await QualityAssessmentQmeanPreset.apply(ref, params, plugin); } else { return await PresetStructureRepresentations.auto.apply(ref, params, plugin); } } });