From 8c9822da28cb7c8c7a232f96aa30a50b4d290e87 Mon Sep 17 00:00:00 2001
From: Alexander Rose <alex.rose@rcsb.org>
Date: Fri, 3 May 2019 15:56:42 -0700
Subject: [PATCH] improved carbohydrate/anomeric carbon picking/loci

---
 .../structure/carbohydrates/compute.ts        | 16 +++++++----
 .../structure/structure/carbohydrates/data.ts |  4 +--
 .../visual/carbohydrate-symbol-mesh.ts        | 20 +++++++-------
 src/mol-repr/structure/visual/util/common.ts  | 27 +++++++++++++++++++
 src/mol-theme/color/carbohydrate-symbol.ts    |  8 +++---
 5 files changed, 54 insertions(+), 21 deletions(-)

diff --git a/src/mol-model/structure/structure/carbohydrates/compute.ts b/src/mol-model/structure/structure/carbohydrates/compute.ts
index 8fc4688a5..342175438 100644
--- a/src/mol-model/structure/structure/carbohydrates/compute.ts
+++ b/src/mol-model/structure/structure/carbohydrates/compute.ts
@@ -421,16 +421,22 @@ function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[
         return `${unit.id}|${residueIndex}`
     }
 
-    const anomericCarbonMap = new Map<string, ElementIndex>()
+    const anomericCarbonMap = new Map<string, ElementIndex[]>()
     for (let i = 0, il = elements.length; i < il; ++i) {
         const { unit, anomericCarbon } = elements[i]
         const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index[anomericCarbon]
-        anomericCarbonMap.set(anomericCarbonKey(unit, residueIndex), anomericCarbon)
+        const k = anomericCarbonKey(unit, residueIndex)
+        if (anomericCarbonMap.has(k)) {
+            anomericCarbonMap.get(k)!.push(anomericCarbon)
+        } else {
+            anomericCarbonMap.set(k, [anomericCarbon])
+        }
     }
 
-    function getAnomericCarbon(unit: Unit, residueIndex: ResidueIndex) {
-        return anomericCarbonMap.get(anomericCarbonKey(unit, residueIndex))
+    const EmptyArray: ReadonlyArray<any> = []
+    function getAnomericCarbons(unit: Unit, residueIndex: ResidueIndex) {
+        return anomericCarbonMap.get(anomericCarbonKey(unit, residueIndex)) || EmptyArray
     }
 
-    return { getElementIndex, getLinkIndex, getLinkIndices, getTerminalLinkIndex, getTerminalLinkIndices, getAnomericCarbon }
+    return { getElementIndex, getLinkIndex, getLinkIndices, getTerminalLinkIndex, getTerminalLinkIndices, getAnomericCarbons }
 }
\ No newline at end of file
diff --git a/src/mol-model/structure/structure/carbohydrates/data.ts b/src/mol-model/structure/structure/carbohydrates/data.ts
index 1eaae487c..a75337837 100644
--- a/src/mol-model/structure/structure/carbohydrates/data.ts
+++ b/src/mol-model/structure/structure/carbohydrates/data.ts
@@ -49,7 +49,7 @@ export interface Carbohydrates {
     getLinkIndices: (unit: Unit, anomericCarbon: ElementIndex) => ReadonlyArray<number>
     getTerminalLinkIndex: (unitA: Unit, elementA: ElementIndex, unitB: Unit, elementB: ElementIndex) => number | undefined
     getTerminalLinkIndices: (unit: Unit, element: ElementIndex) => ReadonlyArray<number>
-    getAnomericCarbon: (unit: Unit, residueIndex: ResidueIndex) => ElementIndex | undefined
+    getAnomericCarbons: (unit: Unit, residueIndex: ResidueIndex) => ReadonlyArray<ElementIndex>
 }
 
 const EmptyArray: ReadonlyArray<any> = []
@@ -63,5 +63,5 @@ export const EmptyCarbohydrates: Carbohydrates = {
     getLinkIndices: () => EmptyArray,
     getTerminalLinkIndex: () => undefined,
     getTerminalLinkIndices: () => EmptyArray,
-    getAnomericCarbon: () => undefined,
+    getAnomericCarbons: () => [],
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
index c1c2b046f..075ffa575 100644
--- a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
+++ b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
@@ -25,7 +25,7 @@ import { OrderedSet, Interval } from 'mol-data/int';
 import { EmptyLoci, Loci } from 'mol-model/loci';
 import { VisualContext } from 'mol-repr/visual';
 import { Theme } from 'mol-theme/theme';
-import { getResidueLoci } from './util/common';
+import { getAltResidueLoci } from './util/common';
 
 const t = Mat4.identity()
 const sVec = Vec3.zero()
@@ -186,28 +186,28 @@ function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: num
     const { objectId, groupId } = pickingId
     if (id === objectId) {
         const carb = structure.carbohydrates.elements[Math.floor(groupId / 2)]
-        return getResidueLoci(structure, carb.unit, carb.anomericCarbon)
+        return getAltResidueLoci(structure, carb.unit, carb.anomericCarbon)
     }
     return EmptyLoci
 }
 
 /** For each carbohydrate (usually a monosaccharide) when all its residue's elements are in a loci. */
 function eachCarbohydrate(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
-    const { getElementIndex, getAnomericCarbon } = structure.carbohydrates
+    const { getElementIndex, getAnomericCarbons } = structure.carbohydrates
     let changed = false
     if (!StructureElement.isLoci(loci)) return false
     if (!Structure.areEquivalent(loci.structure, structure)) return false
     for (const e of loci.elements) {
+        // TODO make more efficient by handling/grouping `e.indices` by residue index
+        // TODO only call apply when the full alt-residue of the unit is part of `e`
         OrderedSet.forEach(e.indices, v => {
             const { model, elements } = e.unit
-            const { index, offsets } = model.atomicHierarchy.residueAtomSegments
+            const { index } = model.atomicHierarchy.residueAtomSegments
             const rI = index[elements[v]]
-            const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI])
-            const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1)
-            const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax)
-            if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
-            const eI = getAnomericCarbon(e.unit, rI)
-            if (eI !== undefined) {
+            const eIndices = getAnomericCarbons(e.unit, rI)
+            for (let i = 0, il = eIndices.length; i < il; ++i) {
+                const eI = eIndices[i]
+                if (!OrderedSet.has(e.indices, OrderedSet.indexOf(elements, eI))) continue
                 const idx = getElementIndex(e.unit, eI)
                 if (idx !== undefined) {
                     if (apply(Interval.ofBounds(idx * 2, idx * 2 + 2))) changed = true
diff --git a/src/mol-repr/structure/visual/util/common.ts b/src/mol-repr/structure/visual/util/common.ts
index f2f8faa6d..7aaa308da 100644
--- a/src/mol-repr/structure/visual/util/common.ts
+++ b/src/mol-repr/structure/visual/util/common.ts
@@ -28,6 +28,33 @@ export function getResidueLoci(structure: Structure, unit: Unit.Atomic, elementI
     return EmptyLoci
 }
 
+/**
+ * Return a Loci for the elements of a whole residue the elementIndex belongs to but
+ * restrict to elements that have the same label_alt_id or none
+ */
+export function getAltResidueLoci(structure: Structure, unit: Unit.Atomic, elementIndex: ElementIndex): Loci {
+    const { elements, model } = unit
+    const { label_alt_id } = model.atomicHierarchy.atoms
+    const elementAltId = label_alt_id.value(elementIndex)
+    if (OrderedSet.indexOf(elements, elementIndex) !== -1) {
+        const { index, offsets } = model.atomicHierarchy.residueAtomSegments
+        const rI = index[elementIndex]
+        const _indices: number[] = []
+        for (let i = offsets[rI], il = offsets[rI + 1]; i < il; ++i) {
+            const unitIndex = OrderedSet.indexOf(elements, i)
+            if (unitIndex !== -1) {
+                const altId = label_alt_id.value(i)
+                if (elementAltId === altId || altId === '') {
+                    _indices.push(unitIndex)
+                }
+            }
+        }
+        const indices = OrderedSet.ofSortedArray<StructureElement.UnitIndex>(SortedArray.ofSortedArray(_indices))
+        return StructureElement.Loci(structure, [{ unit, indices }])
+    }
+    return EmptyLoci
+}
+
 //
 
 export function createUnitsTransform({ units }: Unit.SymmetryGroup, transformData?: TransformData) {
diff --git a/src/mol-theme/color/carbohydrate-symbol.ts b/src/mol-theme/color/carbohydrate-symbol.ts
index a92d183e0..f76bd90ed 100644
--- a/src/mol-theme/color/carbohydrate-symbol.ts
+++ b/src/mol-theme/color/carbohydrate-symbol.ts
@@ -27,13 +27,13 @@ export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: PD.Va
     let color: LocationColor
 
     if (ctx.structure) {
-        const { elements, getElementIndex, getAnomericCarbon } = ctx.structure.carbohydrates
+        const { elements, getElementIndex, getAnomericCarbons } = ctx.structure.carbohydrates
 
         const getColor = (unit: Unit, index: ElementIndex) => {
             const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index[index]
-            const anomericCarbon = getAnomericCarbon(unit, residueIndex)
-            if (anomericCarbon !== undefined) {
-                const idx = getElementIndex(unit, anomericCarbon)
+            const anomericCarbons = getAnomericCarbons(unit, residueIndex)
+            if (anomericCarbons.length > 0) {
+                const idx = getElementIndex(unit, anomericCarbons[0])
                 if (idx !== undefined) return elements[idx].component.color
             }
             return DefaultColor
-- 
GitLab