diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts index 621f33fd33ed7c7ada1162639b97c232a1536427..3f9925eb128862d5b5819a61f2d47b3d86dc1b5e 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts +++ b/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) { + } } } \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/model.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/model.ts index c0a6a5f57b748d0cf9b9c258d3ee45b8b78cf217..ba54b666d9f091c8ae407c3fb6d8ff4ad09a2398 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/model.ts +++ b/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 } } diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2ed0f9eff7a8a50936563a56e8b89dff5b866771 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts +++ b/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 }); +} \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/util.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..d3a4dbf87ff3177cad8fd51a36877b5bd01f9724 --- /dev/null +++ b/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 }; +} \ No newline at end of file diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index 9fb4643bae578f86bc599c5d9bf09f05b9a2deb5..91e102c327bbbdeae1112a966fe3c00ba90a1c6f 100644 --- a/src/mol-plugin/index.ts +++ b/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), diff --git a/src/mol-repr/volume/isosurface.ts b/src/mol-repr/volume/isosurface.ts index 7ebfe7df4ba6f453bcd590045134b82967325380..30c8ba3278f4bb26b0625de9f007c04989294675 100644 --- a/src/mol-repr/volume/isosurface.ts +++ b/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 } diff --git a/src/mol-util/data-source.ts b/src/mol-util/data-source.ts index c370cc066a4e64a2c700fecacf2b2a523c70412d..316403070d3fbd5b9170bc18c1ddc02bc71ded3f 100644 --- a/src/mol-util/data-source.ts +++ b/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; diff --git a/src/mol-util/url-query.ts b/src/mol-util/url.ts similarity index 72% rename from src/mol-util/url-query.ts rename to src/mol-util/url.ts index a4a10a8371c24b55995ea175df35fb14bc6cec86..3234bdbaea7ff7565eb8f2542c057b314242fd02 100644 --- a/src/mol-util/url-query.ts +++ b/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}`; } \ No newline at end of file