diff --git a/src/mol-math/linear-algebra/3d/vec3.ts b/src/mol-math/linear-algebra/3d/vec3.ts index 0be0f033dcbff1020f4ea5f9ac22566f45daf100..3e8fd5d5ee7765fcb88215a1455655b75357b10e 100644 --- a/src/mol-math/linear-algebra/3d/vec3.ts +++ b/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); } diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts index 34bb4d4e6fa78f9b3b2b0796147c41f26253dd7d..35cad50ed5ecb596bf791d532455964f921db7af 100644 --- a/src/mol-model/loci.ts +++ b/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); diff --git a/src/mol-model/structure/structure/element.ts b/src/mol-model/structure/structure/element.ts index 8b39d221530230aa05717c0db078191c3a34360a..1bd19cde969ada74797f1779daafc4cf29981492 100644 --- a/src/mol-model/structure/structure/element.ts +++ b/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() }; + } } } diff --git a/src/mol-plugin/behavior/behavior.ts b/src/mol-plugin/behavior/behavior.ts index f3fae6e80fe4442f4df5c2c7d799d8222a29cdf0..5aa091a68f86ba3b9e726f810fb8d7b1962aae32 100644 --- a/src/mol-plugin/behavior/behavior.ts +++ b/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) { + } + } } \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts new file mode 100644 index 0000000000000000000000000000000000000000..846b9d29977b5600fb4a0218190b7ca6fdf727d8 --- /dev/null +++ b/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); + } + } +} \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..ba54b666d9f091c8ae407c3fb6d8ff4ad09a2398 --- /dev/null +++ b/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[] + } +} \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts new file mode 100644 index 0000000000000000000000000000000000000000..d7c6f12f27e27a0b1e302c471d80b0481a610e32 --- /dev/null +++ b/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 }); +} \ 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/behavior/dynamic/volume.ts b/src/mol-plugin/behavior/dynamic/volume.ts deleted file mode 100644 index 9808a806afb4a1d402cac2274eb90593b789421b..0000000000000000000000000000000000000000 --- a/src/mol-plugin/behavior/dynamic/volume.ts +++ /dev/null @@ -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' }) { } -} \ 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-plugin/state/actions/volume.ts b/src/mol-plugin/state/actions/volume.ts index ab07fd80a3098c5d9b303dbcb1fa4d26a1148ee7..baf6e0ded64d08c380cf907bd4a671302cbeac5a 100644 --- a/src/mol-plugin/state/actions/volume.ts +++ b/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); -}); \ No newline at end of file +})); \ No newline at end of file diff --git a/src/mol-plugin/state/transforms/volume.ts b/src/mol-plugin/state/transforms/volume.ts index 6c682e4a13cc9df01cea36e51391ae1e832e8259..2401cb0cf18fa0bff435a1e8a430841d65a1f2f9 100644 --- a/src/mol-plugin/state/transforms/volume.ts +++ b/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 }; -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx index b2d5e3bab971401f264ec0e2843059a1807e2eb6..ed0baaa35f518ad4dcf46f6b0c8f4b02d84ea619 100644 --- a/src/mol-plugin/ui/controls/parameters.tsx +++ b/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>; diff --git a/src/mol-plugin/util/task-manager.ts b/src/mol-plugin/util/task-manager.ts index 3c5cbb47e72aa442378ceae3bdd8df06d1193a30..d053c4a618e29b5c8c93b05210d44c8d6d002c06 100644 --- a/src/mol-plugin/util/task-manager.ts +++ b/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(); } 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