Переглянути джерело

Merge branch 'master' of https://github.com/molstar/molstar-proto

Alexander Rose 6 роки тому
батько
коміт
2448ca8ad4

+ 5 - 0
src/mol-math/linear-algebra/3d/vec3.ts

@@ -452,9 +452,14 @@ namespace Vec3 {
     }
 
     const rotTemp = zero();
+    const flipScaling = create(-1, -1, -1);
     export function makeRotation(mat: Mat4, a: Vec3, b: Vec3): Mat4 {
         const by = angle(a, b);
         if (Math.abs(by) < 0.0001) return Mat4.setIdentity(mat);
+        if (Math.abs(by - Math.PI) < EPSILON.Value) {
+            // here, axis can be [0,0,0] but the rotation is a simple flip
+            return Mat4.fromScaling(mat, flipScaling);
+        }
         const axis = cross(rotTemp, a, b);
         return Mat4.fromRotation(mat, by, axis);
     }

+ 1 - 19
src/mol-model/loci.ts

@@ -81,25 +81,7 @@ namespace Loci {
         if (loci.kind === 'structure-loci') {
             return Sphere3D.copy(boundingSphere, loci.structure.boundary.sphere)
         } else if (loci.kind === 'element-loci') {
-            for (const e of loci.elements) {
-                const { indices } = e;
-                const pos = e.unit.conformation.position;
-                const { elements } = e.unit;
-                for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
-                    pos(elements[OrderedSet.getAt(indices, i)], tempPos);
-                    sphereHelper.includeStep(tempPos);
-                }
-            }
-            sphereHelper.finishedIncludeStep();
-            for (const e of loci.elements) {
-                const { indices } = e;
-                const pos = e.unit.conformation.position;
-                const { elements } = e.unit;
-                for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
-                    pos(elements[OrderedSet.getAt(indices, i)], tempPos);
-                    sphereHelper.radiusStep(tempPos);
-                }
-            }
+            return StructureElement.Loci.getBoundary(loci).sphere;
         } else if (loci.kind === 'link-loci') {
             for (const e of loci.links) {
                 e.aUnit.conformation.position(e.aUnit.elements[e.aIndex], tempPos);

+ 64 - 0
src/mol-model/structure/structure/element.ts

@@ -9,6 +9,9 @@ import Unit from './unit'
 import { ElementIndex } from '../model';
 import { ResidueIndex, ChainIndex } from '../model/indexing';
 import Structure from './structure';
+import { Boundary } from './util/boundary';
+import { BoundaryHelper } from 'mol-math/geometry/boundary-helper';
+import { Vec3 } from 'mol-math/linear-algebra';
 
 interface StructureElement<U = Unit> {
     readonly kind: 'element-location',
@@ -176,6 +179,67 @@ namespace StructureElement {
 
             return false;
         }
+
+        export function extendToWholeResidues(loci: Loci): Loci {
+            const elements: Loci['elements'][0][] = [];
+
+            for (const lociElement of loci.elements) {
+                if (lociElement.unit.kind !== Unit.Kind.Atomic) elements[elements.length] = lociElement;
+
+                const unitElements = lociElement.unit.elements;
+                const h = lociElement.unit.model.atomicHierarchy;
+
+                const { index: residueIndex, offsets: residueOffsets } = h.residueAtomSegments;
+
+                const newIndices: UnitIndex[] = [];
+                const indices = lociElement.indices, len = OrderedSet.size(indices);
+                let i = 0;
+                while (i < len) {
+                    const rI = residueIndex[unitElements[OrderedSet.getAt(indices, i)]];
+                    while (i < len && residueIndex[unitElements[OrderedSet.getAt(indices, i)]] === rI) {
+                        i++;
+                    }
+
+                    for (let j = residueOffsets[rI], _j = residueOffsets[rI + 1]; j < _j; j++) {
+                        const idx = OrderedSet.indexOf(unitElements, j);
+                        if (idx >= 0) newIndices[newIndices.length] = idx as UnitIndex;
+                    }
+                }
+
+                elements[elements.length] = { unit: lociElement.unit, indices: SortedArray.ofSortedArray(newIndices) };
+            }
+
+            return Loci(loci.structure, elements);
+        }
+
+        const boundaryHelper = new BoundaryHelper(), tempPos = Vec3.zero();
+        export function getBoundary(loci: Loci): Boundary {
+            boundaryHelper.reset(0);
+
+            for (const e of loci.elements) {
+                const { indices } = e;
+                const pos = e.unit.conformation.position, r = e.unit.conformation.r;
+                const { elements } = e.unit;
+                for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
+                    const eI = elements[OrderedSet.getAt(indices, i)];
+                    pos(eI, tempPos);
+                    boundaryHelper.boundaryStep(tempPos, r(eI));
+                }
+            }
+            boundaryHelper.finishBoundaryStep();
+            for (const e of loci.elements) {
+                const { indices } = e;
+                const pos = e.unit.conformation.position, r = e.unit.conformation.r;
+                const { elements } = e.unit;
+                for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
+                    const eI = elements[OrderedSet.getAt(indices, i)];
+                    pos(eI, tempPos);
+                    boundaryHelper.extendStep(tempPos, r(eI));
+                }
+            }
+
+            return { box: boundaryHelper.getBox(), sphere: boundaryHelper.getSphere() };
+        }
     }
 }
 

+ 20 - 0
src/mol-plugin/behavior/behavior.ts

@@ -136,4 +136,24 @@ namespace PluginBehavior {
         constructor(protected ctx: PluginContext, protected params: P) {
         }
     }
+
+    export abstract class WithSubscribers<P = { }> implements PluginBehavior<P> {
+        abstract register(ref: string): void;
+
+        private subs: PluginCommand.Subscription[] = [];
+        protected subscribeCommand<T>(cmd: PluginCommand<T>, action: PluginCommand.Action<T>) {
+            this.subs.push(cmd.subscribe(this.plugin, action));
+        }
+        protected subscribeObservable<T>(o: Observable<T>, action: (v: T) => void) {
+            this.subs.push(o.subscribe(action));
+        }
+
+        unregister() {
+            for (const s of this.subs) s.unsubscribe();
+            this.subs = [];
+        }
+
+        constructor(protected plugin: PluginContext) {
+        }
+    }
 }

+ 232 - 0
src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts

@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginBehavior } from 'mol-plugin/behavior';
+import { PluginStateObject } from 'mol-plugin/state/objects';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { VolumeServerInfo, VolumeServerHeader } from './model';
+import { createIsoValueParam } from 'mol-repr/volume/isosurface';
+import { VolumeIsoValue, VolumeData } from 'mol-model/volume';
+import { Color } from 'mol-util/color';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { PluginContext } from 'mol-plugin/context';
+import { LRUCache } from 'mol-util/lru-cache';
+import CIF from 'mol-io/reader/cif';
+import { Box3D } from 'mol-math/geometry';
+import { urlCombine } from 'mol-util/url';
+import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
+import { StructureElement } from 'mol-model/structure';
+import { CreateVolumeStreamingBehavior } from './transformers';
+
+export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { }
+
+export namespace VolumeStreaming {
+    function channelParam(label: string, color: Color, defaultValue: VolumeIsoValue, stats: VolumeData['dataStats']) {
+        return PD.Group({
+            isoValue: createIsoValueParam(defaultValue, stats),
+            color: PD.Color(color),
+            opacity: PD.Numeric(0.3, { min: 0, max: 1, step: 0.01 })
+        }, { label, isExpanded: true });
+    }
+
+    const fakeSampling: VolumeServerHeader.Sampling = {
+        byteOffset: 0,
+        rate: 1,
+        sampleCount: [1, 1, 1],
+        valuesInfo: [{ mean: 0, min: -1, max: 1, sigma: 0.1 }, { mean: 0, min: -1, max: 1, sigma: 0.1 }]
+    };
+
+    export function createParams(data?: VolumeServerInfo.Data) {
+        // fake the info
+        const info = data || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: VolumeIsoValue.relative(0) };
+
+        return {
+            view: PD.MappedStatic('selection-box', {
+                'box': PD.Group({
+                    bottomLeft: PD.Vec3(Vec3.create(-22.4, -33.4, -21.6)),
+                    topRight: PD.Vec3(Vec3.create(-7.1, -10, -0.9)),
+                }, { description: 'Static box defined by cartesian coords.', isFlat: true }),
+                'selection-box': PD.Group({
+                    radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }),
+                    bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }),
+                    topRight: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }),
+                }, { description: 'Box around last-interacted element.', isFlat: true }),
+                'cell': PD.Group({}),
+                // 'auto': PD.Group({  }), // based on camera distance/active selection/whatever, show whole structure or slice.
+            }, { options: [['box', 'Bounded Box'], ['selection-box', 'Selection'], ['cell', 'Whole Structure']] }),
+            detailLevel: PD.Select<number>(Math.min(1, info.header.availablePrecisions.length - 1),
+                info.header.availablePrecisions.map((p, i) => [i, `${i + 1} (${Math.pow(p.maxVoxels, 1 / 3) | 0}^3)`] as [number, string])),
+            channels: info.kind === 'em'
+                ? PD.Group({
+                    'em': channelParam('EM', Color(0x638F8F), info.emDefaultContourLevel || VolumeIsoValue.relative(1), info.header.sampling[0].valuesInfo[0])
+                }, { isFlat: true })
+                : PD.Group({
+                    '2fo-fc': channelParam('2Fo-Fc', Color(0x3362B2), VolumeIsoValue.relative(1.5), info.header.sampling[0].valuesInfo[0]),
+                    'fo-fc(+ve)': channelParam('Fo-Fc(+ve)', Color(0x33BB33), VolumeIsoValue.relative(3), info.header.sampling[0].valuesInfo[1]),
+                    'fo-fc(-ve)': channelParam('Fo-Fc(-ve)', Color(0xBB3333), VolumeIsoValue.relative(-3), info.header.sampling[0].valuesInfo[1]),
+                }, { isFlat: true })
+        };
+    }
+
+    type RT = typeof createParams extends (...args: any[]) => (infer T) ? T : never
+    export type Params = RT extends PD.Params ? PD.Values<RT> : {}
+
+    type ChannelsInfo = { [name in ChannelType]?: { isoValue: VolumeIsoValue, color: Color, opacity: number } }
+    type ChannelsData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: VolumeData }
+
+    export type ChannelType = 'em' | '2fo-fc' | 'fo-fc(+ve)' | 'fo-fc(-ve)'
+    export const ChannelTypeOptions: [ChannelType, string][] = [['em', 'em'], ['2fo-fc', '2fo-fc'], ['fo-fc(+ve)', 'fo-fc(+ve)'], ['fo-fc(-ve)', 'fo-fc(-ve)']]
+    interface ChannelInfo {
+        data: VolumeData,
+        color: Color,
+        isoValue: VolumeIsoValue.Relative,
+        opacity: number
+    }
+    export type Channels = { [name in ChannelType]?: ChannelInfo }
+
+    export class Behavior extends PluginBehavior.WithSubscribers<Params> {
+        private cache = LRUCache.create<ChannelsData>(25);
+        private params: Params = {} as any;
+        // private ref: string = '';
+
+        channels: Channels = {}
+
+        private async queryData(box?: Box3D) {
+            let url = urlCombine(this.info.serverUrl, `${this.info.kind}/${this.info.dataId}`);
+
+            if (box) {
+                const { min: a, max: b } = box;
+                url += `/box`
+                    + `/${a.map(v => Math.round(1000 * v) / 1000).join(',')}`
+                    + `/${b.map(v => Math.round(1000 * v) / 1000).join(',')}`;
+            } else {
+                url += `/cell`;
+            }
+            url += `?detail=${this.params.detailLevel}`;
+
+            let data = LRUCache.get(this.cache, url);
+            if (data) {
+                return data;
+            }
+
+            const cif = await this.plugin.runTask(this.plugin.fetch({ url, type: 'binary' }));
+            data = await this.parseCif(cif as Uint8Array);
+            if (!data) {
+                return;
+            }
+
+            LRUCache.set(this.cache, url, data);
+            return data;
+        }
+
+        private async parseCif(data: Uint8Array): Promise<ChannelsData | undefined> {
+            const parsed = await this.plugin.runTask(CIF.parseBinary(data));
+            if (parsed.isError) {
+                this.plugin.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString());
+                return;
+            }
+            if (parsed.result.blocks.length < 2) {
+                this.plugin.log.error('VolumeStreaming: Invalid data.');
+                return;
+            }
+
+            const ret: ChannelsData = {};
+            for (let i = 1; i < parsed.result.blocks.length; i++) {
+                const block = parsed.result.blocks[i];
+
+                const densityServerCif = CIF.schema.densityServer(block);
+                const volume = await this.plugin.runTask(await volumeFromDensityServerData(densityServerCif));
+                (ret as any)[block.header as any] = volume;
+            }
+            return ret;
+        }
+
+        register(ref: string): void {
+            // this.ref = ref;
+
+            this.subscribeObservable(this.plugin.events.canvas3d.click, ({ current }) => {
+                if (this.params.view.name !== 'selection-box') return;
+                // TODO: support link loci as well?
+                // Perhaps structure loci too?
+                if (!StructureElement.isLoci(current.loci)) return;
+
+                // TODO: check if it's the related structure
+                const loci = StructureElement.Loci.extendToWholeResidues(current.loci);
+
+                const eR = this.params.view.params.radius;
+                const box = StructureElement.Loci.getBoundary(loci).box;
+                const update = this.plugin.state.dataState.build().to(ref)
+                    .update(CreateVolumeStreamingBehavior, old => ({
+                        ...old,
+                        view: {
+                            name: 'selection-box' as 'selection-box',
+                            params: {
+                                radius: eR,
+                                bottomLeft: box.min,
+                                topRight: box.max
+                            }
+                        }
+                    }));
+
+                this.plugin.runTask(this.plugin.state.dataState.updateTree(update));
+            });
+        }
+
+        async update(params: Params) {
+            this.params = params;
+
+            let box: Box3D | undefined = void 0, emptyData = false;
+
+            switch (params.view.name) {
+                case 'box':
+                    box = Box3D.create(params.view.params.bottomLeft, params.view.params.topRight);
+                    break;
+                case 'selection-box': {
+                    box = Box3D.create(Vec3.clone(params.view.params.bottomLeft), Vec3.clone(params.view.params.topRight));
+                    const r = params.view.params.radius;
+                    emptyData = Box3D.volume(box) < 0.0001;
+                    Box3D.expand(box, box, Vec3.create(r, r, r));
+                    break;
+                }
+                case 'cell':
+                    box = this.info.kind === 'x-ray'
+                        ? this.info.structure.boundary.box
+                        : void 0;
+                    break;
+            }
+
+            const data = emptyData ? { } : await this.queryData(box);
+
+            if (!data) return false;
+
+            const info = params.channels as ChannelsInfo;
+
+            if (this.info.kind === 'x-ray') {
+                this.channels['2fo-fc'] = this.createChannel(data['2FO-FC'] || VolumeData.One, info['2fo-fc'], this.info.header.sampling[0].valuesInfo[0]);
+                this.channels['fo-fc(+ve)'] = this.createChannel(data['FO-FC'] || VolumeData.One, info['fo-fc(+ve)'], this.info.header.sampling[0].valuesInfo[1]);
+                this.channels['fo-fc(-ve)'] = this.createChannel(data['FO-FC'] || VolumeData.One, info['fo-fc(-ve)'], this.info.header.sampling[0].valuesInfo[1]);
+            } else {
+                this.channels['em'] = this.createChannel(data['EM'] || VolumeData.One, info['em'], this.info.header.sampling[0].valuesInfo[0]);
+            }
+
+            return true;
+        }
+
+        private createChannel(data: VolumeData, info: ChannelsInfo['em'], stats: VolumeData['dataStats']): ChannelInfo {
+            const i = info!;
+            return {
+                data,
+                color: i.color,
+                opacity: i.opacity,
+                isoValue: i.isoValue.kind === 'relative' ? i.isoValue : VolumeIsoValue.toRelative(i.isoValue, stats)
+            };
+        }
+
+        constructor(public plugin: PluginContext, public info: VolumeServerInfo.Data) {
+            super(plugin);
+        }
+    }
+}

+ 92 - 0
src/mol-plugin/behavior/dynamic/volume-streaming/model.ts

@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginStateObject } from '../../../state/objects';
+import { VolumeIsoValue } from 'mol-model/volume';
+import { Structure } from 'mol-model/structure';
+
+export class VolumeServerInfo extends PluginStateObject.Create<VolumeServerInfo.Data>({ name: 'Volume Streaming', typeClass: 'Object' }) { }
+
+export namespace VolumeServerInfo {
+    export type Kind = 'x-ray' | 'em'
+    export interface Data  {
+        serverUrl: string,
+        kind: Kind,
+        // for em, the EMDB access code, for x-ray, the PDB id
+        dataId: string,
+        header: VolumeServerHeader,
+        emDefaultContourLevel?: VolumeIsoValue,
+        structure: Structure
+    }
+}
+
+export interface VolumeServerHeader {
+    /** Format version number  */
+    formatVersion: string,
+
+    /** Axis order from the slowest to fastest moving, same as in CCP4 */
+    axisOrder: number[],
+
+    /** Origin in fractional coordinates, in axisOrder */
+    origin: number[],
+
+    /** Dimensions in fractional coordinates, in axisOrder */
+    dimensions: number[],
+
+    spacegroup: VolumeServerHeader.Spacegroup,
+    channels: string[],
+
+    /** Determines the data type of the values */
+    valueType: VolumeServerHeader.ValueType,
+
+    /** The value are stored in blockSize^3 cubes */
+    blockSize: number,
+    sampling: VolumeServerHeader.Sampling[],
+
+    /** Precision data the server can show. */
+    availablePrecisions: VolumeServerHeader.DetailLevel[],
+
+    isAvailable: boolean
+}
+
+export namespace VolumeServerHeader {
+    export type ValueType = 'float32' | 'int8'
+
+    export namespace ValueType {
+        export const Float32: ValueType = 'float32';
+        export const Int8: ValueType = 'int8';
+    }
+
+    export type ValueArray = Float32Array | Int8Array
+
+    export type DetailLevel = { precision: number, maxVoxels: number }
+
+    export interface Spacegroup {
+        number: number,
+        size: number[],
+        angles: number[],
+        /** Determine if the data should be treated as periodic or not. (e.g. X-ray = periodic, EM = not periodic) */
+        isPeriodic: boolean,
+    }
+
+    export interface ValuesInfo {
+        mean: number,
+        sigma: number,
+        min: number,
+        max: number
+    }
+
+    export interface Sampling {
+        byteOffset: number,
+
+        /** How many values along each axis were collapsed into 1 */
+        rate: number,
+        valuesInfo: ValuesInfo[],
+
+        /** Number of samples along each axis, in axisOrder  */
+        sampleCount: number[]
+    }
+}

+ 174 - 0
src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts

@@ -0,0 +1,174 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginStateObject as SO, PluginStateTransform } from '../../../state/objects';
+import { VolumeServerInfo, VolumeServerHeader } from './model';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { Task } from 'mol-task';
+import { PluginContext } from 'mol-plugin/context';
+import { urlCombine } from 'mol-util/url';
+import { createIsoValueParam } from 'mol-repr/volume/isosurface';
+import { VolumeIsoValue } from 'mol-model/volume';
+import { StateAction, StateObject, StateTransformer } from 'mol-state';
+import { getStreamingMethod, getEmdbIdAndContourLevel } from './util';
+import { VolumeStreaming } from './behavior';
+import { VolumeRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation';
+import { BuiltInVolumeRepresentations } from 'mol-repr/volume/registry';
+import { createTheme } from 'mol-theme/theme';
+// import { PluginContext } from 'mol-plugin/context';
+
+export const InitVolumeStreaming = StateAction.build({
+    display: { name: 'Volume Streaming' },
+    from: SO.Molecule.Structure,
+    params(a) {
+        return {
+            method: PD.Select<VolumeServerInfo.Kind>(getStreamingMethod(a && a.data), [['em', 'EM'], ['x-ray', 'X-Ray']]),
+            id: PD.Text((a && a.data.models[0].label) || ''),
+            serverUrl: PD.Text('https://webchem.ncbr.muni.cz/DensityServer')
+        };
+    }
+})(({ ref, state, params }, plugin: PluginContext) => Task.create('Volume Streaming', async taskCtx => {
+    // TODO: custom react view for this and the VolumeStreamingBehavior transformer
+
+    let dataId = params.id.toLowerCase(), emDefaultContourLevel: number | undefined;
+    if (params.method === 'em') {
+        await taskCtx.update('Getting EMDB info...');
+        const emInfo = await getEmdbIdAndContourLevel(plugin, taskCtx, dataId);
+        dataId = emInfo.emdbId;
+        emDefaultContourLevel = emInfo.contour;
+    }
+
+    const infoTree = state.build().to(ref)
+        .apply(CreateVolumeStreamingInfo, {
+            serverUrl: params.serverUrl,
+            source: params.method === 'em'
+                ? { name: 'em', params: { isoValue: VolumeIsoValue.absolute(emDefaultContourLevel || 0) } }
+                : { name: 'x-ray', params: { } },
+            dataId
+        });
+
+    const infoObj = await state.updateTree(infoTree).runInContext(taskCtx);
+
+    const behTree = state.build().to(infoTree.ref).apply(CreateVolumeStreamingBehavior, PD.getDefaultValues(VolumeStreaming.createParams(infoObj.data)))
+    if (params.method === 'em') {
+        behTree.apply(VolumeStreamingVisual, { channel: 'em' }, { props: { isGhost: true } });
+    } else {
+        behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' }, { props: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { props: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { props: { isGhost: true } });
+    }
+    await state.updateTree(behTree).runInContext(taskCtx);
+}));
+
+export { CreateVolumeStreamingInfo }
+type CreateVolumeStreamingInfo = typeof CreateVolumeStreamingInfo
+const CreateVolumeStreamingInfo = PluginStateTransform.BuiltIn({
+    name: 'create-volume-streaming-info',
+    display: { name: 'Volume Streaming Info' },
+    from: SO.Molecule.Structure,
+    to: VolumeServerInfo,
+    params(a) {
+        return {
+            serverUrl: PD.Text('https://webchem.ncbr.muni.cz/DensityServer'),
+            source: PD.MappedStatic('x-ray', {
+                'em': PD.Group({
+                    isoValue: createIsoValueParam(VolumeIsoValue.relative(1))
+                }),
+                'x-ray': PD.Group({ })
+            }),
+            dataId: PD.Text('')
+        };
+    }
+})({
+    apply: ({ a, params }, plugin: PluginContext) => Task.create('', async taskCtx => {
+        const dataId = params.dataId;
+        const emDefaultContourLevel = params.source.name === 'em' ? params.source.params.isoValue : VolumeIsoValue.relative(1);
+        await taskCtx.update('Getting server header...');
+        const header = await plugin.fetch<VolumeServerHeader>({ url: urlCombine(params.serverUrl, `${params.source.name}/${dataId}`), type: 'json' }).runInContext(taskCtx);
+        const data: VolumeServerInfo.Data = {
+            serverUrl: params.serverUrl,
+            dataId,
+            kind: params.source.name,
+            header,
+            emDefaultContourLevel,
+            structure: a.data
+        };
+        return new VolumeServerInfo(data, { label: `Volume Streaming: ${dataId}` });
+    })
+});
+
+export { CreateVolumeStreamingBehavior }
+type CreateVolumeStreamingBehavior = typeof CreateVolumeStreamingBehavior
+const CreateVolumeStreamingBehavior = PluginStateTransform.BuiltIn({
+    name: 'create-volume-streaming-behavior',
+    display: { name: 'Volume Streaming Behavior' },
+    from: VolumeServerInfo,
+    to: VolumeStreaming,
+    params(a) {
+        return VolumeStreaming.createParams(a && a.data);
+    }
+})({
+    canAutoUpdate: ({ oldParams, newParams }) => {
+        return oldParams.view === newParams.view
+            || (oldParams.view.name === newParams.view.name && oldParams.view.name === 'selection-box');
+    },
+    apply: ({ a, params }, plugin: PluginContext) => Task.create('Volume streaming', async _ => {
+        const behavior = new VolumeStreaming.Behavior(plugin, a.data);
+        await behavior.update(params);
+        return new VolumeStreaming(behavior, { label: 'Streaming Controls' });
+    }),
+    update({ b, newParams }) {
+        return Task.create('Update Volume Streaming', async _ => {
+            return await b.data.update(newParams) ? StateTransformer.UpdateResult.Updated : StateTransformer.UpdateResult.Unchanged;
+        });
+    }
+});
+
+
+export { VolumeStreamingVisual }
+type VolumeStreamingVisual = typeof VolumeStreamingVisual
+const VolumeStreamingVisual = PluginStateTransform.BuiltIn({
+    name: 'create-volume-streaming-visual',
+    display: { name: 'Volume Streaming Visual' },
+    from: VolumeStreaming,
+    to: SO.Volume.Representation3D,
+    params: {
+        channel: PD.Select<VolumeStreaming.ChannelType>('em', VolumeStreaming.ChannelTypeOptions, { isHidden: true })
+    }
+})({
+    apply: ({ a, params: srcParams }, plugin: PluginContext) => Task.create('Volume Representation', async ctx => {
+        const channel = a.data.channels[srcParams.channel];
+        if (!channel) return StateObject.Null;
+
+        const params = createVolumeProps(a.data, srcParams.channel);
+
+        const provider = BuiltInVolumeRepresentations.isosurface;
+        const props = params.type.params || {}
+        const repr = provider.factory({ webgl: plugin.canvas3d.webgl, ...plugin.volumeRepresentation.themeCtx }, provider.getParams)
+        repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: channel.data }, params))
+        await repr.createOrUpdate(props, channel.data).runInContext(ctx);
+        return new SO.Volume.Representation3D(repr, { label: `${Math.round(channel.isoValue.relativeValue * 100) / 100} σ [${srcParams.channel}]` });
+    }),
+    update: ({ a, b, oldParams, newParams }, plugin: PluginContext) => Task.create('Volume Representation', async ctx => {
+        // TODO : check if params/underlying data/etc have changed; maybe will need to export "data" or some other "tag" in the Representation for this to work
+
+        const channel = a.data.channels[newParams.channel];
+        // TODO: is this correct behavior?
+        if (!channel) return StateTransformer.UpdateResult.Unchanged;
+
+        const params = createVolumeProps(a.data, newParams.channel);
+        const props = { ...b.data.props, ...params.type.params };
+        b.data.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: channel.data }, params))
+        await b.data.createOrUpdate(props, channel.data).runInContext(ctx);
+        return StateTransformer.UpdateResult.Updated;
+    })
+});
+
+function createVolumeProps(streaming: VolumeStreaming.Behavior, channelName: VolumeStreaming.ChannelType) {
+    const channel = streaming.channels[channelName]!;
+    return VolumeRepresentation3DHelpers.getDefaultParamsStatic(streaming.plugin, 'isosurface',
+        { isoValue: channel.isoValue, alpha: channel.opacity }, 'uniform', { value: channel.color });
+}

+ 51 - 0
src/mol-plugin/behavior/dynamic/volume-streaming/util.ts

@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Structure } from 'mol-model/structure';
+import { VolumeServerInfo } from './model';
+import { PluginContext } from 'mol-plugin/context';
+import { RuntimeContext } from 'mol-task';
+
+export function getStreamingMethod(s?: Structure, defaultKind: VolumeServerInfo.Kind = 'x-ray'): VolumeServerInfo.Kind {
+    if (!s) return defaultKind;
+
+    const model = s.models[0];
+    if (model.sourceData.kind !== 'mmCIF') return defaultKind;
+
+    const data = model.sourceData.data.exptl.method;
+    for (let i = 0; i < data.rowCount; i++) {
+        const v = data.value(i).toUpperCase();
+        if (v.indexOf('MICROSCOPY') >= 0) return 'em';
+    }
+    return 'x-ray';
+}
+
+export async function getEmdbIdAndContourLevel(plugin: PluginContext, taskCtx: RuntimeContext, pdbId: string) {
+    // TODO: parametrize to a differnt URL? in plugin settings perhaps
+    const summary = await plugin.fetch({ url: `https://www.ebi.ac.uk/pdbe/api/pdb/entry/summary/${pdbId}`, type: 'json' }).runInContext(taskCtx);
+
+    const summaryEntry = summary && summary[pdbId];
+    let emdbId: string;
+    if (summaryEntry && summaryEntry[0] && summaryEntry[0].related_structures) {
+        const emdb = summaryEntry[0].related_structures.filter((s: any) => s.resource === 'EMDB');
+        if (!emdb.length) {
+            throw new Error(`No related EMDB entry found for '${pdbId}'.`);
+        }
+        emdbId = emdb[0].accession;
+    } else {
+        throw new Error(`No related EMDB entry found for '${pdbId}'.`);
+    }
+
+    // TODO: parametrize to a differnt URL? in plugin settings perhaps
+    const emdb = await plugin.fetch({ url: `https://www.ebi.ac.uk/pdbe/api/emdb/entry/map/${emdbId}`, type: 'json' }).runInContext(taskCtx);
+    const emdbEntry = emdb && emdb[emdbId];
+    let contour: number | undefined = void 0;
+    if (emdbEntry && emdbEntry[0] && emdbEntry[0].map && emdbEntry[0].map.contour_level && emdbEntry[0].map.contour_level.value !== void 0) {
+        contour = +emdbEntry[0].map.contour_level.value;
+    }
+
+    return { emdbId, contour };
+}

+ 0 - 149
src/mol-plugin/behavior/dynamic/volume.ts

@@ -1,149 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import CIF from 'mol-io/reader/cif';
-import { Box3D } from 'mol-math/geometry';
-import { Vec3 } from 'mol-math/linear-algebra';
-import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
-import { VolumeData, VolumeIsoValue } from 'mol-model/volume';
-import { PluginContext } from 'mol-plugin/context';
-import { PluginStateObject } from 'mol-plugin/state/objects';
-import { createIsoValueParam } from 'mol-repr/volume/isosurface';
-import { Color } from 'mol-util/color';
-import { LRUCache } from 'mol-util/lru-cache';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { PluginBehavior } from '../behavior';
-import { Structure } from 'mol-model/structure';
-
-export namespace VolumeStreaming {
-    function channelParam(label: string, color: Color, defaultValue: number) {
-        return PD.Group({
-            color: PD.Color(color),
-            isoValue: createIsoValueParam(VolumeIsoValue.relative(defaultValue))
-        }, { label });
-    }
-
-    export const Params = {
-        id: PD.Text('1tqn'),
-        levels: PD.MappedStatic('x-ray', {
-            'em': channelParam('EM', Color(0x638F8F), 1.5),
-            'x-ray': PD.Group({
-                '2fo-fc': channelParam('2Fo-Fc', Color(0x3362B2), 1.5),
-                'fo-fc(+ve)': channelParam('Fo-Fc(+ve)', Color(0x33BB33), 3),
-                'fo-fc(-ve)': channelParam('Fo-Fc(-ve)', Color(0xBB3333), -3),
-            })
-        }),
-        box: PD.MappedStatic('static-box', {
-            'static-box': PD.Group({
-                bottomLeft: PD.Vec3(Vec3.create(-22.4, -33.4, -21.6)),
-                topRight: PD.Vec3(Vec3.create(-7.1, -10, -0.9))
-            }, { description: 'Static box defined by cartesian coords.' }),
-            // 'around-selection': PD.Group({ radius: PD.Numeric(5, { min: 0, max: 10 }) }),
-            'cell': PD.Group({  }),
-            // 'auto': PD.Group({  }), // based on camera distance/active selection/whatever, show whole structure or slice.
-        }),
-        detailLevel: PD.Numeric(3, { min: 0, max: 7 }),
-        serverUrl: PD.Text('https://webchem.ncbr.muni.cz/DensityServer'),
-    }
-    export type Params = PD.Values<typeof Params>
-
-    export type ChannelData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: VolumeData }
-    export type LevelType = 'em' | '2fo-fc' | 'fo-fc(+ve)' | 'fo-fc(-ve)'
-
-    export class Behavior implements PluginBehavior<Params> {
-        // TODO: have special value for "cell"?
-        private cache = LRUCache.create<ChannelData>(25);
-        // private ref: string = '';
-
-        currentData: ChannelData = { }
-
-        private async queryData(box?: Box3D) {
-            let url = `${this.params.serverUrl}/${this.params.levels.name}/${this.params.id}`
-
-            if (box) {
-                const { min: a, max: b } = box;
-                url += `/box`
-                    + `/${a.map(v => Math.round(1000 * v) / 1000).join(',')}`
-                    + `/${b.map(v => Math.round(1000 * v) / 1000).join(',')}`;
-            } else {
-                url += `/cell`;
-            }
-            url += `?detail=${this.params.detailLevel}`;
-
-            let data = LRUCache.get(this.cache, url);
-            if (data) {
-                return data;
-            }
-
-            const cif = await this.ctx.runTask(this.ctx.fetch({ url, type: 'binary' }));
-            data = await this.parseCif(cif as Uint8Array);
-            if (!data) {
-                return;
-            }
-
-            LRUCache.set(this.cache, url, data);
-            return data;
-        }
-
-        private async parseCif(data: Uint8Array): Promise<ChannelData | undefined> {
-            const parsed = await this.ctx.runTask(CIF.parseBinary(data));
-            if (parsed.isError) {
-                this.ctx.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString());
-                return;
-            }
-            if (parsed.result.blocks.length < 2) {
-                this.ctx.log.error('VolumeStreaming: Invalid data.');
-                return;
-            }
-
-            const ret: ChannelData = { };
-            for (let i = 1; i < parsed.result.blocks.length; i++) {
-                const block = parsed.result.blocks[i];
-
-                const densityServerCif = CIF.schema.densityServer(block);
-                const volume = await this.ctx.runTask(await volumeFromDensityServerData(densityServerCif));
-                (ret as any)[block.header as any] = volume;
-            }
-            return ret;
-        }
-
-        register(ref: string): void {
-            // TODO: register camera movement/loci so that "around selection box works"
-            // alternatively, and maybe a better solution, write a global behavior that modifies this node from the outside
-        }
-
-        async update(params: Params): Promise<boolean> {
-            this.params = params;
-
-            let box: Box3D | undefined = void 0;
-
-            switch (params.box.name) {
-                case 'static-box':
-                    box = Box3D.create(params.box.params.bottomLeft, params.box.params.topRight);
-                    break;
-                case 'cell':
-                    box = this.params.levels.name === 'x-ray'
-                        ? this.structure.boundary.box
-                        : void 0;
-                    break;
-            }
-
-            const data = await this.queryData(box);
-            this.currentData = data || { };
-
-            return true;
-        }
-
-        unregister(): void {
-            // TODO unsubscribe to events
-        }
-
-        constructor(public ctx: PluginContext, public params: Params, private structure: Structure) {
-        }
-    }
-
-    export class Obj extends PluginStateObject.CreateBehavior<Behavior>({ name: 'Volume Streaming' }) { }
-}

+ 3 - 1
src/mol-plugin/index.ts

@@ -15,6 +15,7 @@ import { StateTransforms } from './state/transforms';
 import { PluginBehaviors } from './behavior';
 import { AnimateModelIndex } from './state/animation/built-in';
 import { StateActions } from './state/actions';
+import { InitVolumeStreaming } from './behavior/dynamic/volume-streaming/transformers';
 
 function getParam(name: string, regex: string): string {
     let r = new RegExp(`${name}=(${regex})[&]?`, 'i');
@@ -29,7 +30,8 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Action(StateActions.Structure.CreateComplexRepresentation),
         PluginSpec.Action(StateActions.Structure.EnableModelCustomProps),
 
-        PluginSpec.Action(StateActions.Volume.InitVolumeStreaming),
+        // PluginSpec.Action(StateActions.Volume.InitVolumeStreaming),
+        PluginSpec.Action(InitVolumeStreaming),
 
         PluginSpec.Action(StateTransforms.Data.Download),
         PluginSpec.Action(StateTransforms.Data.ParseCif),

+ 1 - 22
src/mol-plugin/state/actions/volume.ts

@@ -16,7 +16,6 @@ import { PluginStateObject } from '../objects';
 import { StateTransforms } from '../transforms';
 import { Download } from '../transforms/data';
 import { VolumeRepresentation3DHelpers } from '../transforms/representation';
-import { VolumeStreaming } from 'mol-plugin/behavior/dynamic/volume';
 import { DataFormatProvider } from './data-format';
 
 export const Ccp4Provider: DataFormatProvider<any> = {
@@ -201,24 +200,4 @@ const DownloadDensity = StateAction.build({
 
     const b = state.build().to(data.ref);
     await provider.getDefaultBuilder(ctx, b, state).runInContext(taskCtx)
-}));
-
-export const InitVolumeStreaming = StateAction.build({
-    display: { name: 'Volume Streaming' },
-    from: PluginStateObject.Molecule.Structure,
-    params: VolumeStreaming.Params
-})(({ ref, state, params }, ctx: PluginContext) => {
-    // TODO: specify simpler params
-    // TODO: try to determine if the input is x-ray or emd (in params provider)
-    // TODO: for EMD, use PDBe API to determine controur level https://github.com/dsehnal/LiteMol/blob/master/src/Viewer/Extensions/DensityStreaming/Entity.ts#L168
-    // TODO: custom react view for this and the VolumeStreamingBehavior transformer
-
-    const root = state.build().to(ref)
-        .apply(StateTransforms.Volume.VolumeStreamingBehavior, params);
-
-    root.apply(StateTransforms.Volume.VolumeStreamingVisual, { channel: '2FO-FC', level: '2fo-fc' }, { props: { isGhost: true } });
-    root.apply(StateTransforms.Volume.VolumeStreamingVisual, { channel: 'FO-FC', level: 'fo-fc(+ve)' }, { props: { isGhost: true } });
-    root.apply(StateTransforms.Volume.VolumeStreamingVisual, { channel: 'FO-FC', level: 'fo-fc(-ve)' }, { props: { isGhost: true } });
-
-    return state.updateTree(root);
-});
+}));

+ 1 - 104
src/mol-plugin/state/transforms/volume.ts

@@ -13,14 +13,6 @@ import { volumeFromDsn6 } from 'mol-model-formats/volume/dsn6';
 import { Task } from 'mol-task';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { PluginStateObject as SO, PluginStateTransform } from '../objects';
-import { VolumeStreaming } from 'mol-plugin/behavior/dynamic/volume';
-import { PluginContext } from 'mol-plugin/context';
-import { StateTransformer } from 'mol-state';
-import { VolumeData, VolumeIsoValue } from 'mol-model/volume';
-import { BuiltInVolumeRepresentations } from 'mol-repr/volume/registry';
-import { createTheme } from 'mol-theme/theme';
-import { VolumeRepresentation3DHelpers } from './representation';
-import { Color } from 'mol-util/color';
 
 export { VolumeFromCcp4 };
 export { VolumeFromDsn6 };
@@ -97,99 +89,4 @@ const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({
             return new SO.Volume.Data(volume, props);
         });
     }
-});
-
-export { VolumeStreamingBehavior }
-type VolumeStreamingBehavior = typeof VolumeStreamingBehavior
-const VolumeStreamingBehavior = PluginStateTransform.BuiltIn({
-    name: 'volume-streaming-behavior',
-    display: { name: 'Volume Streaming Behavior', description: 'Create Volume Streaming behavior.' },
-    from: SO.Molecule.Structure,
-    to: VolumeStreaming.Obj,
-    params: VolumeStreaming.Params
-})({
-    canAutoUpdate: ({ oldParams, newParams }) => oldParams.serverUrl === newParams.serverUrl && oldParams.id === newParams.id,
-    apply: ({ a, params }, plugin: PluginContext) => Task.create('Volume Streaming', async ctx => {
-        const behavior = new VolumeStreaming.Behavior(plugin, params, a.data);
-        // get the initial data now so that the child projections dont get empty volumes.
-        await behavior.update(behavior.params);
-        return new VolumeStreaming.Obj(behavior, { label: 'Volume Streaming' });
-    }),
-    update({ b, newParams }) {
-        return Task.create('Update Volume Streaming', async _ => {
-            await b.data.update(newParams);
-            return StateTransformer.UpdateResult.Updated;
-        });
-    }
-});
-
-// export { VolumeStreamingData }
-// type VolumeStreamingData = typeof VolumeStreamingData
-// const VolumeStreamingData = PluginStateTransform.BuiltIn({
-//     name: 'volume-streaming-data',
-//     display: { name: 'Volume Streaming Data' },
-//     from: VolumeStreaming.Obj,
-//     to: SO.Volume.Data,
-//     params: {
-//         channel: PD.Select<keyof VolumeStreaming.ChannelData>('EM', [['EM', 'EM'], ['FO-FC', 'Fo-Fc'], ['2FO-FC', '2Fo-Fc']], { isHidden: true }),
-//         level: PD.Text<VolumeStreaming.LevelType>('em')
-//     }
-// })({
-//     apply({ a, params }, plugin: PluginContext) {
-//         const data = a.data.currentData[params.channel] || VolumeData.Empty;
-//         console.log({ data });
-//         return new SO.Volume.Data(a.data.currentData[params.channel] || VolumeData.Empty, { label: params.level });
-//     }
-// });
-
-export { VolumeStreamingVisual }
-type VolumeStreamingVisual = typeof VolumeStreamingVisual
-const VolumeStreamingVisual = PluginStateTransform.BuiltIn({
-    name: 'volume-streaming-visual',
-    display: { name: 'Volume Streaming Visual' },
-    from: VolumeStreaming.Obj,
-    to: SO.Volume.Representation3D,
-    params: {
-        channel: PD.Select<keyof VolumeStreaming.ChannelData>('EM', [['EM', 'EM'], ['FO-FC', 'Fo-Fc'], ['2FO-FC', '2Fo-Fc']], { isHidden: true }),
-        level: PD.Text<VolumeStreaming.LevelType>('em')
-    }
-})({
-    apply: ({ a, params: srcParams }, plugin: PluginContext) => Task.create('Volume Representation', async ctx => {
-        const { data, params } = createVolumeProps(a.data, srcParams.channel, srcParams.level);
-
-        const provider = BuiltInVolumeRepresentations.isosurface;
-        const props = params.type.params || {}
-        const repr = provider.factory({ webgl: plugin.canvas3d.webgl, ...plugin.volumeRepresentation.themeCtx }, provider.getParams)
-        repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: data }, params))
-        await repr.createOrUpdate(props, data).runInContext(ctx);
-        return new SO.Volume.Representation3D(repr, { label: srcParams.level, description: VolumeRepresentation3DHelpers.getDescription(props) });
-    }),
-    update: ({ a, b, oldParams, newParams }, plugin: PluginContext) => Task.create('Volume Representation', async ctx => {
-        // TODO : check if params/underlying data/etc have changed; maybe will need to export "data" or some other "tag" in the Representation for this to work
-        const { data, params } = createVolumeProps(a.data, newParams.channel, newParams.level);
-        const props = { ...b.data.props, ...params.type.params };
-        b.data.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: data }, params))
-        await b.data.createOrUpdate(props, data).runInContext(ctx);
-        return StateTransformer.UpdateResult.Updated;
-    })
-});
-
-function createVolumeProps(streaming: VolumeStreaming.Behavior, channel: keyof VolumeStreaming.ChannelData, level: VolumeStreaming.LevelType) {
-    const data = streaming.currentData[channel] || VolumeData.One;
-    // TODO: createTheme fails when VolumeData.Empty is used for some reason.
-
-    let isoValue: VolumeIsoValue, color: Color;
-
-    if (level === 'em' && streaming.params.levels.name === 'em') {
-        isoValue = streaming.params.levels.params.isoValue;
-        color = streaming.params.levels.params.color;
-    } else if (level !== 'em' && streaming.params.levels.name === 'x-ray') {
-        isoValue = streaming.params.levels.params[level].isoValue;
-        color = streaming.params.levels.params[level].color;
-    } else {
-        throw new Error(`Unsupported iso level ${level}.`);
-    }
-
-    const params = VolumeRepresentation3DHelpers.getDefaultParamsStatic(streaming.ctx, 'isosurface', { isoValue, alpha: 0.3 }, 'uniform', { value: color });
-    return { data, params };
-}
+});

+ 1 - 1
src/mol-plugin/ui/controls/parameters.tsx

@@ -221,7 +221,7 @@ export class SelectControl extends SimpleParam<PD.Select<string | number>> {
     }
     renderControl() {
         const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value);
-        return <select value={this.props.value || this.props.param.defaultValue} onChange={this.onChange} disabled={this.props.isDisabled}>
+        return <select value={this.props.value !== void 0 ? this.props.value : this.props.param.defaultValue} onChange={this.onChange} disabled={this.props.isDisabled}>
             {isInvalid && <option key={this.props.value} value={this.props.value}>{`[Invalid] ${this.props.value}`}</option>}
             {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
         </select>;

+ 12 - 4
src/mol-plugin/util/task-manager.ts

@@ -13,18 +13,21 @@ export { TaskManager }
 class TaskManager {
     private ev = RxEventHelper.create();
     private id = 0;
+    private abortRequests = new Map<number, string | undefined>();
 
     readonly events = {
         progress: this.ev<TaskManager.ProgressEvent>(),
         finished: this.ev<{ id: number }>()
     };
 
-    private track(id: number) {
+    private track(internalId: number, taskId: number) {
         return (progress: Progress) => {
+            if (progress.canAbort && progress.requestAbort && this.abortRequests.has(taskId)) {
+                progress.requestAbort(this.abortRequests.get(taskId));
+            }
             const elapsed = now() - progress.root.progress.startedTime;
-            progress.root.progress.startedTime
             this.events.progress.next({
-                id,
+                id: internalId,
                 level: elapsed < 250 ? 'none' : elapsed < 1500 ? 'background' : 'overlay',
                 progress
             });
@@ -34,13 +37,18 @@ class TaskManager {
     async run<T>(task: Task<T>): Promise<T> {
         const id = this.id++;
         try {
-            const ret = await task.run(this.track(id), 100);
+            const ret = await task.run(this.track(id, task.id), 100);
             return ret;
         } finally {
             this.events.finished.next({ id });
+            this.abortRequests.delete(task.id);
         }
     }
 
+    requestAbort(task: Task<any> | number, reason?: string) {
+        this.abortRequests.set(typeof task === 'number' ? task : task.id, reason);
+    }
+
     dispose() {
         this.ev.dispose();
     }

+ 25 - 27
src/mol-repr/volume/isosurface.ts

@@ -19,24 +19,41 @@ import { VisualContext } from 'mol-repr/visual';
 import { NullLocation } from 'mol-model/location';
 import { Lines } from 'mol-geo/geometry/lines/lines';
 
-export function createIsoValueParam(defaultValue: VolumeIsoValue) {
+const defaultStats: VolumeData['dataStats'] = { min: -1, max: 1, mean: 0, sigma: 0.1  };
+export function createIsoValueParam(defaultValue: VolumeIsoValue, stats?: VolumeData['dataStats']) {
+    const sts = stats || defaultStats;
+    const { min, max, mean, sigma } = sts;
+
+    // using ceil/floor could lead to "ouf of bounds" when converting
+    const relMin = (min - mean) / sigma;
+    const relMax = (max - mean) / sigma;
+
+    let def = defaultValue;
+    if (defaultValue.kind === 'absolute') {
+        if (defaultValue.absoluteValue < min) def = VolumeIsoValue.absolute(min);
+        else if (defaultValue.absoluteValue > max) def = VolumeIsoValue.absolute(max);
+    } else {
+        if (defaultValue.relativeValue < relMin) def = VolumeIsoValue.relative(relMin);
+        else if (defaultValue.relativeValue > relMax) def = VolumeIsoValue.relative(relMax);
+    }
+
     return PD.Conditioned(
-        defaultValue,
+        def,
         {
             'absolute': PD.Converted(
                 (v: VolumeIsoValue) => VolumeIsoValue.toAbsolute(v, VolumeData.One.dataStats).absoluteValue,
                 (v: number) => VolumeIsoValue.absolute(v),
-                PD.Numeric(0.5, { min: -1, max: 1, step: 0.01 })
+                PD.Numeric(mean, { min, max, step: sigma / 100 })
             ),
             'relative': PD.Converted(
                 (v: VolumeIsoValue) => VolumeIsoValue.toRelative(v, VolumeData.One.dataStats).relativeValue,
                 (v: number) => VolumeIsoValue.relative(v),
-                PD.Numeric(2, { min: -10, max: 10, step: 0.01 })
+                PD.Numeric(Math.min(1, relMax), { min: relMin, max: relMax, step: Math.round(((max - min) / sigma)) / 100 })
             )
         },
         (v: VolumeIsoValue) => v.kind === 'absolute' ? 'absolute' : 'relative',
-        (v: VolumeIsoValue, c: 'absolute' | 'relative') => c === 'absolute' ? VolumeIsoValue.toAbsolute(v, VolumeData.One.dataStats) : VolumeIsoValue.toRelative(v, VolumeData.One.dataStats)
-    )
+        (v: VolumeIsoValue, c: 'absolute' | 'relative') => c === 'absolute' ? VolumeIsoValue.toAbsolute(v, sts) : VolumeIsoValue.toRelative(v, sts)
+    );
 }
 
 export const IsoValueParam = createIsoValueParam(VolumeIsoValue.relative(2));
@@ -138,27 +155,8 @@ export const IsosurfaceParams = {
 }
 export type IsosurfaceParams = typeof IsosurfaceParams
 export function getIsosurfaceParams(ctx: ThemeRegistryContext, volume: VolumeData) {
-    const p = PD.clone(IsosurfaceParams)
-    const stats = volume.dataStats
-    const { min, max, mean, sigma } = stats
-    p.isoValue = PD.Conditioned(
-        VolumeIsoValue.relative(2),
-        {
-            'absolute': PD.Converted(
-                (v: VolumeIsoValue) => VolumeIsoValue.toAbsolute(v, stats).absoluteValue,
-                (v: number) => VolumeIsoValue.absolute(v),
-                PD.Numeric(mean, { min, max, step: sigma / 100 })
-            ),
-            'relative': PD.Converted(
-                (v: VolumeIsoValue) => VolumeIsoValue.toRelative(v, stats).relativeValue,
-                (v: number) => VolumeIsoValue.relative(v),
-                PD.Numeric(2, { min: Math.floor((min - mean) / sigma), max: Math.ceil((max - mean) / sigma), step: Math.ceil((max - min) / sigma) / 100 })
-            )
-        },
-        (v: VolumeIsoValue) => v.kind === 'absolute' ? 'absolute' : 'relative',
-        (v: VolumeIsoValue, c: 'absolute' | 'relative') => c === 'absolute' ? VolumeIsoValue.toAbsolute(v, stats) : VolumeIsoValue.toRelative(v, stats)
-    )
-
+    const p = PD.clone(IsosurfaceParams);
+    p.isoValue = createIsoValueParam(VolumeIsoValue.relative(2), volume.dataStats);
     return p
 }
 

+ 15 - 14
src/mol-util/data-source.ts

@@ -14,7 +14,7 @@ import { utf8Read } from 'mol-io/common/utf8';
 //     Gzip
 // }
 
-export interface AjaxGetParams<T extends 'string' | 'binary' = 'string'> {
+export interface AjaxGetParams<T extends 'string' | 'binary' | 'json' = 'string'> {
     url: string,
     type?: T,
     title?: string,
@@ -34,21 +34,14 @@ export function readFromFile(file: File, type: 'string' | 'binary') {
     return <Task<Uint8Array | string>>readFromFileInternal(file, type === 'binary');
 }
 
-export function ajaxGetString(url: string, title?: string) {
-    return <Task<string>>ajaxGetInternal(title, url, false, false);
-}
-
-export function ajaxGetUint8Array(url: string, title?: string) {
-    return <Task<Uint8Array>>ajaxGetInternal(title, url, true, false);
-}
-
 export function ajaxGet(url: string): Task<string>
 export function ajaxGet(params: AjaxGetParams<'string'>): Task<string>
 export function ajaxGet(params: AjaxGetParams<'binary'>): Task<Uint8Array>
-export function ajaxGet(params: AjaxGetParams<'string' | 'binary'>): Task<string | Uint8Array>
-export function ajaxGet(params: AjaxGetParams<'string' | 'binary'> | string) {
-    if (typeof params === 'string') return ajaxGetInternal(params, params, false, false);
-    return ajaxGetInternal(params.title, params.url, params.type === 'binary', false /* params.compression === DataCompressionMethod.Gzip */, params.body);
+export function ajaxGet<T = any>(params: AjaxGetParams<'json'>): Task<T>
+export function ajaxGet(params: AjaxGetParams<'string' | 'binary' | 'json'>): Task<string | Uint8Array | object>
+export function ajaxGet(params: AjaxGetParams<'string' | 'binary' | 'json'> | string) {
+    if (typeof params === 'string') return ajaxGetInternal(params, params, 'string', false);
+    return ajaxGetInternal(params.title, params.url, params.type || 'string', false /* params.compression === DataCompressionMethod.Gzip */, params.body);
 }
 
 export type AjaxTask = typeof ajaxGet
@@ -168,10 +161,11 @@ async function processAjax(ctx: RuntimeContext, asUint8Array: boolean, decompres
     }
 }
 
-function ajaxGetInternal(title: string | undefined, url: string, asUint8Array: boolean, decompressGzip: boolean, body?: string): Task<string | Uint8Array> {
+function ajaxGetInternal(title: string | undefined, url: string, type: 'json' | 'string' | 'binary', decompressGzip: boolean, body?: string): Task<string | Uint8Array> {
     let xhttp: XMLHttpRequest | undefined = void 0;
     return Task.create(title ? title : 'Download', async ctx => {
         try {
+            const asUint8Array = type === 'binary';
             if (!asUint8Array && decompressGzip) {
                 throw 'Decompress is only available when downloading binary data.';
             }
@@ -185,6 +179,13 @@ function ajaxGetInternal(title: string | undefined, url: string, asUint8Array: b
             ctx.update({ message: 'Waiting for server...', canAbort: true });
             const e = await readData(ctx, 'Downloading...', xhttp, asUint8Array);
             const result = await processAjax(ctx, asUint8Array, decompressGzip, e)
+
+            if (type === 'json') {
+                ctx.update({ message: 'Parsing JSON...', canAbort: false });
+                const data = JSON.parse(result);
+                return data;
+            }
+
             return result;
         } finally {
             xhttp = void 0;

+ 4 - 0
src/mol-util/url-query.ts → src/mol-util/url.ts

@@ -9,4 +9,8 @@ export function urlQueryParameter (id: string) {
     const a = new RegExp(`${id}=([^&#=]*)`)
     const m = a.exec(window.location.search)
     return m ? decodeURIComponent(m[1]) : undefined
+}
+
+export function urlCombine(base: string, query: string) {
+    return `${base}${base[base.length - 1] === '/' || query[0] === '/' ? '' : '/'}${query}`;
 }