Browse Source

added scene-labels behavior

Alexander Rose 6 years ago
parent
commit
9e8951da94

+ 3 - 1
src/mol-plugin/behavior.ts

@@ -15,6 +15,7 @@ import * as DynamicRepresentation from './behavior/dynamic/representation'
 import * as DynamicCamera from './behavior/dynamic/camera'
 import * as DynamicCustomProps from './behavior/dynamic/custom-props'
 import * as DynamicAnimation from './behavior/dynamic/animation'
+import * as DynamicLabels from './behavior/dynamic/labels'
 
 export const BuiltInPluginBehaviors = {
     State: StaticState,
@@ -27,5 +28,6 @@ export const PluginBehaviors = {
     Representation: DynamicRepresentation,
     Camera: DynamicCamera,
     CustomProps: DynamicCustomProps,
-    Animation: DynamicAnimation
+    Animation: DynamicAnimation,
+    Labels: DynamicLabels
 }

+ 1 - 1
src/mol-plugin/behavior/behavior.ts

@@ -95,7 +95,7 @@ namespace PluginBehavior {
             for (const s of this.subs) s.unsubscribe();
             this.subs = [];
         }
-        update(params: P): boolean {
+        update(params: P): boolean | Promise<boolean> {
             if (shallowEqual(params, this.params)) return false;
             this.params = params;
             return true;

+ 233 - 0
src/mol-plugin/behavior/dynamic/labels.ts

@@ -0,0 +1,233 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginContext } from 'mol-plugin/context';
+import { PluginBehavior } from '../behavior';
+import { ParamDefinition as PD } from 'mol-util/param-definition'
+import { Mat4, Vec3 } from 'mol-math/linear-algebra';
+import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
+import { StateSelection } from 'mol-state/state/selection';
+import { StateObjectCell, State } from 'mol-state';
+import { RuntimeContext } from 'mol-task';
+import { Shape } from 'mol-model/shape';
+import { Text } from 'mol-geo/geometry/text/text';
+import { ShapeRepresentation } from 'mol-repr/shape/representation';
+import { ColorNames } from 'mol-util/color/tables';
+import { TextBuilder } from 'mol-geo/geometry/text/text-builder';
+import { Unit, StructureElement, StructureProperties } from 'mol-model/structure';
+import { SetUtils } from 'mol-util/set';
+import { arrayEqual } from 'mol-util';
+import { MoleculeType } from 'mol-model/structure/model/types';
+
+// TODO
+// - support more object types than structures
+// - tether label to the element nearest to the bounding sphere center
+// - [Started] multiple levels of labels: structure, polymer, ligand
+// - show structure/unit label only when there is a representation with sufficient overlap
+// - support highlighting
+// - better support saccharides (use data available after re-mediation)
+// - size based on min bbox dimension (to avoid huge labels for very long but narrow polymers)
+// - fixed size labels (invariant to zoom) [needs feature in text geo]
+// - ??? max label length
+// - ??? multi line labels [needs feature in text geo]
+// - ??? use prevalent (how to define) color of representations of a structure to color the label
+// - completely different approach (render not as 3d objects): overlay free layout in screenspace with occlusion info from bboxes
+
+export type SceneLabelsLevels = 'structure' | 'polymer' | 'ligand'
+
+export const SceneLabelsParams = {
+    ...Text.Params,
+
+    background: PD.Boolean(true),
+    backgroundMargin: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }),
+    backgroundColor: PD.Color(ColorNames.snow),
+    backgroundOpacity: PD.Numeric(0.9, { min: 0, max: 1, step: 0.01 }),
+
+    levels: PD.MultiSelect([] as SceneLabelsLevels[], [
+        ['structure', 'structure'], ['polymer', 'polymer'], ['ligand', 'ligand']
+    ] as [SceneLabelsLevels, string][]),
+}
+export type SceneLabelsParams = typeof SceneLabelsParams
+export type SceneLabelsProps = PD.Values<typeof SceneLabelsParams>
+
+interface LabelsData {
+    transforms: Mat4[]
+    texts: string[]
+    positions: Vec3[]
+    sizes: number[]
+    depths: number[]
+}
+
+function getLabelsText(data: LabelsData, props: PD.Values<Text.Params>, text?: Text) {
+    const { texts, positions, depths } = data
+    const textBuilder = TextBuilder.create(props, texts.length * 10, texts.length * 10 / 2, text)
+    for (let i = 0, il = texts.length; i < il; ++i) {
+        const p = positions[i]
+        textBuilder.add(texts[i], p[0], p[1], p[2], depths[i], i)
+    }
+    return textBuilder.getText()
+}
+
+export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
+    name: 'scene-labels',
+    display: { name: 'Scene Labels', group: 'Labels' },
+    ctor: class extends PluginBehavior.Handler<SceneLabelsProps> {
+        private data: LabelsData = {
+            transforms: [Mat4.identity()],
+            texts: [],
+            positions: [],
+            sizes: [],
+            depths: []
+        }
+        private repr: ShapeRepresentation<LabelsData, Text, SceneLabelsParams>
+        private geo = Text.createEmpty()
+        private structures = new Set<SO.Molecule.Structure>()
+
+        constructor(protected ctx: PluginContext, protected params: SceneLabelsProps) {
+            super(ctx, params)
+            this.repr = ShapeRepresentation(this.getLabelsShape, Text.Utils)
+            ctx.events.state.object.created.subscribe(this.triggerUpdate)
+            ctx.events.state.object.removed.subscribe(this.triggerUpdate)
+            ctx.events.state.object.updated.subscribe(this.triggerUpdate)
+            ctx.events.state.cell.stateUpdated.subscribe(this.triggerUpdate)
+        }
+
+        private triggerUpdate = async () => {
+            await this.update(this.params)
+        }
+
+        private getColor = () => ColorNames.dimgrey
+        private getSize = (groupId: number) => this.data.sizes[groupId]
+        private getLabel = () => ''
+
+        private getLabelsShape = (ctx: RuntimeContext, data: LabelsData, props: SceneLabelsProps, shape?: Shape<Text>) => {
+            this.geo = getLabelsText(data, props, this.geo)
+            return Shape.create('Scene Labels', this.geo, this.getColor, this.getSize, this.getLabel, data.transforms)
+        }
+
+        /** Update structures to be labeled, returns true if changed */
+        private updateStructures(p: SceneLabelsProps) {
+            const state = this.ctx.state.dataState
+            const structures = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Structure));
+            const rootStructures = new Set<SO.Molecule.Structure>()
+            for (const s of structures) {
+                const rootStructure = getRootStructure(s, state)
+                if (!rootStructure || !SO.Molecule.Structure.is(rootStructure.obj)) continue
+                if (!state.cellStates.get(s.transform.ref).isHidden) {
+                    rootStructures.add(rootStructure.obj)
+                }
+            }
+            if (!SetUtils.areEqual(rootStructures, this.structures)) {
+                this.structures = rootStructures
+                return true
+            } else {
+                return false
+            }
+        }
+
+        private updateLabels(p: SceneLabelsProps) {
+            const l = StructureElement.create()
+
+            const { texts, positions, sizes, depths } = this.data
+            texts.length = 0
+            positions.length = 0
+            sizes.length = 0
+            depths.length = 0
+
+            this.structures.forEach(structure => {
+                if (p.levels.includes('structure')) {
+                    texts.push(`${structure.data.model.label}`)
+                    positions.push(structure.data.boundary.sphere.center)
+                    sizes.push(structure.data.boundary.sphere.radius / 10)
+                    depths.push(structure.data.boundary.sphere.radius)
+                }
+
+                for (let i = 0, il = structure.data.units.length; i < il; ++i) {
+                    let label = ''
+                    const u = structure.data.units[i]
+                    l.unit = u
+                    l.element = u.elements[0]
+
+                    if (p.levels.includes('polymer') && u.polymerElements.length) {
+                        label = `${StructureProperties.entity.pdbx_description(l)} (${getAsymId(u)(l)})`
+                    }
+
+                    if (p.levels.includes('ligand') && !u.polymerElements.length) {
+                        const compId = StructureProperties.residue.label_comp_id(l)
+                        const chemComp = u.model.properties.chemicalComponentMap.get(compId)
+                        const moleculeType = chemComp ? chemComp.moleculeType : MoleculeType.unknown
+                        if (moleculeType === MoleculeType.other || moleculeType === MoleculeType.saccharide) {
+                            label = `${StructureProperties.entity.pdbx_description(l)} (${getAsymId(u)(l)})`
+                        }
+                    }
+
+                    if (label) {
+                        texts.push(label)
+                        const { center, radius } = u.lookup3d.boundary.sphere
+                        const transformedCenter = Vec3.transformMat4(Vec3.zero(), center, u.conformation.operator.matrix)
+                        positions.push(transformedCenter)
+                        sizes.push(Math.max(2, radius / 10))
+                        depths.push(radius)
+                    }
+                }
+            })
+        }
+
+        register(): void { }
+
+        async update(p: SceneLabelsProps) {
+            // console.log('update')
+            let updated = false
+            if (this.updateStructures(p) || !arrayEqual(this.params.levels, p.levels)) {
+                // console.log('update with data')
+                this.updateLabels(p)
+                await this.repr.createOrUpdate(p, this.data).run()
+                updated = true
+            } else if (!PD.areEqual(SceneLabelsParams, this.params, p)) {
+                // console.log('update props only')
+                await this.repr.createOrUpdate(p).run()
+                updated = true
+            }
+            if (updated) {
+                Object.assign(this.params, p)
+                this.ctx.canvas3d.add(this.repr)
+            }
+            return updated;
+        }
+
+        unregister() {
+
+        }
+    },
+    params: () => SceneLabelsParams
+});
+
+//
+
+function getRootStructure(root: StateObjectCell, state: State) {
+    let parent: StateObjectCell | undefined
+    while (true) {
+        const _parent = StateSelection.findAncestorOfType(state.tree, state.cells, root.transform.ref, [PluginStateObject.Molecule.Structure])
+        if (_parent) {
+            parent = _parent
+            root = _parent
+        } else {
+            break
+        }
+    }
+    return parent ? parent :
+        SO.Molecule.Structure.is(root.obj) ? root : undefined
+}
+
+function getAsymId(unit: Unit): StructureElement.Property<string> {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic:
+            return StructureProperties.chain.auth_asym_id
+        case Unit.Kind.Spheres:
+        case Unit.Kind.Gaussians:
+            return StructureProperties.coarse.asym_id
+    }
+}

+ 3 - 0
src/mol-plugin/index.ts

@@ -14,6 +14,8 @@ import { PluginSpec } from './spec';
 import { DownloadStructure, CreateComplexRepresentation, OpenStructure } from './state/actions/basic';
 import { StateTransforms } from './state/transforms';
 import { PluginBehaviors } from './behavior';
+import { ParamDefinition as PD } from 'mol-util/param-definition'
+import { SceneLabelsParams } from './behavior/dynamic/labels';
 
 function getParam(name: string, regex: string): string {
     let r = new RegExp(`${name}=(${regex})[&]?`, 'i');
@@ -38,6 +40,7 @@ const DefaultSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
         PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }),
         PluginSpec.Behavior(PluginBehaviors.Animation.StructureAnimation, { rotate: false, rotateValue: 0, explode: false, explodeValue: 0 }),
+        PluginSpec.Behavior(PluginBehaviors.Labels.SceneLabels, { ...PD.getDefaultValues(SceneLabelsParams) }), // TODO how to properly call PluginBehaviors.Labels.SceneLabels.definition.params()
         PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }),
     ]