Browse Source

wip volume streaming

David Sehnal 6 years ago
parent
commit
7330d9f9f5

+ 173 - 4
src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts

@@ -6,22 +6,191 @@
 
 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';
 
 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) };
 
-    export interface BehaviorParams {
-        
+        return {
+            view: PD.MappedStatic('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)),
+                    autoUpdate: PD.Boolean(true, { description: 'Update the box when user clicks an element.' })
+                }, { description: 'Static box defined by cartesian coords.', isFlat: true }),
+                'cell': PD.Group({}),
+                // 'auto': PD.Group({  }), // based on camera distance/active selection/whatever, show whole structure or slice.
+            }, { options: [['box', 'Bounded Box'], ['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 implements PluginBehavior<{}> {
+        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 {
-            throw new Error('Method not implemented.');
+            this.ref = ref;
+        }
+
+        async update(params: Params) {
+            this.params = params;
+
+            let box: Box3D | undefined = void 0;
+
+            switch (params.view.name) {
+                case 'box':
+                    box = Box3D.create(params.view.params.bottomLeft, params.view.params.topRight);
+                    break;
+                case 'cell':
+                    box = this.info.kind === 'x-ray'
+                        ? this.info.structure.boundary.box
+                        : void 0;
+                    break;
+            }
+
+            const data = 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']!, info['2fo-fc'], this.info.header.sampling[0].valuesInfo[0]);
+                this.channels['fo-fc(+ve)'] = this.createChannel(data['FO-FC']!, info['fo-fc(+ve)'], this.info.header.sampling[0].valuesInfo[1]);
+                this.channels['fo-fc(-ve)'] = this.createChannel(data['FO-FC']!, info['fo-fc(-ve)'], this.info.header.sampling[0].valuesInfo[1]);
+            } else {
+                this.channels['em'] = this.createChannel(data['EM']!, 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)
+            };
         }
 
         unregister(): void {
-            throw new Error('Method not implemented.');
+            // throw new Error('Method not implemented.');
+        }
+
+        constructor(public plugin: PluginContext, public info: VolumeServerInfo.Data) {
+
         }
     }
 }

+ 7 - 2
src/mol-plugin/behavior/dynamic/volume-streaming/model.ts

@@ -5,16 +5,21 @@
  */
 
 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: 'x-ray' | 'em',
+        kind: Kind,
         // for em, the EMDB access code, for x-ray, the PDB id
         dataId: string,
-        header: VolumeServerHeader
+        header: VolumeServerHeader,
+        emDefaultContourLevel?: VolumeIsoValue,
+        structure: Structure
     }
 }
 

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

@@ -0,0 +1,173 @@
+/**
+ * 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, emDefaultContourLevel: number | undefined;
+    if (params.method === 'em') {
+        await taskCtx.update('Getting EMDB info...');
+        const emInfo = await getEmdbIdAndContourLevel(plugin, taskCtx, params.id);
+        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' });
+    } else {
+        behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' });
+        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' });
+        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' });
+    }
+    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;
+    },
+    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 };
+}

+ 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),

+ 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}`;
 }