Skip to content

Commit 453e313

Browse files
committed
State gallery: Config
1 parent c69c7f4 commit 453e313

File tree

6 files changed

+94
-55
lines changed

6 files changed

+94
-55
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file, following t
77
- Added options `leftPanel`, `rightPanel`, `logPanel`
88
- All color options accept color names and hexcodes
99
- `visualStyle` option allows per-component specification
10+
- StateGallery extension (work in progress)
1011

1112
## [v3.2.0] - 2024-04-24
1213
- Mol* core dependency updated to 3.45.0

src/app/extensions/state-gallery/behavior.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { Model, Structure } from 'molstar/lib/mol-model/structure';
21
import { PluginBehavior } from 'molstar/lib/mol-plugin/behavior';
3-
import { StateGalleryControls } from './ui';
42
import { StateGalleryManager } from './manager';
3+
import { StateGalleryControls } from './ui';
54

65

76
/** All public functions provided by the StateGallery extension */
@@ -60,9 +59,3 @@ export const StateGallery = PluginBehavior.create<{ autoAttach: boolean }>({
6059
// serverUrl: PD.Text(AssemblySymmetryData.DefaultServerUrl)
6160
// })
6261
});
63-
64-
65-
// TODO move elsewhere
66-
export function isApplicable(structure?: Structure): boolean {
67-
return !!structure && structure.models.length === 1 && Model.hasPdbId(structure.models[0]);
68-
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { PluginConfigItem } from 'molstar/lib/mol-plugin/config';
2+
import { PluginContext } from 'molstar/lib/mol-plugin/context';
3+
import { PluginConfigUtils } from '../../helpers';
4+
5+
6+
export const StateGalleryConfigDefaults = {
7+
/** Base URL of the state API (list of states will be downloaded from `{ServerUrl}/{entryId}.json`, states from `{ServerUrl}/{stateName}.molj`) */
8+
ServerUrl: 'https://www.ebi.ac.uk/pdbe/static/entry',
9+
/** Load canvas properties, such as background, axes indicator, fog, outline (if false, keep current canvas properties) */
10+
LoadCanvasProps: false,
11+
/** Load camera orientation when loading state (if false, keep current orientation) */
12+
LoadCameraOrientation: true,
13+
/** Time in miliseconds between loading state and starting camera transition */
14+
CameraPreTransitionMs: 100,
15+
/** Duration of the camera transition in miliseconds */
16+
CameraTransitionMs: 400,
17+
};
18+
export type StateGalleryConfigValues = typeof StateGalleryConfigDefaults;
19+
20+
export const StateGalleryConfig: PluginConfigUtils.ConfigFor<StateGalleryConfigValues> = {
21+
ServerUrl: new PluginConfigItem<string>('pdbe-state-gallery.server-url', StateGalleryConfigDefaults.ServerUrl),
22+
LoadCanvasProps: new PluginConfigItem<boolean>('pdbe-state-gallery.load-canvas-props', StateGalleryConfigDefaults.LoadCanvasProps),
23+
LoadCameraOrientation: new PluginConfigItem<boolean>('pdbe-state-gallery.load-camera-orientation', StateGalleryConfigDefaults.LoadCameraOrientation),
24+
CameraPreTransitionMs: new PluginConfigItem<number>('pdbe-state-gallery.camera-pre-transition-ms', StateGalleryConfigDefaults.CameraPreTransitionMs),
25+
CameraTransitionMs: new PluginConfigItem<number>('pdbe-state-gallery.camera-transition-ms', StateGalleryConfigDefaults.CameraTransitionMs),
26+
};
27+
28+
export function getStateGalleryConfig(plugin: PluginContext): StateGalleryConfigValues {
29+
return PluginConfigUtils.getConfigValues(plugin, StateGalleryConfig, StateGalleryConfigDefaults);
30+
}

src/app/extensions/state-gallery/manager.ts

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@ import { isPlainObject } from 'molstar/lib/mol-util/object';
77
import { sleep } from 'molstar/lib/mol-util/sleep';
88
import { BehaviorSubject } from 'rxjs';
99
import { PreemptiveQueue, PreemptiveQueueResult, combineUrl, distinct } from '../../helpers';
10-
11-
12-
const LOAD_BACKGROUND = false; // TODO put to config+param
13-
const LOAD_CAMERA_ORIENTATION = true; // TODO put to config+param
14-
const CAMERA_PRE_TRANSITION_MS = 100; // TODO put to config+param
15-
const CAMERA_TRANSITION_MS = 400; // TODO put to config+param
10+
import { StateGalleryConfigValues, getStateGalleryConfig } from './config';
1611

1712

1813
export interface StateGalleryData {
@@ -81,23 +76,26 @@ export class StateGalleryManager {
8176
public readonly images: Image[];
8277
public readonly requestedStateName = new BehaviorSubject<string | undefined>(undefined);
8378
public readonly loadedStateName = new BehaviorSubject<string | undefined>(undefined);
79+
/** True if at least one state has been loaded (this is to skip animation on the first load) */
8480
private firstLoaded = false;
8581

8682
private constructor(
8783
public readonly plugin: PluginContext,
88-
public readonly serverUrl: string,
8984
public readonly entryId: string,
9085
public readonly data: StateGalleryData | undefined,
86+
public readonly options: StateGalleryConfigValues,
9187
) {
92-
this.images = removeWithSuffixes(listImages(data, true), ['_side', '_top']); // TODO allow suffixes by a parameter, sort by parameter
88+
const allImages = listImages(data, true);
89+
this.images = removeWithSuffixes(allImages, ['_side', '_top']); // removing images in different orientation than 'front'
9390
}
9491

95-
static async create(plugin: PluginContext, serverUrl: string, entryId: string) {
96-
const data = await getData(plugin, serverUrl, entryId);
92+
static async create(plugin: PluginContext, entryId: string, options?: Partial<StateGalleryConfigValues>) {
93+
const fullOptions = { ...getStateGalleryConfig(plugin), ...options };
94+
const data = await getData(plugin, fullOptions.ServerUrl, entryId);
9795
if (data === undefined) {
9896
console.error(`StateGalleryManager failed to get data for entry ${entryId}`);
9997
}
100-
return new this(plugin, serverUrl, entryId, data);
98+
return new this(plugin, entryId, data, fullOptions);
10199
}
102100

103101
private async _load(filename: string): Promise<void> {
@@ -108,17 +106,18 @@ export class StateGalleryManager {
108106
const incomingCamera = getCameraFromSnapshot(snapshot); // Camera position from the MOLJ file, which may be incorrectly zoomed if viewport width < height
109107
const newCamera: Camera.Snapshot = { ...oldCamera, ...refocusCameraSnapshot(this.plugin.canvas3d.camera, incomingCamera) };
110108
snapshot = modifySnapshot(snapshot, {
111-
removeBackground: !LOAD_BACKGROUND,
109+
removeCanvasProps: !this.options.LoadCanvasProps,
112110
replaceCamera: {
113-
camera: (LOAD_CAMERA_ORIENTATION && !this.firstLoaded) ? newCamera : oldCamera,
111+
camera: (this.options.LoadCameraOrientation && !this.firstLoaded) ? newCamera : oldCamera,
114112
transitionDurationInMs: 0,
115113
}
116114
});
117-
const file = new File([snapshot], `${filename}.molj`);
118-
await PluginCommands.State.Snapshots.OpenFile(this.plugin, { file });
119-
// await this.plugin.managers.snapshot.setStateSnapshot(JSON.parse(data));
120-
await sleep(this.firstLoaded ? CAMERA_PRE_TRANSITION_MS : 0); // it is necessary to sleep even for 0 ms here, to get animation
121-
await PluginCommands.Camera.Reset(this.plugin, { snapshot: LOAD_CAMERA_ORIENTATION ? newCamera : undefined, durationMs: this.firstLoaded ? CAMERA_TRANSITION_MS : 0 });
115+
await this.plugin.managers.snapshot.setStateSnapshot(JSON.parse(snapshot));
116+
await sleep(this.firstLoaded ? this.options.CameraPreTransitionMs : 0); // it is necessary to sleep even for 0 ms here, to get animation
117+
await PluginCommands.Camera.Reset(this.plugin, {
118+
snapshot: this.options.LoadCameraOrientation ? newCamera : undefined,
119+
durationMs: this.firstLoaded ? this.options.CameraTransitionMs : 0,
120+
});
122121

123122
this.firstLoaded = true;
124123
}
@@ -135,7 +134,7 @@ export class StateGalleryManager {
135134

136135
private readonly cache: { [filename: string]: string } = {};
137136
private async fetchSnapshot(filename: string): Promise<string> {
138-
const url = combineUrl(this.serverUrl, `${filename}.molj`);
137+
const url = combineUrl(this.options.ServerUrl, `${filename}.molj`);
139138
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'string' }));
140139
return data;
141140
}
@@ -145,7 +144,7 @@ export class StateGalleryManager {
145144
}
146145

147146

148-
async function getData(plugin: PluginContext, serverUrl: string, entryId: string) {
147+
async function getData(plugin: PluginContext, serverUrl: string, entryId: string): Promise<StateGalleryData | undefined> {
149148
const url = combineUrl(serverUrl, entryId + '.json');
150149
try {
151150
const text = await plugin.runTask(plugin.fetch(url));
@@ -161,7 +160,6 @@ function listImages(data: StateGalleryData | undefined, byCategory: boolean = fa
161160
const out: Image[] = [];
162161

163162
// Entry
164-
// out.push(...data?.entry?.all?.image ?? []);
165163
for (const img of data?.entry?.all?.image ?? []) {
166164
const title = img.filename.includes('_chemically_distinct_molecules')
167165
? 'Deposited model (color by entity)'
@@ -171,19 +169,16 @@ function listImages(data: StateGalleryData | undefined, byCategory: boolean = fa
171169
out.push({ ...img, category: 'Entry', simple_title: title });
172170
}
173171
// Validation
174-
// out.push(...data?.validation?.geometry?.deposited?.image ?? []);
175172
for (const img of data?.validation?.geometry?.deposited?.image ?? []) {
176173
out.push({ ...img, category: 'Entry', simple_title: 'Geometry validation' });
177174
}
178175
// Bfactor
179-
// out.push(...data?.entry?.bfactor?.image ?? []);
180176
for (const img of data?.entry?.bfactor?.image ?? []) {
181177
out.push({ ...img, category: 'Entry', simple_title: 'B-factor' });
182178
}
183179
// Assembly
184180
const assemblies = data?.assembly;
185181
for (const ass in assemblies) {
186-
// out.push(...assemblies[ass].image);
187182
for (const img of assemblies[ass].image ?? []) {
188183
const title = img.filename.includes('_chemically_distinct_molecules')
189184
? `Assembly ${ass} (color by entity)`
@@ -196,23 +191,20 @@ function listImages(data: StateGalleryData | undefined, byCategory: boolean = fa
196191
// Entity
197192
const entities = data?.entity;
198193
for (const entity in entities) {
199-
// out.push(...entities[entity].image);
200194
for (const img of entities[entity].image ?? []) {
201195
out.push({ ...img, category: 'Entities', simple_title: `Entity ${entity}` });
202196
}
203197
}
204198
// Ligand
205199
const ligands = data?.entry?.ligands;
206200
for (const ligand in ligands) {
207-
// out.push(...ligands[ligand].image);
208201
for (const img of ligands[ligand].image ?? []) {
209202
out.push({ ...img, category: 'Ligands', simple_title: `Ligand environment for ${ligand}` });
210203
}
211204
}
212205
// Modres
213206
const modres = data?.entry?.mod_res;
214207
for (const res in modres) {
215-
// out.push(...modres[res].image);
216208
for (const img of modres[res].image ?? []) {
217209
out.push({ ...img, category: 'Modified residues', simple_title: `Modified residue ${res}` });
218210
}
@@ -223,9 +215,8 @@ function listImages(data: StateGalleryData | undefined, byCategory: boolean = fa
223215
for (const db in dbs) {
224216
const domains = dbs[db];
225217
for (const domain in domains) {
226-
// out.push(...domains[domain].image);
227218
for (const img of domains[domain].image ?? []) {
228-
out.push({ ...img, category: 'Domains', simple_title: `${db} ${domain} in entity ${entity}` });
219+
out.push({ ...img, category: 'Domains', simple_title: `${db} ${domain} (entity ${entity})` });
229220
}
230221
}
231222
}
@@ -257,12 +248,12 @@ function removeWithSuffixes(images: Image[], suffixes: string[]): Image[] {
257248
return images.filter(img => !suffixes.some(suffix => img.filename.endsWith(suffix)));
258249
}
259250

260-
function modifySnapshot(snapshot: string, options: { removeBackground?: boolean, replaceCamera?: { camera: Camera.Snapshot, transitionDurationInMs: number } }) {
251+
function modifySnapshot(snapshot: string, options: { removeCanvasProps?: boolean, replaceCamera?: { camera: Camera.Snapshot, transitionDurationInMs: number } }) {
261252
const json = JSON.parse(snapshot) as PluginStateSnapshotManager.StateSnapshot;
262253
for (const entry of json.entries ?? []) {
263254
if (entry.snapshot) {
264-
if (options.removeBackground) {
265-
delete entry.snapshot.canvas3d;
255+
if (options.removeCanvasProps && entry.snapshot.canvas3d) {
256+
delete entry.snapshot.canvas3d.props;
266257
}
267258
if (options.replaceCamera) {
268259
const { camera, transitionDurationInMs } = options.replaceCamera;

src/app/extensions/state-gallery/ui.tsx

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
66
import React from 'react';
77
import { createIndex, groupElements, nonnegativeModulo } from '../../helpers';
88
import { ChevronLeftSvg, ChevronRightSvg, CollectionsOutlinedSvg, EmptyIconSvg, HourglassBottomSvg } from '../../ui/icons';
9-
import { StateGalleryManager } from './manager';
9+
import { Image, StateGalleryManager } from './manager';
1010

1111

1212
interface StateGalleryControlsState {
@@ -79,7 +79,7 @@ export class StateGalleryControls extends CollapsableControls<{}, StateGalleryCo
7979
};
8080
private load = async () => {
8181
this.setState({ isLoading: true, description: undefined });
82-
const manager = await StateGalleryManager.create(this.plugin, 'https://www.ebi.ac.uk/pdbe/static/entry', this.state.entryId);
82+
const manager = await StateGalleryManager.create(this.plugin, this.state.entryId);
8383
this.setState({ manager, isLoading: false, description: this.state.entryId.toUpperCase() });
8484
};
8585
private onChangeValues = (values: Values) => {
@@ -104,10 +104,15 @@ export class StateGalleryControls extends CollapsableControls<{}, StateGalleryCo
104104

105105
function ManagerControls(props: { manager: StateGalleryManager }) {
106106
const images = props.manager.images;
107-
const categories = React.useMemo(() => groupElements(images, img => img.category ?? 'Miscellaneous'), [images]);
108-
const imageIndex = React.useMemo(() => createIndex(images), [images]);
109107
const nImages = images.length;
110-
const [selected, setSelected] = React.useState(images[0]);
108+
109+
if (nImages === 0) {
110+
return <div style={{ margin: 8 }}>No data available for {props.manager.entryId}.</div>;
111+
}
112+
113+
const imageIndex = React.useMemo(() => createIndex(images), [images]);
114+
const categories = React.useMemo(() => groupElements(images, img => img.category ?? 'Miscellaneous'), [images]);
115+
const [selected, setSelected] = React.useState<Image>(images[0]);
111116
const [status, setStatus] = React.useState<'ready' | 'loading' | 'error'>('ready');
112117

113118
React.useEffect(() => {
@@ -119,7 +124,6 @@ function ManagerControls(props: { manager: StateGalleryManager }) {
119124

120125
const keyDownTargetRef = React.useRef<HTMLDivElement>(null);
121126
React.useEffect(() => keyDownTargetRef.current?.focus(), []);
122-
const selectedStateIcon = (status === 'loading') ? HourglassBottomSvg : (status === 'error') ? ErrorSvg : CheckSvg;
123127

124128
const shift = (x: number) => setSelected(old => {
125129
const oldIndex = imageIndex.get(old) ?? 0;
@@ -133,20 +137,12 @@ function ManagerControls(props: { manager: StateGalleryManager }) {
133137
if (e.code === 'ArrowRight') selectNext();
134138
};
135139

136-
if (nImages === 0) {
137-
return <div style={{ margin: 8 }}>No data available for {props.manager.entryId}.</div>;
138-
}
139-
140140
return <div className='pdbemolstar-state-gallery-controls' onKeyDown={handleKeyDown} tabIndex={-1} ref={keyDownTargetRef} >
141141
<ExpandGroup header='States' initiallyExpanded={true} key='states'>
142142
{categories.groups.map(cat =>
143143
<ExpandGroup header={cat} key={cat} initiallyExpanded={true} >
144144
{categories.members.get(cat)?.map(img =>
145-
<Button key={img.filename} className='msp-action-menu-button' onClick={() => setSelected(img)} title={img.simple_title ?? img.filename}
146-
icon={img === selected ? selectedStateIcon : EmptyIconSvg}
147-
style={{ height: 24, lineHeight: '24px', textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', fontWeight: img === selected ? 'bold' : undefined }}>
148-
{img.category ?? 'Miscellaneous'}: {img.simple_title ?? img.filename}
149-
</Button>
145+
<StateButton key={img.filename} img={img} isSelected={img === selected} status={status} onClick={() => setSelected(img)} />
150146
)}
151147
</ExpandGroup>
152148
)}
@@ -165,3 +161,17 @@ function ManagerControls(props: { manager: StateGalleryManager }) {
165161
</div >
166162
</div>;
167163
}
164+
165+
function StateButton(props: { img: Image, isSelected: boolean, status: 'ready' | 'loading' | 'error', onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void }) {
166+
const { img, isSelected, status, onClick } = props;
167+
const icon = !isSelected ? EmptyIconSvg : (status === 'loading') ? HourglassBottomSvg : (status === 'error') ? ErrorSvg : CheckSvg;
168+
const title = img.simple_title ?? img.filename;
169+
const errorMsg = (isSelected && status === 'error') ? '[Failed to load] ' : '';
170+
return <Button className='msp-action-menu-button'
171+
icon={icon}
172+
onClick={onClick}
173+
title={`${errorMsg}${title}`}
174+
style={{ height: 24, lineHeight: '24px', textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', fontWeight: isSelected ? 'bold' : undefined }}>
175+
{title}
176+
</Button>;
177+
}

src/app/helpers.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { StructureRef } from 'molstar/lib/mol-plugin-state/manager/structure/hie
77
import { StateTransforms } from 'molstar/lib/mol-plugin-state/transforms';
88
import { CreateVolumeStreamingInfo } from 'molstar/lib/mol-plugin/behavior/dynamic/volume-streaming/transformers';
99
import { PluginCommands } from 'molstar/lib/mol-plugin/commands';
10+
import { PluginConfigItem } from 'molstar/lib/mol-plugin/config';
1011
import { PluginContext } from 'molstar/lib/mol-plugin/context';
1112
import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
1213
import { Expression } from 'molstar/lib/mol-script/language/expression';
@@ -606,3 +607,16 @@ export class PreemptiveQueue<X, Y> {
606607
}
607608
}
608609
}
610+
611+
612+
export namespace PluginConfigUtils {
613+
export type ConfigFor<T> = { [key in keyof T]: PluginConfigItem<T[key]> }
614+
615+
export function getConfigValues<T>(plugin: PluginContext | undefined, configItems: { [name in keyof T]: PluginConfigItem<T[name]> }, defaults: T): T {
616+
const values = {} as T;
617+
for (const name in configItems) {
618+
values[name] = plugin?.config.get(configItems[name]) ?? defaults[name];
619+
}
620+
return values;
621+
}
622+
}

0 commit comments

Comments
 (0)