diff --git a/CHANGELOG.md b/CHANGELOG.md index 233f9a06d5fe730231585a5faaebaf982a405084..9c9d982c8c61f1c60a650d7a40d09f4e17fc1f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ Note that since we don't clearly distinguish between a public and private interf - Add support to dim unmarked groups - Add support for marker edge strength - Factor out common code in `Dnatco` extension +- Add `NtC tube` visual. Applicable for structures with NtC annotation +- [Breaking] Rename `DnatcoConfalPyramids` to `DnatcoNtCs` ## [v3.28.0] - 2022-12-20 diff --git a/src/apps/viewer/app.ts b/src/apps/viewer/app.ts index cdc4c46de0653d027c0a5eab5e0a2697dfd1e37d..c6a531763122c27697585f4efe94df0328924b02 100644 --- a/src/apps/viewer/app.ts +++ b/src/apps/viewer/app.ts @@ -7,7 +7,7 @@ import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior'; import { CellPack } from '../../extensions/cellpack'; -import { DnatcoConfalPyramids } from '../../extensions/dnatco'; +import { DnatcoNtCs } from '../../extensions/dnatco'; import { G3DFormat, G3dProvider } from '../../extensions/g3d/format'; import { Volseg, VolsegVolumeServerConfig } from '../../extensions/volumes-and-segmentations'; import { GeometryExport } from '../../extensions/geo-export'; @@ -60,7 +60,7 @@ const Extensions = { 'volseg': PluginSpec.Behavior(Volseg), 'backgrounds': PluginSpec.Behavior(Backgrounds), 'cellpack': PluginSpec.Behavior(CellPack), - 'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids), + 'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs), 'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport), 'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry), 'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport), diff --git a/src/extensions/dnatco/behavior.ts b/src/extensions/dnatco/behavior.ts new file mode 100644 index 0000000000000000000000000000000000000000..57ca11ca03eef315f8b9677fbbd995c922c68c02 --- /dev/null +++ b/src/extensions/dnatco/behavior.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiřà Černý <jiri.cerny@ibt.cas.cz> + */ + +import { PluginBehavior } from '../../mol-plugin/behavior/behavior'; +import { ParamDefinition as PD } from '../../mol-util/param-definition'; +import { ConfalPyramidsPreset } from './confal-pyramids/behavior'; +import { ConfalPyramidsColorThemeProvider } from './confal-pyramids/color'; +import { ConfalPyramidsProvider } from './confal-pyramids/property'; +import { ConfalPyramidsRepresentationProvider } from './confal-pyramids/representation'; +import { NtCTubePreset } from './ntc-tube/behavior'; +import { NtCTubeColorThemeProvider } from './ntc-tube/color'; +import { NtCTubeProvider } from './ntc-tube/property'; +import { NtCTubeRepresentationProvider } from './ntc-tube/representation'; + + +export const DnatcoNtCs = PluginBehavior.create<{ autoAttach: boolean, showToolTip: boolean }>({ + name: 'dnatco-ntcs', + category: 'custom-props', + display: { + name: 'DNATCO NtC Annotations', + description: 'DNATCO NtC Annotations', + }, + ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showToolTip: boolean }> { + register(): void { + this.ctx.customModelProperties.register(ConfalPyramidsProvider, this.params.autoAttach); + this.ctx.customModelProperties.register(NtCTubeProvider, this.params.autoAttach); + + this.ctx.representation.structure.themes.colorThemeRegistry.add(ConfalPyramidsColorThemeProvider); + this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider); + this.ctx.representation.structure.themes.colorThemeRegistry.add(NtCTubeColorThemeProvider); + this.ctx.representation.structure.registry.add(NtCTubeRepresentationProvider); + + this.ctx.builders.structure.representation.registerPreset(ConfalPyramidsPreset); + this.ctx.builders.structure.representation.registerPreset(NtCTubePreset); + } + + unregister() { + this.ctx.customModelProperties.unregister(NtCTubeProvider.descriptor.name); + + this.ctx.representation.structure.registry.remove(NtCTubeRepresentationProvider); + this.ctx.representation.structure.themes.colorThemeRegistry.remove(NtCTubeColorThemeProvider); + + this.ctx.builders.structure.representation.unregisterPreset(NtCTubePreset); + } + }, + params: () => ({ + autoAttach: PD.Boolean(true), + showToolTip: PD.Boolean(true) + }) +}); + diff --git a/src/extensions/dnatco/confal-pyramids/behavior.ts b/src/extensions/dnatco/confal-pyramids/behavior.ts index b793ee7b61a670c0b3f03bacc25cc89f824d2e49..47955bc9bffe3efa887ad1f4775b758b7cf90a89 100644 --- a/src/extensions/dnatco/confal-pyramids/behavior.ts +++ b/src/extensions/dnatco/confal-pyramids/behavior.ts @@ -10,13 +10,11 @@ import { ConfalPyramidsProvider } from './property'; import { ConfalPyramidsRepresentationProvider } from './representation'; import { Dnatco } from '../property'; import { DnatcoTypes } from '../types'; -import { PluginBehavior } from '../../../mol-plugin/behavior/behavior'; import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset'; import { StateObjectRef } from '../../../mol-state'; import { Task } from '../../../mol-task'; -import { ParamDefinition as PD } from '../../../mol-util/param-definition'; -export const DnatcoConfalPyramidsPreset = StructureRepresentationPresetProvider({ +export const ConfalPyramidsPreset = StructureRepresentationPresetProvider({ id: 'preset-structure-representation-confal-pyramids', display: { name: 'Confal Pyramids', group: 'Annotation', @@ -49,50 +47,7 @@ export const DnatcoConfalPyramidsPreset = StructureRepresentationPresetProvider( } }); -export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean, showToolTip: boolean }>({ - name: 'dnatco-confal-pyramids-prop', - category: 'custom-props', - display: { - name: 'Confal Pyramids', - description: 'Schematic depiction of conformer class and confal value.', - }, - ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showToolTip: boolean }> { - private provider = ConfalPyramidsProvider; - - register(): void { - this.ctx.customModelProperties.register(this.provider, this.params.autoAttach); - - this.ctx.representation.structure.themes.colorThemeRegistry.add(ConfalPyramidsColorThemeProvider); - this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider); - - this.ctx.builders.structure.representation.registerPreset(DnatcoConfalPyramidsPreset); - } - - update(p: { autoAttach: boolean, showToolTip: boolean }) { - const updated = this.params.autoAttach !== p.autoAttach; - this.params.autoAttach = p.autoAttach; - this.params.showToolTip = p.showToolTip; - this.ctx.customModelProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach); - return updated; - } - - unregister() { - this.ctx.customModelProperties.unregister(ConfalPyramidsProvider.descriptor.name); - - this.ctx.representation.structure.registry.remove(ConfalPyramidsRepresentationProvider); - this.ctx.representation.structure.themes.colorThemeRegistry.remove(ConfalPyramidsColorThemeProvider); - - this.ctx.builders.structure.representation.unregisterPreset(DnatcoConfalPyramidsPreset); - } - }, - params: () => ({ - autoAttach: PD.Boolean(true), - showToolTip: PD.Boolean(true) - }) -}); - -export function confalPyramidLabel(halfStep: DnatcoTypes.HalfStep) { - const { step } = halfStep; +export function confalPyramidLabel(step: DnatcoTypes.Step) { return ` <b>${step.auth_asym_id_1}</b> | <b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''} diff --git a/src/extensions/dnatco/confal-pyramids/representation.ts b/src/extensions/dnatco/confal-pyramids/representation.ts index 60a77075dd4305ff1a309802f74e02f9f27680f5..f55cb6ef923bbd28f7e7a26c94498b5b95b3e19b 100644 --- a/src/extensions/dnatco/confal-pyramids/representation.ts +++ b/src/extensions/dnatco/confal-pyramids/representation.ts @@ -151,9 +151,7 @@ function getConfalPyramidLoci(pickingId: PickingId, structureGroup: StructureGro if (halfPyramidsCount <= groupId) return EmptyLoci; const idx = Math.floor(groupId / 2); // Map groupIndex to a step, see createConfalPyramidsMesh() for full explanation - const step = data.steps[idx]; - - return CPT.Loci({ step, isLower: groupId % 2 === 1 }, [{}]); + return CPT.Loci(data.steps, [idx]); } function eachConfalPyramid(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) { diff --git a/src/extensions/dnatco/confal-pyramids/types.ts b/src/extensions/dnatco/confal-pyramids/types.ts index 89363cc140b0d7b0756bad5c21796139a9a785cd..c308096da9f387cd498c3a1bd7622754acb97215 100644 --- a/src/extensions/dnatco/confal-pyramids/types.ts +++ b/src/extensions/dnatco/confal-pyramids/types.ts @@ -21,10 +21,10 @@ export namespace ConfalPyramidsTypes { return !!x && x.kind === 'data-location' && x.tag === DnatcoTypes.DataTag; } - export interface Loci extends DataLoci<DnatcoTypes.HalfStep, {}> {} + export interface Loci extends DataLoci<DnatcoTypes.Step[], number> {} - export function Loci(data: DnatcoTypes.HalfStep, elements: ReadonlyArray<{}>): Loci { - return DataLoci(DnatcoTypes.DataTag, data, elements, undefined, () => confalPyramidLabel(data)); + export function Loci(data: DnatcoTypes.Step[], elements: ReadonlyArray<number>): Loci { + return DataLoci(DnatcoTypes.DataTag, data, elements, undefined, () => elements[0] !== undefined ? confalPyramidLabel(data[elements[0]]) : ''); } export function isLoci(x: any): x is Loci { diff --git a/src/extensions/dnatco/confal-pyramids/util.ts b/src/extensions/dnatco/confal-pyramids/util.ts index f30784b80b27bb4cec0963028fe9386d76df8fb8..6ab28c6f5c9ed549ca7651ec811ed12da11aa28b 100644 --- a/src/extensions/dnatco/confal-pyramids/util.ts +++ b/src/extensions/dnatco/confal-pyramids/util.ts @@ -21,12 +21,17 @@ export type Pyramid = { stepIdx: number, }; -function getPyramid(loc: StructureElement.Location, one: DnatcoUtil.Residue, two: DnatcoUtil.Residue, altIdOne: string, altIdTwo: string, confalScore: number, stepIdx: number): Pyramid { - const O3 = DnatcoUtil.getAtomIndex(loc, one, ['O3\'', 'O3*'], altIdOne); - const P = DnatcoUtil.getAtomIndex(loc, two, ['P'], altIdTwo); - const OP1 = DnatcoUtil.getAtomIndex(loc, two, ['OP1'], altIdTwo); - const OP2 = DnatcoUtil.getAtomIndex(loc, two, ['OP2'], altIdTwo); - const O5 = DnatcoUtil.getAtomIndex(loc, two, ['O5\'', 'O5*'], altIdTwo); +function getPyramid( + loc: StructureElement.Location, + one: DnatcoUtil.Residue, two: DnatcoUtil.Residue, + altIdOne: string, altIdTwo: string, + insCodeOne: string, insCodeTwo: string, + confalScore: number, stepIdx: number): Pyramid { + const O3 = DnatcoUtil.getAtomIndex(loc, one, ['O3\'', 'O3*'], altIdOne, insCodeOne); + const P = DnatcoUtil.getAtomIndex(loc, two, ['P'], altIdTwo, insCodeTwo); + const OP1 = DnatcoUtil.getAtomIndex(loc, two, ['OP1'], altIdTwo, insCodeTwo); + const OP2 = DnatcoUtil.getAtomIndex(loc, two, ['OP2'], altIdTwo, insCodeTwo); + const O5 = DnatcoUtil.getAtomIndex(loc, two, ['O5\'', 'O5*'], altIdTwo, insCodeTwo); return { O3, P, OP1, OP2, O5, confalScore, stepIdx }; } @@ -52,7 +57,7 @@ export class ConfalPyramidsIterator { const points = []; for (const idx of indices) { const step = this.data!.steps[idx]; - points.push(getPyramid(this.loc, one, two, step.label_alt_id_1, step.label_alt_id_2, step.confal_score, idx)); + points.push(getPyramid(this.loc, one, two, step.label_alt_id_1, step.label_alt_id_2, step.PDB_ins_code_1, step.PDB_ins_code_2, step.confal_score, idx)); } return points; diff --git a/src/extensions/dnatco/index.ts b/src/extensions/dnatco/index.ts index 1be8f0e39ba10650fe31fe539c80d38e89e7a4f2..f96c7567cda8248642f9e7a141dc2fcc1bfcac94 100644 --- a/src/extensions/dnatco/index.ts +++ b/src/extensions/dnatco/index.ts @@ -1,8 +1,8 @@ /** - * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Michal Malý <michal.maly@ibt.cas.cz> * @author Jiřà Černý <jiri.cerny@ibt.cas.cz> */ -export { DnatcoConfalPyramids } from './confal-pyramids/behavior'; +export { DnatcoNtCs } from './behavior'; diff --git a/src/extensions/dnatco/ntc-tube/behavior.ts b/src/extensions/dnatco/ntc-tube/behavior.ts new file mode 100644 index 0000000000000000000000000000000000000000..aaf6a76687a0b86cb41dfe602850a28ca0b4aeb8 --- /dev/null +++ b/src/extensions/dnatco/ntc-tube/behavior.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiřà Černý <jiri.cerny@ibt.cas.cz> + */ + +import { NtCTubeColorThemeProvider } from './color'; +import { NtCTubeProvider } from './property'; +import { NtCTubeRepresentationProvider } from './representation'; +import { DnatcoTypes } from '../types'; +import { Dnatco } from '../property'; +import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset'; +import { StateObjectRef } from '../../../mol-state'; +import { Task } from '../../../mol-task'; + +export const NtCTubePreset = StructureRepresentationPresetProvider({ + id: 'preset-structure-representation-ntc-tube', + display: { + name: 'NtC Tube', group: 'Annotation', + description: 'NtC Tube', + }, + isApplicable(a) { + return a.data.models.length >= 1 && a.data.models.some(m => Dnatco.isApplicable(m)); + }, + params: () => StructureRepresentationPresetProvider.CommonParams, + async apply(ref, params, plugin) { + const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref); + const model = structureCell?.obj?.data.model; + if (!structureCell || !model) return {}; + + await plugin.runTask(Task.create('NtC tube', async runtime => { + await NtCTubeProvider.attach({ runtime, assetManager: plugin.managers.asset }, model); + })); + + const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin); + + const tube = await plugin.builders.structure.tryCreateComponentStatic(structureCell, 'nucleic', { label: 'NtC Tube' }); + const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params); + + let tubeRepr; + if (representations) + tubeRepr = builder.buildRepresentation(update, tube, { type: NtCTubeRepresentationProvider, typeParams, color: NtCTubeColorThemeProvider }, { tag: 'ntc-tube' }); + + await update.commit({ revertOnError: true }); + return { components: { ...components, tube }, representations: { ...representations, tubeRepr } }; + } +}); + +export function NtCTubeSegmentLabel(step: DnatcoTypes.Step) { + return ` + <b>${step.auth_asym_id_1}</b> | + <b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''} + ${step.label_comp_id_2} ${step.auth_seq_id_2}${step.PDB_ins_code_2}${step.label_alt_id_2.length > 0 ? ` (alt ${step.label_alt_id_2})` : ''} </b><br /> + <i>NtC:</i> ${step.NtC} | <i>Confal score:</i> ${step.confal_score} | <i>RMSD:</i> ${step.rmsd.toFixed(2)} + `; +} diff --git a/src/extensions/dnatco/ntc-tube/color.ts b/src/extensions/dnatco/ntc-tube/color.ts new file mode 100644 index 0000000000000000000000000000000000000000..412069d8f8242fbca3a061a69cb3e9205cf7e2c6 --- /dev/null +++ b/src/extensions/dnatco/ntc-tube/color.ts @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiřà Černý <jiri.cerny@ibt.cas.cz> + */ + +import { ErrorColor, NtCColors } from '../color'; +import { NtCTubeProvider } from './property'; +import { NtCTubeTypes as NTT } from './types'; +import { Dnatco } from '../property'; +import { Location } from '../../../mol-model/location'; +import { CustomProperty } from '../../../mol-model-props/common/custom-property'; +import { ColorTheme } from '../../../mol-theme/color'; +import { ThemeDataContext } from '../../../mol-theme/theme'; +import { Color, ColorMap } from '../../../mol-util/color'; +import { getColorMapParams } from '../../../mol-util/color/params'; +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; +import { TableLegend } from '../../../mol-util/legend'; +import { ObjectKeys } from '../../../mol-util/type-helpers'; + +const Description = 'Assigns colors to NtC Tube segments'; + +const NtCTubeColors = ColorMap({ + ...NtCColors, + residueMarker: Color(0x222222), + stepBoundaryMarker: Color(0x656565), +}); +type NtCTubeColors = typeof NtCTubeColors; + +export const NtCTubeColorThemeParams = { + colors: PD.MappedStatic('default', { + 'default': PD.EmptyGroup(), + 'custom': PD.Group(getColorMapParams(NtCTubeColors)) + }), + markResidueBoundaries: PD.Boolean(true), + markSegmentBoundaries: PD.Boolean(true), +}; +export type NtCTubeColorThemeParams = typeof NtCTubeColorThemeParams; + +export function getNtCTubeColorThemeParams(ctx: ThemeDataContext) { + return PD.clone(NtCTubeColorThemeParams); +} + +export function NtCTubeColorTheme(ctx: ThemeDataContext, props: PD.Values<NtCTubeColorThemeParams>): ColorTheme<NtCTubeColorThemeParams> { + const colorMap = props.colors.name === 'default' ? NtCTubeColors : props.colors.params; + + function color(location: Location, isSecondary: boolean): Color { + if (NTT.isLocation(location)) { + const { data } = location; + const { step, kind } = data; + let key; + if (kind === 'upper') + key = step.NtC + '_Upr' as keyof NtCTubeColors; + else if (kind === 'lower') + key = step.NtC + '_Lwr' as keyof NtCTubeColors; + else if (kind === 'residue-boundary') + key = (!props.markResidueBoundaries ? step.NtC + '_Lwr' : 'residueMarker') as keyof NtCTubeColors; + else /* segment-boundary */ + key = (!props.markSegmentBoundaries ? step.NtC + '_Lwr' : 'stepBoundaryMarker') as keyof NtCTubeColors; + + return colorMap[key] ?? ErrorColor; + } + + return ErrorColor; + } + + return { + factory: NtCTubeColorTheme, + granularity: 'group', + color, + props, + description: Description, + legend: TableLegend(ObjectKeys(colorMap).map(k => [k.replace('_', ' '), colorMap[k]] as [string, Color]).concat([['Error', ErrorColor]])), + }; +} + +export const NtCTubeColorThemeProvider: ColorTheme.Provider<NtCTubeColorThemeParams, 'ntc-tube'> = { + name: 'ntc-tube', + label: 'NtC Tube', + category: ColorTheme.Category.Residue, + factory: NtCTubeColorTheme, + getParams: getNtCTubeColorThemeParams, + defaultValues: PD.getDefaultValues(NtCTubeColorThemeParams), + isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => Dnatco.isApplicable(m)), + ensureCustomProperties: { + attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? NtCTubeProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(), + detach: (data) => data.structure && NtCTubeProvider.ref(data.structure.models[0], false) + } +}; diff --git a/src/extensions/dnatco/ntc-tube/property.ts b/src/extensions/dnatco/ntc-tube/property.ts new file mode 100644 index 0000000000000000000000000000000000000000..661050d6980e74f719e68b17af4b4641f7360e12 --- /dev/null +++ b/src/extensions/dnatco/ntc-tube/property.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiřà Černý <jiri.cerny@ibt.cas.cz> + */ + +import { NtCTubeTypes as NTT } from './types'; +import { Dnatco, DnatcoParams } from '../property'; +import { CustomPropertyDescriptor } from '../../../mol-model/custom-property'; +import { Model } from '../../../mol-model/structure'; +import { CustomProperty } from '../../../mol-model-props/common/custom-property'; +import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property'; +import { PropertyWrapper } from '../../../mol-model-props/common/wrapper'; +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; + +export const NtCTubeParams = { ...DnatcoParams }; +export type NtCTubeParams = typeof NtCTubeParams; +export type NtCTubeProps = PD.Values<NtCTubeParams>; +export type NtCTubeData = PropertyWrapper<NTT.Data | undefined>; + +async function fromCif(ctx: CustomProperty.Context, model: Model, props: NtCTubeProps): Promise<CustomProperty.Data<NtCTubeData>> { + const info = PropertyWrapper.createInfo(); + const data = Dnatco.getCifData(model); + if (data === undefined) return { value: { info, data: undefined } }; + + const steps = Dnatco.getStepsFromCif(model, data.steps, data.stepsSummary); + return { value: { info, data: { data: steps } } }; +} + +export const NtCTubeProvider: CustomModelProperty.Provider<NtCTubeParams, NtCTubeData> = CustomModelProperty.createProvider({ + label: 'NtC Tube', + descriptor: CustomPropertyDescriptor({ + name: 'ntc-tube', + }), + type: 'static', + defaultParams: NtCTubeParams, + getParams: (data: Model) => NtCTubeParams, + isApplicable: (data: Model) => Dnatco.isApplicable(data), + obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<NtCTubeProps>) => { + const p = { ...PD.getDefaultValues(NtCTubeParams), ...props }; + return fromCif(ctx, data, p); + } +}); diff --git a/src/extensions/dnatco/ntc-tube/representation.ts b/src/extensions/dnatco/ntc-tube/representation.ts new file mode 100644 index 0000000000000000000000000000000000000000..cedbd31c5f626023f8f8cd477cb0ed9def9d2cbe --- /dev/null +++ b/src/extensions/dnatco/ntc-tube/representation.ts @@ -0,0 +1,454 @@ +/** + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiřà Černý <jiri.cerny@ibt.cas.cz> + */ + +import { NtCTubeProvider } from './property'; +import { NtCTubeSegmentsIterator } from './util'; +import { NtCTubeTypes as NTT } from './types'; +import { Dnatco } from '../property'; +import { DnatcoTypes } from '../types'; +import { DnatcoUtil } from '../util'; +import { Interval } from '../../../mol-data/int'; +import { BaseGeometry, VisualQuality } from '../../../mol-geo/geometry/base'; +import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; +import { addFixedCountDashedCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder'; +import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; +import { addTube } from '../../../mol-geo/geometry/mesh/builder/tube'; +import { PickingId } from '../../../mol-geo/geometry/picking'; +import { CylinderProps } from '../../../mol-geo/primitive/cylinder'; +import { LocationIterator } from '../../../mol-geo/util/location-iterator'; +import { Sphere3D } from '../../../mol-math/geometry/primitives/sphere3d'; +import { Vec3 } from '../../../mol-math/linear-algebra'; +import { smoothstep } from '../../../mol-math/interpolate'; +import { NullLocation } from '../../../mol-model/location'; +import { EmptyLoci, Loci } from '../../../mol-model/loci'; +import { Structure, StructureElement, Unit } from '../../../mol-model/structure'; +import { structureUnion } from '../../../mol-model/structure/query/utils/structure-set'; +import { CustomProperty } from '../../../mol-model-props/common/custom-property'; +import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation'; +import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder, UnitsRepresentation } from '../../../mol-repr/structure/representation'; +import { UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual'; +import { createCurveSegmentState, CurveSegmentState } from '../../../mol-repr/structure/visual/util/polymer'; +import { getStructureQuality, VisualUpdateState } from '../../../mol-repr/util'; +import { VisualContext } from '../../../mol-repr/visual'; +import { StructureGroup } from '../../../mol-repr/structure/visual/util/common'; +import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme'; +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; + +const v3add = Vec3.add; +const v3copy = Vec3.copy; +const v3cross = Vec3.cross; +const v3fromArray = Vec3.fromArray; +const v3matchDirection = Vec3.matchDirection; +const v3normalize = Vec3.normalize; +const v3orthogonalize = Vec3.orthogonalize; +const v3scale = Vec3.scale; +const v3slerp = Vec3.slerp; +const v3spline = Vec3.spline; +const v3sub = Vec3.sub; +const v3toArray = Vec3.toArray; + +const NtCTubeMeshParams = { + ...UnitsMeshParams, + linearSegments: PD.Numeric(4, { min: 2, max: 8, step: 1 }, BaseGeometry.CustomQualityParamInfo), + radialSegments: PD.Numeric(22, { min: 4, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo), + residueMarkerWidth: PD.Numeric(0.05, { min: 0.01, max: 0.25, step: 0.01 }), + segmentBoundaryWidth: PD.Numeric(0.05, { min: 0.01, max: 0.25, step: 0.01 }), +}; +type NtCTubeMeshParams = typeof NtCTubeMeshParams; + +type QualityOptions = Exclude<VisualQuality, 'auto' | 'custom'>; +const LinearSegmentCount: Record<QualityOptions, number> = { + highest: 6, + higher: 6, + high: 4, + medium: 4, + low: 3, + lower: 3, + lowest: 2, +}; +const RadialSegmentCount: Record<QualityOptions, number> = { + highest: 32, + higher: 26, + high: 22, + medium: 18, + low: 14, + lower: 10, + lowest: 6, +}; + +const _curvePoint = Vec3(); +const _tanA = Vec3(); +const _tanB = Vec3(); +const _firstTangentVec = Vec3(); +const _lastTangentVec = Vec3(); +const _firstNormalVec = Vec3(); +const _lastNormalVec = Vec3(); + +const _tmpNormal = Vec3(); +const _tangentVec = Vec3(); +const _normalVec = Vec3(); +const _binormalVec = Vec3(); +const _prevNormal = Vec3(); +const _nextNormal = Vec3(); + +function interpolatePointsAndTangents(state: CurveSegmentState, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, tRange: number[]) { + const { curvePoints, tangentVectors, linearSegments } = state; + const tension = 0.5; + const r = tRange[1] - tRange[0]; + + for (let j = 0; j <= linearSegments; ++j) { + const t = j * r / linearSegments + tRange[0]; + + v3spline(_curvePoint, p0, p1, p2, p3, t, tension); + v3spline(_tanA, p0, p1, p2, p3, t - 0.01, tension); + v3spline(_tanB, p0, p1, p2, p3, t + 0.01, tension); + + v3toArray(_curvePoint, curvePoints, j * 3); + v3normalize(_tangentVec, v3sub(_tangentVec, _tanA, _tanB)); + v3toArray(_tangentVec, tangentVectors, j * 3); + } +} + +function interpolateNormals(state: CurveSegmentState, firstDirection: Vec3, lastDirection: Vec3) { + const { curvePoints, tangentVectors, normalVectors, binormalVectors } = state; + + const n = curvePoints.length / 3; + + v3fromArray(_firstTangentVec, tangentVectors, 0); + v3fromArray(_lastTangentVec, tangentVectors, (n - 1) * 3); + + v3orthogonalize(_firstNormalVec, _firstTangentVec, firstDirection); + v3orthogonalize(_lastNormalVec, _lastTangentVec, lastDirection); + v3matchDirection(_lastNormalVec, _lastNormalVec, _firstNormalVec); + + v3copy(_prevNormal, _firstNormalVec); + + const n1 = n - 1; + for (let i = 0; i < n; ++i) { + const j = smoothstep(0, n1, i) * n1; + const t = i === 0 ? 0 : 1 / (n - j); + + v3fromArray(_tangentVec, tangentVectors, i * 3); + + v3orthogonalize(_normalVec, _tangentVec, v3slerp(_tmpNormal, _prevNormal, _lastNormalVec, t)); + v3toArray(_normalVec, normalVectors, i * 3); + + v3copy(_prevNormal, _normalVec); + + v3normalize(_binormalVec, v3cross(_binormalVec, _tangentVec, _normalVec)); + v3toArray(_binormalVec, binormalVectors, i * 3); + } + + for (let i = 1; i < n1; ++i) { + v3fromArray(_prevNormal, normalVectors, (i - 1) * 3); + v3fromArray(_normalVec, normalVectors, i * 3); + v3fromArray(_nextNormal, normalVectors, (i + 1) * 3); + + v3scale(_normalVec, v3add(_normalVec, _prevNormal, v3add(_normalVec, _nextNormal, _normalVec)), 1 / 3); + v3toArray(_normalVec, normalVectors, i * 3); + + v3fromArray(_tangentVec, tangentVectors, i * 3); + v3normalize(_binormalVec, v3cross(_binormalVec, _tangentVec, _normalVec)); + v3toArray(_binormalVec, binormalVectors, i * 3); + } +} + +function interpolate(state: CurveSegmentState, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, firstDir: Vec3, lastDir: Vec3, tRange = [0, 1]) { + interpolatePointsAndTangents(state, p0, p1, p2, p3, tRange); + interpolateNormals(state, firstDir, lastDir); +} + +function createNtCTubeSegmentsIterator(structureGroup: StructureGroup): LocationIterator { + const { structure, group } = structureGroup; + const instanceCount = group.units.length; + + const data = NtCTubeProvider.get(structure.model)?.value?.data; + if (!data) return LocationIterator(0, 1, 1, () => NullLocation); + + const numBlocks = data.data.steps.length * 4; + + const getLocation = (groupId: number, instanceId: number) => { + if (groupId > numBlocks) return NullLocation; + const stepIdx = Math.floor(groupId / 4); + const step = data.data.steps[stepIdx]; + const r = groupId % 4; + const kind = + r === 0 ? 'upper' : + r === 1 ? 'lower' : + r === 2 ? 'residue-boundary' : 'segment-boundary'; + + return NTT.Location({ step, kind }); + }; + return LocationIterator(totalMeshGroupsCount(data.data.steps) + 1, instanceCount, 1, getLocation); +} + +function segmentCount(structure: Structure, props: PD.Values<NtCTubeMeshParams>): { linear: number, radial: number } { + const quality = props.quality; + + if (quality === 'custom') + return { linear: props.linearSegments, radial: props.radialSegments }; + else if (quality === 'auto') { + const autoQuality = getStructureQuality(structure) as QualityOptions; + return { linear: LinearSegmentCount[autoQuality], radial: RadialSegmentCount[autoQuality] }; + } else + return { linear: LinearSegmentCount[quality], radial: RadialSegmentCount[quality] }; +} + +function stepBoundingSphere(step: DnatcoTypes.Step, struLoci: StructureElement.Loci): Sphere3D | undefined { + const one = DnatcoUtil.residueToLoci(step.auth_asym_id_1, step.auth_seq_id_1, step.label_alt_id_1, step.PDB_ins_code_1, struLoci, 'auth'); + const two = DnatcoUtil.residueToLoci(step.auth_asym_id_2, step.auth_seq_id_2, step.label_alt_id_2, step.PDB_ins_code_2, struLoci, 'auth'); + + if (StructureElement.Loci.is(one) && StructureElement.Loci.is(two)) { + const union = structureUnion(struLoci.structure, [StructureElement.Loci.toStructure(one), StructureElement.Loci.toStructure(two)]); + return union.boundary.sphere; + } + return void 0; +} + +function totalMeshGroupsCount(steps: DnatcoTypes.Step[]) { + // Each segment has two blocks, Residue Boundary marker and a Segment Boundary marker + return steps.length * 4 - 1; // Subtract one because the last Segment Boundary marker is not drawn +} + +function createNtCTubeMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<NtCTubeMeshParams>, mesh?: Mesh) { + if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh); + + const prop = NtCTubeProvider.get(structure.model).value; + if (prop === undefined || prop.data === undefined) return Mesh.createEmpty(mesh); + + const { data } = prop.data; + if (data.steps.length === 0) return Mesh.createEmpty(mesh); + + const MarkerLinearSegmentCount = 2; + const segCount = segmentCount(structure, props); + const vertexCount = Math.floor((segCount.linear * 4 * data.steps.length / structure.model.atomicHierarchy.chains._rowCount) * segCount.radial); + const chunkSize = Math.floor(vertexCount / 3); + const diameter = 1.0 * theme.size.props.value; + + const mb = MeshBuilder.createState(vertexCount, chunkSize, mesh); + + const state = createCurveSegmentState(segCount.linear); + const { curvePoints, normalVectors, binormalVectors, widthValues, heightValues } = state; + for (let idx = 0; idx <= segCount.linear; idx++) { + widthValues[idx] = diameter; + heightValues[idx] = diameter; + } + const [normals, binormals] = [binormalVectors, normalVectors]; // Needed so that the tube is not drawn from inside out + + const markerState = createCurveSegmentState(MarkerLinearSegmentCount); + const { curvePoints: mCurvePoints, normalVectors: mNormalVectors, binormalVectors: mBinormalVectors, widthValues: mWidthValues, heightValues: mHeightValues } = markerState; + for (let idx = 0; idx <= MarkerLinearSegmentCount; idx++) { + mWidthValues[idx] = diameter; + mHeightValues[idx] = diameter; + } + const [mNormals, mBinormals] = [mBinormalVectors, mNormalVectors]; + + const firstDir = Vec3(); + const lastDir = Vec3(); + const markerDir = Vec3(); + + const residueMarkerWidth = props.residueMarkerWidth / 2; + const it = new NtCTubeSegmentsIterator(structure, unit); + while (it.hasNext) { + const segment = it.move(); + if (!segment) + continue; + + const { p_1, p0, p1, p2, p3, p4, pP } = segment; + const FirstBlockId = segment.stepIdx * 4; + const SecondBlockId = FirstBlockId + 1; + const ResidueMarkerId = FirstBlockId + 2; + const SegmentBoundaryMarkerId = FirstBlockId + 3; + + const { rmShift, rmPos } = calcResidueMarkerShift(p2, p3, pP); + + if (segment.firstInChain) { + v3normalize(firstDir, v3sub(firstDir, p2, p1)); + v3normalize(lastDir, v3sub(lastDir, rmPos, p2)); + } else { + v3copy(firstDir, lastDir); + v3normalize(lastDir, v3sub(lastDir, rmPos, p2)); + } + + // C5' -> O3' block + interpolate(state, p0, p1, p2, p3, firstDir, lastDir); + mb.currentGroup = FirstBlockId; + addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, segment.firstInChain || segment.followsGap, false, 'rounded'); + + // O3' -> C5' block + v3copy(firstDir, lastDir); + v3normalize(markerDir, v3sub(markerDir, p3, rmPos)); + v3normalize(lastDir, v3sub(lastDir, p4, p3)); + + // From O3' to the residue marker + interpolate(state, p1, p2, p3, p4, firstDir, markerDir, [0, rmShift - residueMarkerWidth]); + mb.currentGroup = SecondBlockId; + addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, false, 'rounded'); + + // Residue marker + interpolate(markerState, p1, p2, p3, p4, markerDir, markerDir, [rmShift - residueMarkerWidth, rmShift + residueMarkerWidth]); + mb.currentGroup = ResidueMarkerId; + addTube(mb, mCurvePoints, mNormals, mBinormals, MarkerLinearSegmentCount, segCount.radial, mWidthValues, mHeightValues, false, false, 'rounded'); + + if (segment.capEnd) { + // From the residue marker to C5' of the end + interpolate(state, p1, p2, p3, p4, markerDir, lastDir, [rmShift + residueMarkerWidth, 1]); + mb.currentGroup = SecondBlockId; + addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, true, 'rounded'); + } else { + // From the residue marker to C5' of the step boundary marker + interpolate(state, p1, p2, p3, p4, markerDir, lastDir, [rmShift + residueMarkerWidth, 1 - props.segmentBoundaryWidth]); + mb.currentGroup = SecondBlockId; + addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, false, 'rounded'); + + // Step boundary marker + interpolate(markerState, p1, p2, p3, p4, lastDir, lastDir, [1 - props.segmentBoundaryWidth, 1]); + mb.currentGroup = SegmentBoundaryMarkerId; + addTube(mb, mCurvePoints, mNormals, mBinormals, MarkerLinearSegmentCount, segCount.radial, mWidthValues, mHeightValues, false, false, 'rounded'); + } + + if (segment.followsGap) { + const cylinderProps: CylinderProps = { + radiusTop: diameter / 2, radiusBottom: diameter / 2, topCap: true, bottomCap: true, radialSegments: segCount.radial, + }; + mb.currentGroup = FirstBlockId; + addFixedCountDashedCylinder(mb, p_1, p1, 1, 2 * segCount.linear, cylinderProps); + } + } + + const boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1.05); + + const m = MeshBuilder.getMesh(mb); + m.setBoundingSphere(boundingSphere); + return m; +} + +const _rmvCO = Vec3(); +const _rmvPO = Vec3(); +const _rmPos = Vec3(); +const _HalfPi = Math.PI / 2; +function calcResidueMarkerShift(pO: Vec3, pC: Vec3, pP: Vec3): { rmShift: number, rmPos: Vec3 } { + v3sub(_rmvCO, pC, pO); + v3sub(_rmvPO, pP, pO); + + // Project position of P atom on the O3' -> C5' vector + const beta = Vec3.angle(_rmvPO, _rmvCO); + const alpha = _HalfPi - Math.abs(beta); + const lengthMO = Math.cos(alpha) * Vec3.magnitude(_rmvPO); + const shift = lengthMO / Vec3.magnitude(_rmvCO); + + v3scale(_rmvCO, _rmvCO, shift); + v3add(_rmPos, _rmvCO, pO); + + return { rmShift: shift, rmPos: _rmPos }; +} + +function getNtCTubeSegmentLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) { + const { groupId, objectId, instanceId } = pickingId; + if (objectId !== id) return EmptyLoci; + + const { structure } = structureGroup; + + const unit = structureGroup.group.units[instanceId]; + if (!Unit.isAtomic(unit)) return EmptyLoci; + + const data = NtCTubeProvider.get(structure.model)?.value?.data ?? undefined; + if (!data) return EmptyLoci; + + const MeshGroupsCount = totalMeshGroupsCount(data.data.steps); + if (groupId > MeshGroupsCount) return EmptyLoci; + + const stepIdx = Math.floor(groupId / 4); + const bs = stepBoundingSphere(data.data.steps[stepIdx], Structure.toStructureElementLoci(structure)); + + /* + * NOTE 1) Each step is drawn with 4 mesh groups. We need to divide/multiply by 4 to convert between steps and mesh groups. + * NOTE 2) Molstar will create a mesh only for the asymmetric unit. When the entire biological assembly + * is displayed, Molstar just copies and transforms the mesh. This means that even though the mesh + * might be displayed multiple times, groupIds of the individual blocks in the mesh will be the same. + * If there are multiple copies of a mesh, Molstar needs to be able to tell which block belongs to which copy of the mesh. + * To do that, Molstar adds an offset to groupIds of the copied meshes. Offset is calculated as follows: + * + * offset = NumberOfBlocks * UnitIndex + * + * "NumberOfBlocks" is the number of valid Location objects got from LocationIterator *or* the greatest groupId set by + * the mesh generator - whichever is smaller. + * + * UnitIndex is the index of the Unit the mesh belongs to, starting from 0. (See "unitMap" in the Structure object). + * We can also get this index from the value "instanceId" of the "pickingId" object. + * + * If this offset is not applied, picking a piece of one of the copied meshes would actually pick that piece in the original mesh. + * This is particularly apparent with highlighting - hovering over items in a copied mesh incorrectly highlights those items in the source mesh. + * + * Molstar can take advantage of the fact that ElementLoci has a reference to the Unit object attached to it. Since we cannot attach ElementLoci + * to a step, we need to calculate the offseted groupId here and pass it as part of the DataLoci. + */ + const offsetGroupId = stepIdx * 4 + (MeshGroupsCount + 1) * instanceId; + return NTT.Loci(data.data.steps, [stepIdx], [offsetGroupId], bs); +} + +function eachNtCTubeSegment(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) { + if (NTT.isLoci(loci)) { + const offsetGroupId = loci.elements[0]; + return apply(Interval.ofBounds(offsetGroupId, offsetGroupId + 4)); + } + return false; +} + +function NtCTubeVisual(materialId: number): UnitsVisual<NtCTubeMeshParams> { + return UnitsMeshVisual<NtCTubeMeshParams>({ + defaultProps: PD.getDefaultValues(NtCTubeMeshParams), + createGeometry: createNtCTubeMesh, + createLocationIterator: createNtCTubeSegmentsIterator, + getLoci: getNtCTubeSegmentLoci, + eachLocation: eachNtCTubeSegment, + setUpdateState: (state: VisualUpdateState, newProps: PD.Values<NtCTubeMeshParams>, currentProps: PD.Values<NtCTubeMeshParams>) => { + state.createGeometry = ( + newProps.quality !== currentProps.quality || + newProps.residueMarkerWidth !== currentProps.residueMarkerWidth || + newProps.segmentBoundaryWidth !== currentProps.segmentBoundaryWidth || + newProps.doubleSided !== currentProps.doubleSided || + newProps.alpha !== currentProps.alpha || + newProps.linearSegments !== currentProps.linearSegments || + newProps.radialSegments !== currentProps.radialSegments + ); + } + }, materialId); + +} +const NtCTubeVisuals = { + 'ntc-tube-symbol': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NtCTubeMeshParams>) => UnitsRepresentation('NtC Tube Mesh', ctx, getParams, NtCTubeVisual), +}; + +export const NtCTubeParams = { + ...NtCTubeMeshParams +}; +export type NtCTubeParams = typeof NtCTubeParams; +export function getNtCTubeParams(ctx: ThemeRegistryContext, structure: Structure) { + return PD.clone(NtCTubeParams); +} + +export type NtCTubeRepresentation = StructureRepresentation<NtCTubeParams>; +export function NtCTubeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NtCTubeParams>): NtCTubeRepresentation { + return Representation.createMulti('NtC Tube', ctx, getParams, StructureRepresentationStateBuilder, NtCTubeVisuals as unknown as Representation.Def<Structure, NtCTubeParams>); +} + +export const NtCTubeRepresentationProvider = StructureRepresentationProvider({ + name: 'ntc-tube', + label: 'NtC Tube', + description: 'Displays schematic representation of NtC conformers', + factory: NtCTubeRepresentation, + getParams: getNtCTubeParams, + defaultValues: PD.getDefaultValues(NtCTubeParams), + defaultColorTheme: { name: 'ntc-tube' }, + defaultSizeTheme: { name: 'uniform', props: { value: 2.0 } }, + isApplicable: (structure: Structure) => structure.models.every(m => Dnatco.isApplicable(m)), + ensureCustomProperties: { + attach: async (ctx: CustomProperty.Context, structure: Structure) => structure.models.forEach(m => NtCTubeProvider.attach(ctx, m, void 0, true)), + detach: (data) => data.models.forEach(m => NtCTubeProvider.ref(m, false)), + }, +}); diff --git a/src/extensions/dnatco/ntc-tube/types.ts b/src/extensions/dnatco/ntc-tube/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa7a4cbbe5a05ed7980d993948492558e050e9a2 --- /dev/null +++ b/src/extensions/dnatco/ntc-tube/types.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiřà Černý <jiri.cerny@ibt.cas.cz> + */ + +import { NtCTubeSegmentLabel } from './behavior'; +import { DnatcoTypes } from '../types'; +import { Sphere3D } from '../../../mol-math/geometry/primitives/sphere3d'; +import { DataLocation } from '../../../mol-model/location'; +import { DataLoci } from '../../../mol-model/loci'; + +export namespace NtCTubeTypes { + const DataTag = 'dnatco-tube-segment-data'; + const DummyTag = 'dnatco-tube-dummy'; + + export type Data = { + data: DnatcoTypes.Steps, + } + + export type TubeBlock = { + step: DnatcoTypes.Step, + kind: 'upper' | 'lower' | 'residue-boundary' | 'segment-boundary'; + } + + export interface Location extends DataLocation<TubeBlock> {} + + export function Location(payload: TubeBlock) { + return DataLocation(DataTag, payload, {}); + } + + export function isLocation(x: any): x is Location { + return !!x && x.kind === 'data-location' && x.tag === DataTag; + } + + export interface Loci extends DataLoci<DnatcoTypes.Step[], number> {} + export interface DummyLoci extends DataLoci<{}, number> {} + + export function Loci(data: DnatcoTypes.Step[], stepIndices: number[], elements: number[], boundingSphere?: Sphere3D): Loci { + return DataLoci(DataTag, data, elements, boundingSphere ? () => boundingSphere : undefined, () => stepIndices[0] !== undefined ? NtCTubeSegmentLabel(data[stepIndices[0]]) : ''); + } + + export function DummyLoci(): DummyLoci { + return DataLoci(DummyTag, {}, [], undefined, () => ''); + } + + export function isLoci(x: any): x is Loci { + return !!x && x.kind === 'data-loci' && x.tag === DataTag; + } +} diff --git a/src/extensions/dnatco/ntc-tube/util.ts b/src/extensions/dnatco/ntc-tube/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..84d4f341b2468892e8ea009b2a4a7b18212c8dbe --- /dev/null +++ b/src/extensions/dnatco/ntc-tube/util.ts @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiřà Černý <jiri.cerny@ibt.cas.cz> + */ + +import { NtCTubeTypes as NTT } from './types'; +import { NtCTubeProvider } from './property'; +import { DnatcoUtil } from '../util'; +import { Segmentation, SortedArray } from '../../../mol-data/int'; +import { Vec3 } from '../../../mol-math/linear-algebra'; +import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure'; + +function getAtomPosition(vec: Vec3, loc: StructureElement.Location, residue: DnatcoUtil.Residue, names: string[], altId: string, insCode: string) { + const eI = DnatcoUtil.getAtomIndex(loc, residue, names, altId, insCode); + if (eI !== -1) + loc.unit.conformation.invariantPosition(eI, vec); + else { + vec[0] = 0; vec[1] = 0; vec[2] = 0; + } +} + +const p_1 = Vec3(); +const p0 = Vec3(); +const p1 = Vec3(); +const p2 = Vec3(); +const p3 = Vec3(); +const p4 = Vec3(); +const pP = Vec3(); + +function getPoints( + loc: StructureElement.Location, + r0: DnatcoUtil.Residue | undefined, r1: DnatcoUtil.Residue, r2: DnatcoUtil.Residue, + altId0: string, altId1: string, altId2: string, + insCode0: string, insCode1: string, insCode2: string, +) { + if (r0) getAtomPosition(p_1, loc, r0, ['C5\'', 'C5*'], altId0, insCode0); + r0 ? getAtomPosition(p0, loc, r0, ['O3\'', 'O3*'], altId0, insCode0) : getAtomPosition(p0, loc, r1, ['O5\'', 'O5*'], altId1, insCode1); + getAtomPosition(p1, loc, r1, ['C5\'', 'C5*'], altId1, insCode1); + getAtomPosition(p2, loc, r1, ['O3\'', 'O3*'], altId1, insCode1); + getAtomPosition(p3, loc, r2, ['C5\'', 'C5*'], altId2, insCode2); + getAtomPosition(p4, loc, r2, ['O3\'', 'O3*'], altId2, insCode2); + getAtomPosition(pP, loc, r2, ['P'], altId2, insCode2); + + return { p_1, p0, p1, p2, p3, p4, pP }; +} + +function hasGapElements(r: DnatcoUtil.Residue, unit: Unit) { + for (let xI = r.start; xI < r.end; xI++) { + const eI = unit.elements[xI]; + if (SortedArray.has(unit.gapElements, eI)) { + return true; + } + } + + return false; +} + +export type NtCTubeSegment = { + p_1: Vec3, + p0: Vec3, + p1: Vec3, + p2: Vec3, + p3: Vec3, + p4: Vec3, + pP: Vec3, + stepIdx: number, + followsGap: boolean, + firstInChain: boolean, + capEnd: boolean, +} + +export class NtCTubeSegmentsIterator { + private chainIt: Segmentation.SegmentIterator<ChainIndex>; + private residueIt: Segmentation.SegmentIterator<ResidueIndex>; + private residuePrev?: DnatcoUtil.Residue; + private residueOne?: DnatcoUtil.Residue; + private residueTwo: DnatcoUtil.Residue; + private data?: NTT.Data; + private altIdOne = ''; + private insCodeOne = ''; + private loc: StructureElement.Location; + + private moveStep() { + this.residuePrev = DnatcoUtil.copyResidue(this.residueOne); + this.residueOne = DnatcoUtil.copyResidue(this.residueTwo); + this.residueTwo = DnatcoUtil.copyResidue(this.residueIt.move())!; + + return this.toSegment(this.residuePrev, this.residueOne!, this.residueTwo); + } + + private toSegment(r0: DnatcoUtil.Residue | undefined, r1: DnatcoUtil.Residue, r2: DnatcoUtil.Residue): NtCTubeSegment | undefined { + const indices = DnatcoUtil.getStepIndices(this.data!.data, this.loc, r1); + if (indices.length === 0) + return void 0; + + const stepIdx = indices[0]; + const step = this.data!.data.steps[stepIdx]; + + const altIdPrev = this.altIdOne; + const insCodePrev = this.insCodeOne; + this.altIdOne = step.label_alt_id_1; + this.insCodeOne = step.PDB_ins_code_1; + const altIdTwo = step.label_alt_id_2; + const insCodeTwo = step.PDB_ins_code_2; + const followsGap = !!r0 && hasGapElements(r0, this.loc.unit) && hasGapElements(r1, this.loc.unit); + + return { + ...getPoints(this.loc, r0, r1, r2, altIdPrev, this.altIdOne, altIdTwo, insCodePrev, this.insCodeOne, insCodeTwo), + stepIdx, + followsGap, + firstInChain: !r0, + capEnd: !this.residueIt.hasNext || hasGapElements(r2, this.loc.unit), + }; + } + + constructor(structure: Structure, unit: Unit.Atomic) { + this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements); + this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements); + + const prop = NtCTubeProvider.get(unit.model).value; + this.data = prop?.data; + + if (this.chainIt.hasNext) { + this.residueIt.setSegment(this.chainIt.move()); + if (this.residueIt.hasNext) + this.residueTwo = this.residueIt.move(); + } + + this.loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex); + } + + get hasNext() { + if (!this.data) + return false; + return this.residueIt.hasNext + ? true + : this.chainIt.hasNext; + } + + move() { + if (this.residueIt.hasNext) { + return this.moveStep(); + } else { + this.residuePrev = void 0; // Assume discontinuity when we switch chains + this.residueIt.setSegment(this.chainIt.move()); + if (this.residueIt.hasNext) + this.residueTwo = this.residueIt.move(); + return this.moveStep(); + } + } +} diff --git a/src/extensions/dnatco/property.ts b/src/extensions/dnatco/property.ts index 6af9c995d4223080285ddbc431532b751d396545..a3fac00e758cb7b5b9568489437ffdb3cfee991a 100644 --- a/src/extensions/dnatco/property.ts +++ b/src/extensions/dnatco/property.ts @@ -57,16 +57,80 @@ export namespace Dnatco { }; export type Schema = typeof Schema; + export function getStepsFromCif( + model: Model, + cifSteps: Table<typeof Dnatco.Schema.ndb_struct_ntc_step>, + stepsSummary: StepsSummaryTable + ): DnatcoTypes.Steps { + const steps = new Array<DnatcoTypes.Step>(); + const mapping = new Array<DnatcoTypes.MappedChains>(); + + const { + id, PDB_model_number, name, + auth_asym_id_1, auth_seq_id_1, label_comp_id_1, label_alt_id_1, PDB_ins_code_1, + auth_asym_id_2, auth_seq_id_2, label_comp_id_2, label_alt_id_2, PDB_ins_code_2, + _rowCount + } = cifSteps; + + if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data'); + + for (let i = 0; i < _rowCount; i++) { + const { + NtC, + confal_score, + rmsd + } = getSummaryData(id.value(i), i, stepsSummary); + const modelNum = PDB_model_number.value(i); + const chainId = auth_asym_id_1.value(i); + const seqId = auth_seq_id_1.value(i); + const modelIdx = modelNum - 1; + + if (mapping.length <= modelIdx || !mapping[modelIdx]) + mapping[modelIdx] = new Map<string, DnatcoTypes.MappedResidues>(); + + const step = { + PDB_model_number: modelNum, + name: name.value(i), + auth_asym_id_1: chainId, + auth_seq_id_1: seqId, + label_comp_id_1: label_comp_id_1.value(i), + label_alt_id_1: label_alt_id_1.value(i), + PDB_ins_code_1: PDB_ins_code_1.value(i), + auth_asym_id_2: auth_asym_id_2.value(i), + auth_seq_id_2: auth_seq_id_2.value(i), + label_comp_id_2: label_comp_id_2.value(i), + label_alt_id_2: label_alt_id_2.value(i), + PDB_ins_code_2: PDB_ins_code_2.value(i), + confal_score, + NtC, + rmsd, + }; + + steps.push(step); + + const mappedChains = mapping[modelIdx]; + const residuesOnChain = mappedChains.get(chainId) ?? new Map<number, number[]>(); + const stepsForResidue = residuesOnChain.get(seqId) ?? []; + stepsForResidue.push(steps.length - 1); + + residuesOnChain.set(seqId, stepsForResidue); + mappedChains.set(chainId, residuesOnChain); + mapping[modelIdx] = mappedChains; + } + + return { steps, mapping }; + } + export async function fromCif(ctx: CustomProperty.Context, model: Model, props: DnatcoProps): Promise<CustomProperty.Data<DnatcoSteps>> { const info = PropertyWrapper.createInfo(); const data = getCifData(model); if (data === undefined) return { value: { info, data: undefined } }; - const fromCif = createPyramidsFromCif(model, data.steps, data.stepsSummary); + const fromCif = getStepsFromCif(model, data.steps, data.stepsSummary); return { value: { info, data: fromCif } }; } - function getCifData(model: Model) { + export function getCifData(model: Model) { if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF'); if (!hasNdbStructNtcCategories(model)) return undefined; return { @@ -88,70 +152,6 @@ export namespace Dnatco { type StepsSummaryTable = Table<typeof Dnatco.Schema.ndb_struct_ntc_step_summary>; -function createPyramidsFromCif( - model: Model, - cifSteps: Table<typeof Dnatco.Schema.ndb_struct_ntc_step>, - stepsSummary: StepsSummaryTable -): DnatcoTypes.Steps { - const steps = new Array<DnatcoTypes.Step>(); - const mapping = new Array<DnatcoTypes.MappedChains>(); - - const { - id, PDB_model_number, name, - auth_asym_id_1, auth_seq_id_1, label_comp_id_1, label_alt_id_1, PDB_ins_code_1, - auth_asym_id_2, auth_seq_id_2, label_comp_id_2, label_alt_id_2, PDB_ins_code_2, - _rowCount - } = cifSteps; - - if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data'); - - for (let i = 0; i < _rowCount; i++) { - const { - NtC, - confal_score, - rmsd - } = getSummaryData(id.value(i), i, stepsSummary); - const modelNum = PDB_model_number.value(i); - const chainId = auth_asym_id_1.value(i); - const seqId = auth_seq_id_1.value(i); - const modelIdx = modelNum - 1; - - if (mapping.length <= modelIdx || !mapping[modelIdx]) - mapping[modelIdx] = new Map<string, DnatcoTypes.MappedResidues>(); - - const step = { - PDB_model_number: modelNum, - name: name.value(i), - auth_asym_id_1: chainId, - auth_seq_id_1: seqId, - label_comp_id_1: label_comp_id_1.value(i), - label_alt_id_1: label_alt_id_1.value(i), - PDB_ins_code_1: PDB_ins_code_1.value(i), - auth_asym_id_2: auth_asym_id_2.value(i), - auth_seq_id_2: auth_seq_id_2.value(i), - label_comp_id_2: label_comp_id_2.value(i), - label_alt_id_2: label_alt_id_2.value(i), - PDB_ins_code_2: PDB_ins_code_2.value(i), - confal_score, - NtC, - rmsd, - }; - - steps.push(step); - - const mappedChains = mapping[modelIdx]; - const residuesOnChain = mappedChains.get(chainId) ?? new Map<number, number[]>(); - const stepsForResidue = residuesOnChain.get(seqId) ?? []; - stepsForResidue.push(steps.length - 1); - - residuesOnChain.set(seqId, stepsForResidue); - mappedChains.set(chainId, residuesOnChain); - mapping[modelIdx] = mappedChains; - } - - return { steps, mapping }; -} - function getSummaryData(id: number, i: number, stepsSummary: StepsSummaryTable) { const { step_id, diff --git a/src/extensions/dnatco/util.ts b/src/extensions/dnatco/util.ts index 0db2e9dec70d84fc56846f4d2b2c9e133cf5e4c6..e6654056a8fcbd36a4314583e553e9fb6412a112 100644 --- a/src/extensions/dnatco/util.ts +++ b/src/extensions/dnatco/util.ts @@ -1,6 +1,14 @@ +/** + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Michal Malý <michal.maly@ibt.cas.cz> + * @author Jiřà Černý <jiri.cerny@ibt.cas.cz> + */ + import { DnatcoTypes } from './types'; -import { Segmentation } from '../../mol-data/int'; -import { ElementIndex, ResidueIndex, StructureElement, StructureProperties } from '../../mol-model/structure'; +import { OrderedSet, Segmentation } from '../../mol-data/int'; +import { EmptyLoci } from '../../mol-model/loci'; +import { ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../mol-model/structure'; const EmptyStepIndices = new Array<number>(); @@ -11,13 +19,14 @@ export namespace DnatcoUtil { return r ? { index: r.index, start: r.start, end: r.end } : void 0; } - export function getAtomIndex(loc: StructureElement.Location, residue: Residue, names: string[], altId: string): ElementIndex { + export function getAtomIndex(loc: StructureElement.Location, residue: Residue, names: string[], altId: string, insCode: string): ElementIndex { for (let eI = residue.start; eI < residue.end; eI++) { loc.element = loc.unit.elements[eI]; const elName = StructureProperties.atom.label_atom_id(loc); const elAltId = StructureProperties.atom.label_alt_id(loc); + const elInsCode = StructureProperties.residue.pdbx_PDB_ins_code(loc); - if (names.includes(elName) && (elAltId === altId || elAltId.length === 0)) + if (names.includes(elName) && (elAltId === altId || elAltId.length === 0) && (elInsCode === insCode)) return loc.element; } @@ -30,11 +39,79 @@ export namespace DnatcoUtil { const modelIdx = StructureProperties.unit.model_num(loc) - 1; const chainId = StructureProperties.chain.auth_asym_id(loc); const seqId = StructureProperties.residue.auth_seq_id(loc); + const insCode = StructureProperties.residue.pdbx_PDB_ins_code(loc); const chains = data.mapping[modelIdx]; if (!chains) return EmptyStepIndices; const residues = chains.get(chainId); if (!residues) return EmptyStepIndices; - return residues.get(seqId) ?? EmptyStepIndices; + const indices = residues.get(seqId); + if (!indices) return EmptyStepIndices; + + return insCode !== '' ? indices.filter(idx => data.steps[idx].PDB_ins_code_1 === insCode) : indices; + } + + export function residueAltIds(structure: Structure, unit: Unit, residue: Residue) { + const altIds = new Array<string>(); + const loc = StructureElement.Location.create(structure, unit); + for (let eI = residue.start; eI < residue.end; eI++) { + loc.element = OrderedSet.getAt(unit.elements, eI); + const altId = StructureProperties.atom.label_alt_id(loc); + if (altId !== '' && !altIds.includes(altId)) + altIds.push(altId); + } + + return altIds; + } + + const _loc = StructureElement.Location.create(); + export function residueToLoci(asymId: string, seqId: number, altId: string | undefined, insCode: string, loci: StructureElement.Loci, source: 'label' | 'auth') { + _loc.structure = loci.structure; + for (const e of loci.elements) { + _loc.unit = e.unit; + + const getAsymId = source === 'label' ? StructureProperties.chain.label_asym_id : StructureProperties.chain.auth_asym_id; + const getSeqId = source === 'label' ? StructureProperties.residue.label_seq_id : StructureProperties.residue.auth_seq_id; + + // Walk the entire unit and look for the requested residue + const chainIt = Segmentation.transientSegments(e.unit.model.atomicHierarchy.chainAtomSegments, e.unit.elements); + const residueIt = Segmentation.transientSegments(e.unit.model.atomicHierarchy.residueAtomSegments, e.unit.elements); + + const elemIndex = (idx: number) => OrderedSet.getAt(e.unit.elements, idx); + while (chainIt.hasNext) { + const chain = chainIt.move(); + _loc.element = elemIndex(chain.start); + const _asymId = getAsymId(_loc); + if (_asymId !== asymId) + continue; // Wrong chain, skip it + + residueIt.setSegment(chain); + while (residueIt.hasNext) { + const residue = residueIt.move(); + _loc.element = elemIndex(residue.start); + + const _seqId = getSeqId(_loc); + if (_seqId === seqId) { + const _insCode = StructureProperties.residue.pdbx_PDB_ins_code(_loc); + if (_insCode !== insCode) + continue; + if (altId) { + const _altIds = residueAltIds(loci.structure, e.unit, residue); + if (!_altIds.includes(altId)) + continue; + } + + const start = residue.start as StructureElement.UnitIndex; + const end = residue.end as StructureElement.UnitIndex; + return StructureElement.Loci( + loci.structure, + [{ unit: e.unit, indices: OrderedSet.ofBounds(start, end) }] + ); + } + } + } + } + + return EmptyLoci; } }