diff --git a/src/extensions/dnatco/confal-pyramids/behavior.ts b/src/extensions/dnatco/confal-pyramids/behavior.ts index b88a2c9daf8b4069048018c898c8ab675cff52b2..2333dadf4eba40d7d9a09a45bfc6bc253b06ccc6 100644 --- a/src/extensions/dnatco/confal-pyramids/behavior.ts +++ b/src/extensions/dnatco/confal-pyramids/behavior.ts @@ -6,6 +6,7 @@ */ import { ConfalPyramidsProvider } from './property'; +import { ConfalPyramidsRepresentationProvider } from './representation'; import { Loci } from '../../../mol-model/loci'; import { PluginBehavior } from '../../../mol-plugin/behavior/behavior'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; @@ -34,7 +35,9 @@ export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean, this.ctx.customModelProperties.register(this.provider, this.params.autoAttach); this.ctx.managers.lociLabels.addProvider(this.labelConfalPyramids); - /* TODO: Add color and visual providers */ + this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider); + + /* TODO: Add color provider */ } update(p: { autoAttach: boolean, showToolTip: boolean }) { @@ -49,7 +52,9 @@ export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean, this.ctx.customModelProperties.unregister(ConfalPyramidsProvider.descriptor.name); this.ctx.managers.lociLabels.removeProvider(this.labelConfalPyramids); - /* TODO: Unregister color and visual providers */ + this.ctx.representation.structure.registry.remove(ConfalPyramidsRepresentationProvider); + + /* TODO: Unregister color provider */ } }, params: () => ({ diff --git a/src/extensions/dnatco/confal-pyramids/representation.ts b/src/extensions/dnatco/confal-pyramids/representation.ts new file mode 100644 index 0000000000000000000000000000000000000000..e916e1fd8e3f3cb0f67b9f92064a10965c68e367 --- /dev/null +++ b/src/extensions/dnatco/confal-pyramids/representation.ts @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2018-2020 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 { ConfalPyramids, ConfalPyramidsProvider } from './property'; +import { ConfalPyramidsUtil } from './util'; +import { ConfalPyramidsTypes as CPT } from './types'; +import { Interval } from '../../../mol-data/int'; +import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; +import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; +import { PickingId } from '../../../mol-geo/geometry/picking'; +import { PrimitiveBuilder } from '../../../mol-geo/primitive/primitive'; +import { LocationIterator } from '../../../mol-geo/util/location-iterator'; +import { Mat4, Vec3 } from '../../../mol-math/linear-algebra'; +import { EmptyLoci, Loci } from '../../../mol-model/loci'; +import { Structure, Unit } from '../../../mol-model/structure'; +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 { StructureGroup, UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual'; +import { VisualUpdateState } from '../../../mol-repr/util'; +import { VisualContext } from '../../../mol-repr/visual'; +import { Color } from '../../../mol-util/color'; +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; +import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme'; +import { NullLocation } from '../../../mol-model/location'; + +const t = Mat4.identity(); +const w = Vec3.zero(); +const mp = Vec3.zero(); + +function calcMidpoint(mp: Vec3, v: Vec3, w: Vec3) { + Vec3.sub(mp, v, w); + Vec3.scale(mp, mp, 0.5); + Vec3.add(mp, mp, w); +} + +function shiftVertex(vec: Vec3, ref: Vec3, scale: number) { + Vec3.sub(w, vec, ref); + Vec3.scale(w, w, scale); + Vec3.add(vec, vec, w); +} + +const ConfalPyramidsMeshParams = { + ...UnitsMeshParams +}; +type ConfalPyramidsMeshParams = typeof ConfalPyramidsMeshParams; + +function createConfalPyramidsIterator(structureGroup: StructureGroup): LocationIterator { + const { structure } = structureGroup; + const unit = structureGroup.group.units[0]; + + if (!Unit.isAtomic(unit)) { + // Noop + return LocationIterator(0, 1, () => NullLocation); + } + + const prop = ConfalPyramidsProvider.get(structure.model).value; + if (prop === undefined || prop.data === undefined) { + return LocationIterator(0, 1, (groupIndex: number, instanceIndex: number) => { + return NullLocation; + }); + } + + const getLocation = (groupIndex: number, instanceIndex: number) => { + return NullLocation; // TODO: Implement me + }; + return LocationIterator(prop.data.locations.length, 1, getLocation); // TODO: Implement me +} + +function createConfalPyramidsMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<ConfalPyramidsMeshParams>, mesh?: Mesh) { + if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh); + + const prop = ConfalPyramidsProvider.get(structure.model).value; + if (prop === undefined || prop.data === undefined) return Mesh.createEmpty(mesh); + + const { pyramids } = prop.data; + if (pyramids.length === 0) return Mesh.createEmpty(mesh); + + const mb = MeshBuilder.createState(512, 512, mesh); + + const handler = (pyramid: CPT.Pyramid, first: ConfalPyramidsUtil.FirstResidueAtoms, second: ConfalPyramidsUtil.SecondResidueAtoms, firsLocIndex: number, secondLocIndex: number) => { + if (firsLocIndex === -1 || secondLocIndex === -1) + throw new Error('Invalid location index'); + + const scale = (pyramid.confal_score - 20.0) / 100.0; + const O3 = first.O3.pos; + const OP1 = second.OP1.pos; const OP2 = second.OP2.pos; const O5 = second.O5.pos; const P = second.P.pos; + + shiftVertex(O3, P, scale); + shiftVertex(OP1, P, scale); + shiftVertex(OP2, P, scale); + shiftVertex(O5, P, scale); + calcMidpoint(mp, O3, O5); + + mb.currentGroup = firsLocIndex; + let pb = PrimitiveBuilder(3); + /* Upper part (for first residue in step) */ + pb.add(O3, OP1, OP2); + pb.add(O3, mp, OP1); + pb.add(O3, OP2, mp); + MeshBuilder.addPrimitive(mb, t, pb.getPrimitive()); + + /* Lower part (for second residue in step */ + mb.currentGroup = secondLocIndex; + pb = PrimitiveBuilder(3); + pb.add(mp, O5, OP1); + pb.add(mp, OP2, O5); + pb.add(O5, OP2, OP1); + MeshBuilder.addPrimitive(mb, t, pb.getPrimitive()); + }; + + const walker = new ConfalPyramidsUtil.UnitWalker(structure, unit, handler); + walker.walk(); + + return MeshBuilder.getMesh(mb); +} + +function getConfalPyramidLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) { + return EmptyLoci; // TODO: Implement me +} + +function eachConfalPyramid(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) { + return false; // TODO: Implement me +} + +function ConfalPyramidsVisual(materialId: number): UnitsVisual<ConfalPyramidsMeshParams> { + return UnitsMeshVisual<ConfalPyramidsMeshParams>({ + defaultProps: PD.getDefaultValues(ConfalPyramidsMeshParams), + createGeometry: createConfalPyramidsMesh, + createLocationIterator: createConfalPyramidsIterator, + getLoci: getConfalPyramidLoci, + eachLocation: eachConfalPyramid, + setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ConfalPyramidsMeshParams>, currentProps: PD.Values<ConfalPyramidsMeshParams>) => { + } + }, materialId); +} +const ConfalPyramidsVisuals = { + 'confal-pyramids-symbol': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, UnitsMeshParams>) => UnitsRepresentation('Confal Pyramids Symbol Mesh', ctx, getParams, ConfalPyramidsVisual), +}; + +export const ConfalPyramidsParams = { + ...UnitsMeshParams +}; +export type ConfalPyramidsParams = typeof ConfalPyramidsParams; +export function getConfalPyramidsParams(ctx: ThemeRegistryContext, structure: Structure) { + return PD.clone(ConfalPyramidsParams); +} + +export type ConfalPyramidsRepresentation = StructureRepresentation<ConfalPyramidsParams>; +export function ConfalPyramidsRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ConfalPyramidsParams>): ConfalPyramidsRepresentation { + const repr = Representation.createMulti('Confal Pyramids', ctx, getParams, StructureRepresentationStateBuilder, ConfalPyramidsVisuals as unknown as Representation.Def<Structure, ConfalPyramidsParams>); + return repr; +} + +export const ConfalPyramidsRepresentationProvider = StructureRepresentationProvider({ + name: 'confal-pyramids', + label: 'Confal Pyramids', + description: 'Displays schematic depiction of conformer classes and confal values', + factory: ConfalPyramidsRepresentation, + getParams: getConfalPyramidsParams, + defaultValues: PD.getDefaultValues(ConfalPyramidsParams), + defaultColorTheme: { name: 'uniform', props: { value: Color(0xFF3D5A) } }, // TODO: Change to the actual color theme once it is implemented + defaultSizeTheme: { name: 'uniform' }, + isApplicable: (structure: Structure) => structure.models.some(m => ConfalPyramids.isApplicable(m)), + ensureCustomProperties: { + attach: (ctx: CustomProperty.Context, structure: Structure) => ConfalPyramidsProvider.attach(ctx, structure.model, void 0, true), + detach: (data) => ConfalPyramidsProvider.ref(data.model, false), + } +}); diff --git a/src/extensions/dnatco/confal-pyramids/util.ts b/src/extensions/dnatco/confal-pyramids/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f4c627a643f6ed9df090ce7a24c01b240e5117d --- /dev/null +++ b/src/extensions/dnatco/confal-pyramids/util.ts @@ -0,0 +1,299 @@ +/** + * Copyright (c) 2018-2020 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 { ConfalPyramidsProvider } from './property'; +import { ConfalPyramidsTypes as CPT } from './types'; +import { OrderedSet, Segmentation } from '../../../mol-data/int'; +import { Vec3 } from '../../../mol-math/linear-algebra'; +import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure'; + +export namespace ConfalPyramidsUtil { + type Residue = Segmentation.Segment<ResidueIndex>; + + export type AtomInfo = { + pos: Vec3, + index: ElementIndex, + fakeAltId: string, + }; + + export type FirstResidueAtoms = { + O3: AtomInfo, + }; + + export type SecondResidueAtoms = { + OP1: AtomInfo, + OP2: AtomInfo, + O5: AtomInfo, + P: AtomInfo, + }; + + type ResidueInfo = { + PDB_model_num: number, + asym_id: string, + auth_asym_id: string, + seq_id: number, + auth_seq_id: number, + comp_id: string, + alt_id: string, + ins_code: string, + }; + + export type Handler = (pyramid: CPT.Pyramid, first: FirstResidueAtoms, second: SecondResidueAtoms, firstLocIndex: number, secondLocIndex: number) => void; + + function residueInfoFromLocation(loc: StructureElement.Location): ResidueInfo { + return { + PDB_model_num: StructureProperties.unit.model_num(loc), + asym_id: StructureProperties.chain.label_asym_id(loc), + auth_asym_id: StructureProperties.chain.auth_asym_id(loc), + seq_id: StructureProperties.residue.label_seq_id(loc), + auth_seq_id: StructureProperties.residue.auth_seq_id(loc), + comp_id: StructureProperties.atom.label_comp_id(loc), + alt_id: StructureProperties.atom.label_alt_id(loc), + ins_code: StructureProperties.residue.pdbx_PDB_ins_code(loc) + }; + } + + export function hasMultipleModels(unit: Unit.Atomic): boolean { + const prop = ConfalPyramidsProvider.get(unit.model).value; + if (prop === undefined || prop.data === undefined) throw new Error('No custom properties data'); + return prop.data.hasMultipleModels; + } + + function getPossibleAltIdsIndices(eIFirst: ElementIndex, eILast: ElementIndex, structure: Structure, unit: Unit.Atomic): string[] { + const loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex); + + const uIFirst = OrderedSet.indexOf(unit.elements, eIFirst); + const uILast = OrderedSet.indexOf(unit.elements, eILast); + + const possibleAltIds: string[] = []; + for (let uI = uIFirst; uI <= uILast; uI++) { + loc.element = unit.elements[uI]; + const altId = StructureProperties.atom.label_alt_id(loc); + if (altId !== '' && !possibleAltIds.includes(altId)) possibleAltIds.push(altId); + } + + return possibleAltIds; + } + + function getPossibleAltIdsResidue(residue: Residue, structure: Structure, unit: Unit.Atomic): string[] { + return getPossibleAltIdsIndices(unit.elements[residue.start], unit.elements[residue.end - 1], structure, unit); + } + + class Utility { + protected getPyramidByName(name: string): { pyramid: CPT.Pyramid | undefined, index: number } { + const index = this.data.names.get(name); + if (index === undefined) return { pyramid: undefined, index: -1 }; + + return { pyramid: this.data.pyramids[index], index }; + } + + protected stepToName(entry_id: string, modelNum: number, locFirst: StructureElement.Location, locSecond: StructureElement.Location, fakeAltId_1: string, fakeAltId_2: string) { + const first = residueInfoFromLocation(locFirst); + const second = residueInfoFromLocation(locSecond); + const model_id = this.hasMultipleModels ? `-m${modelNum}` : ''; + const alt_id_1 = fakeAltId_1 !== '' ? `.${fakeAltId_1}` : (first.alt_id.length ? `.${first.alt_id}` : ''); + const alt_id_2 = fakeAltId_2 !== '' ? `.${fakeAltId_2}` : (second.alt_id.length ? `.${second.alt_id}` : ''); + const ins_code_1 = first.ins_code.length ? `.${first.ins_code}` : ''; + const ins_code_2 = second.ins_code.length ? `.${second.ins_code}` : ''; + + return `${entry_id}${model_id}_${first.auth_asym_id}_${first.comp_id}${alt_id_1}_${first.auth_seq_id}${ins_code_1}_${second.comp_id}${alt_id_2}_${second.auth_seq_id}${ins_code_2}`; + } + + constructor(unit: Unit.Atomic) { + const prop = ConfalPyramidsProvider.get(unit.model).value; + if (prop === undefined || prop.data === undefined) throw new Error('No custom properties data'); + + this.data = prop.data; + this.hasMultipleModels = hasMultipleModels(unit); + + this.entryId = unit.model.entryId.toLowerCase(); + this.modelNum = unit.model.modelNum; + } + + protected readonly data: CPT.PyramidsData + protected readonly hasMultipleModels: boolean; + protected readonly entryId: string; + protected readonly modelNum: number; + } + + export class UnitWalker extends Utility { + private getAtomIndices(names: string[], residue: Residue): ElementIndex[] { + let rI = residue.start; + const rILast = residue.end - 1; + const indices: ElementIndex[] = []; + + for (; rI !== rILast; rI++) { + const eI = this.unit.elements[rI]; + const loc = StructureElement.Location.create(this.structure, this.unit, eI); + const thisName = StructureProperties.atom.label_atom_id(loc); + if (names.includes(thisName)) indices.push(eI); + } + + if (indices.length === 0) + throw new Error(`Element ${name} not found on residue ${residue.index}`); + + return indices; + } + + private getAtomPositions(indices: ElementIndex[]): Vec3[] { + const pos = this.unit.conformation.invariantPosition; + const positions: Vec3[] = []; + + for (const eI of indices) { + const v = Vec3.zero(); + pos(eI, v); + positions.push(v); + } + + return positions; + } + + private handleStep(firstAtoms: FirstResidueAtoms[], secondAtoms: SecondResidueAtoms[]) { + const modelNum = this.hasMultipleModels ? this.modelNum : -1; + let ok = false; + + const firstLoc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex); + const secondLoc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex); + for (let i = 0; i < firstAtoms.length; i++) { + const first = firstAtoms[i]; + for (let j = 0; j < secondAtoms.length; j++) { + const second = secondAtoms[j]; + firstLoc.element = first.O3.index; + secondLoc.element = second.OP1.index; + + const name = this.stepToName(this.entryId, modelNum, firstLoc, secondLoc, first.O3.fakeAltId, second.OP1.fakeAltId); + const { pyramid, index } = this.getPyramidByName(name); + if (pyramid !== undefined) { + const setLoc = (loc: CPT.Location, eI: ElementIndex) => { + loc.element.structure = this.structure; + loc.element.unit = this.unit; + loc.element.element = eI; + }; + + const locIndex = index * 2; + setLoc(this.data.locations[locIndex], firstLoc.element); + setLoc(this.data.locations[locIndex + 1], secondLoc.element); + this.handler(pyramid, first, second, locIndex, locIndex + 1); + ok = true; + } + } + } + + if (!ok) throw new Error('Bogus step'); + } + + private processFirstResidue(residue: Residue, possibleAltIds: string[]) { + const indO3 = this.getAtomIndices(['O3\'', 'O3*'], residue); + const posO3 = this.getAtomPositions(indO3); + + const altPos: FirstResidueAtoms[] = [ + { O3: { pos: posO3[0], index: indO3[0], fakeAltId: '' } } + ]; + + for (let i = 1; i < indO3.length; i++) { + altPos.push({ O3: { pos: posO3[i], index: indO3[i], fakeAltId: '' } }); + } + + if (altPos.length === 1 && possibleAltIds.length > 1) { + /* We have some alternate positions on the residue but O3 does not have any - fake them */ + altPos[0].O3.fakeAltId = possibleAltIds[0]; + + for (let i = 1; i < possibleAltIds.length; i++) + altPos.push({ O3: { pos: posO3[0], index: indO3[0], fakeAltId: possibleAltIds[i] } }); + } + + return altPos; + } + + private processSecondResidue(residue: Residue, possibleAltIds: string[]) { + const indOP1 = this.getAtomIndices(['OP1'], residue); + const indOP2 = this.getAtomIndices(['OP2'], residue); + const indO5 = this.getAtomIndices(['O5\'', 'O5*'], residue); + const indP = this.getAtomIndices(['P'], residue); + + const posOP1 = this.getAtomPositions(indOP1); + const posOP2 = this.getAtomPositions(indOP2); + const posO5 = this.getAtomPositions(indO5); + const posP = this.getAtomPositions(indP); + + const infoOP1: AtomInfo[] = []; + /* We use OP1 as "pivotal" atom. There is no specific reason + * to pick OP1, it is as good a choice as any other atom + */ + if (indOP1.length === 1 && possibleAltIds.length > 1) { + /* No altIds on OP1, fake them */ + for (const altId of possibleAltIds) + infoOP1.push({ pos: posOP1[0], index: indOP1[0], fakeAltId: altId }); + } else { + for (let i = 0; i < indOP1.length; i++) + infoOP1.push({ pos: posOP1[i], index: indOP1[i], fakeAltId: '' }); + } + + const mkInfo = (i: number, indices: ElementIndex[], positions: Vec3[], altId: string) => { + if (i >= indices.length) { + const last = indices.length - 1; + return { pos: positions[last], index: indices[last], fakeAltId: altId }; + } + + return { pos: positions[i], index: indices[i], fakeAltId: altId }; + }; + + const altPos: SecondResidueAtoms[] = []; + for (let i = 0; i < infoOP1.length; i++) { + const altId = infoOP1[i].fakeAltId; + + const OP2 = mkInfo(i, indOP2, posOP2, altId); + const O5 = mkInfo(i, indO5, posO5, altId); + const P = mkInfo(i, indP, posP, altId); + + altPos.push({ OP1: infoOP1[i], OP2, O5, P }); + } + + return altPos; + } + + private step(residue: Residue): { firstAtoms: FirstResidueAtoms[], secondAtoms: SecondResidueAtoms[] } { + const firstPossibleAltIds = getPossibleAltIdsResidue(residue, this.structure, this.unit); + const firstAtoms = this.processFirstResidue(residue, firstPossibleAltIds); + + residue = this.residueIt.move(); + + const secondPossibleAltIds = getPossibleAltIdsResidue(residue, this.structure, this.unit); + const secondAtoms = this.processSecondResidue(residue, secondPossibleAltIds); + + return { firstAtoms, secondAtoms }; + } + + walk() { + while (this.chainIt.hasNext) { + this.residueIt.setSegment(this.chainIt.move()); + + let residue = this.residueIt.move(); + while (this.residueIt.hasNext) { + try { + const { firstAtoms, secondAtoms } = this.step(residue); + + this.handleStep(firstAtoms, secondAtoms); + } catch (error) { + /* Skip and move along */ + residue = this.residueIt.move(); + } + } + } + } + + constructor(private structure: Structure, private unit: Unit.Atomic, private handler: Handler) { + super(unit); + + this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements); + this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements); + } + + private chainIt: Segmentation.SegmentIterator<ChainIndex>; + private residueIt: Segmentation.SegmentIterator<ResidueIndex>; + } +}