Browse Source

wip, carbohydrates

Alexander Rose 6 years ago
parent
commit
7ee3a1f767

+ 11 - 3
src/mol-geo/representation/structure/cartoon.ts

@@ -14,6 +14,7 @@ import { PolymerTraceVisual, DefaultPolymerTraceProps } from './visual/polymer-t
 import { PolymerGapVisual, DefaultPolymerGapProps } from './visual/polymer-gap-cylinder';
 import { NucleotideBlockVisual, DefaultNucleotideBlockProps } from './visual/nucleotide-block-mesh';
 import { PolymerDirectionVisual, DefaultPolymerDirectionProps } from './visual/polymer-direction-wedge';
+import { CarbohydrateSymbolVisual } from './visual/carbohydrate-symbol-mesh';
 
 export const DefaultCartoonProps = {
     ...DefaultPolymerTraceProps,
@@ -28,13 +29,14 @@ export function CartoonRepresentation(): StructureRepresentation<CartoonProps> {
     const gapRepr = StructureUnitsRepresentation(PolymerGapVisual)
     const blockRepr = StructureUnitsRepresentation(NucleotideBlockVisual)
     const directionRepr = StructureUnitsRepresentation(PolymerDirectionVisual)
+    const carbohydrateRepr = StructureRepresentation(CarbohydrateSymbolVisual)
 
     return {
         get renderObjects() {
-            return [ ...traceRepr.renderObjects, ...gapRepr.renderObjects, ...blockRepr.renderObjects, ...directionRepr.renderObjects ]
+            return [ ...traceRepr.renderObjects, ...gapRepr.renderObjects, ...blockRepr.renderObjects, ...directionRepr.renderObjects, ...carbohydrateRepr.renderObjects ]
         },
         get props() {
-            return { ...traceRepr.props, ...gapRepr.props, ...blockRepr.props, ...directionRepr.props }
+            return { ...traceRepr.props, ...gapRepr.props, ...blockRepr.props, ...carbohydrateRepr.props }
         },
         create: (structure: Structure, props: CartoonProps = {} as CartoonProps) => {
             const p = Object.assign({}, DefaultCartoonProps, props)
@@ -43,6 +45,7 @@ export function CartoonRepresentation(): StructureRepresentation<CartoonProps> {
                 await gapRepr.create(structure, p).runInContext(ctx)
                 await blockRepr.create(structure, p).runInContext(ctx)
                 await directionRepr.create(structure, p).runInContext(ctx)
+                await carbohydrateRepr.create(structure, p).runInContext(ctx)
             })
         },
         update: (props: CartoonProps) => {
@@ -52,6 +55,7 @@ export function CartoonRepresentation(): StructureRepresentation<CartoonProps> {
                 await gapRepr.update(p).runInContext(ctx)
                 await blockRepr.update(p).runInContext(ctx)
                 await directionRepr.update(p).runInContext(ctx)
+                await carbohydrateRepr.update(p).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {
@@ -59,22 +63,26 @@ export function CartoonRepresentation(): StructureRepresentation<CartoonProps> {
             const gapLoci = gapRepr.getLoci(pickingId)
             const blockLoci = blockRepr.getLoci(pickingId)
             const directionLoci = directionRepr.getLoci(pickingId)
+            const carbohydrateRepr = directionRepr.getLoci(pickingId)
             return !isEmptyLoci(traceLoci) ? traceLoci
                 : !isEmptyLoci(gapLoci) ? gapLoci
                 : !isEmptyLoci(blockLoci) ? blockLoci
-                : directionLoci
+                : !isEmptyLoci(directionLoci) ? directionLoci
+                : carbohydrateRepr
         },
         mark: (loci: Loci, action: MarkerAction) => {
             traceRepr.mark(loci, action)
             gapRepr.mark(loci, action)
             blockRepr.mark(loci, action)
             directionRepr.mark(loci, action)
+            carbohydrateRepr.mark(loci, action)
         },
         destroy() {
             traceRepr.destroy()
             gapRepr.destroy()
             blockRepr.destroy()
             directionRepr.destroy()
+            carbohydrateRepr.destroy()
         }
     }
 }

+ 128 - 0
src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts

@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Unit, Structure } from 'mol-model/structure';
+import { DefaultStructureProps, StructureVisual } from '..';
+import { RuntimeContext } from 'mol-task'
+import { createIdentityTransform } from './util/common';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { createMarkers, MarkerAction } from '../../../util/marker-data';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { SizeTheme } from '../../../theme';
+import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
+import { MeshBuilder } from '../../../shape/mesh-builder';
+import { Vec3, Mat4 } from 'mol-math/linear-algebra';
+import { createUniformColor } from '../../../util/color-data';
+
+async function createCarbohydrateSymbolMesh(ctx: RuntimeContext, structure: Structure, mesh?: Mesh) {
+    const builder = MeshBuilder.create(256, 128, mesh)
+
+    const t = Mat4.identity()
+    const p = Vec3.zero()
+    const { carbohydrates } = structure
+
+    const linkParams = { radiusTop: 0.2, radiusBottom: 0.2 }
+
+    for (let i = 0, il = carbohydrates.elements.length; i < il; ++i) {
+        const c = carbohydrates.elements[i]
+        Mat4.setTranslation(t, c.center)
+        builder.addBox(t, { width: 2, height: 2, depth: 2 })
+    }
+
+    for (let i = 0, il = carbohydrates.links.length; i < il; ++i) {
+        const l = carbohydrates.links[i]
+        const centerA = carbohydrates.elements[l.carbohydrateIndexA].center
+        const centerB = carbohydrates.elements[l.carbohydrateIndexB].center
+        builder.addCylinder(centerA, centerB, 0.5, linkParams)
+    }
+
+    for (let i = 0, il = carbohydrates.terminalLinks.length; i < il; ++i) {
+        const tl = carbohydrates.terminalLinks[i]
+        const center = carbohydrates.elements[tl.carbohydrateIndex].center
+        tl.elementUnit.conformation.position(tl.elementUnit.elements[tl.elementIndex], p)
+        if (tl.fromCarbohydrate) {
+            builder.addCylinder(center, p, 0.5, linkParams)
+        } else {
+            builder.addCylinder(p, center, 0.5, linkParams)
+        }
+    }
+
+    return builder.getMesh()
+}
+
+export const DefaultCarbohydrateSymbolProps = {
+    ...DefaultMeshProps,
+    ...DefaultStructureProps,
+    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
+    detail: 0,
+    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+}
+export type CarbohydrateSymbolProps = Partial<typeof DefaultCarbohydrateSymbolProps>
+
+export function CarbohydrateSymbolVisual(): StructureVisual<CarbohydrateSymbolProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultCarbohydrateSymbolProps
+    let mesh: Mesh
+    let currentStructure: Structure
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, structure: Structure, props: CarbohydrateSymbolProps = {}) {
+            currentProps = Object.assign({}, DefaultCarbohydrateSymbolProps, props)
+            currentStructure = structure
+
+            const instanceCount = 1
+            const elementCount = currentStructure.elementCount
+
+            mesh = await createCarbohydrateSymbolMesh(ctx, currentStructure, mesh)
+            // console.log(mesh)
+
+            const transforms = createIdentityTransform()
+            const color = createUniformColor({ value: 0x999911 }) // TODO
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+                aColor: ValueCell.create(new Float32Array(mesh.vertexCount * 3))
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: CarbohydrateSymbolProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            return false
+        },
+        getLoci(pickingId: PickingId) {
+            return EmptyLoci
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            // TODO
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 2 - 2
src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts

@@ -33,8 +33,8 @@ async function createNucleotideBlockMesh(ctx: RuntimeContext, unit: Unit, mesh?:
     const builder = MeshBuilder.create(256, 128, mesh)
 
     const { elements, model } = unit
-    const { chemicalComponentMap, modifiedResidues } = unit.model.properties
-    const { chainAtomSegments, residueAtomSegments, residues } = unit.model.atomicHierarchy
+    const { chemicalComponentMap, modifiedResidues } = model.properties
+    const { chainAtomSegments, residueAtomSegments, residues } = model.atomicHierarchy
     const { label_comp_id } = residues
     const pos = unit.conformation.invariantPosition
 

+ 20 - 0
src/mol-math/linear-algebra/3d/vec3.ts

@@ -162,6 +162,26 @@ namespace Vec3 {
         return out;
     }
 
+    /**
+     * Returns the minimum of two Vec3's
+     */
+    export function min(out: Vec3, a: Vec3, b: Vec3) {
+        out[0] = Math.min(a[0], b[0]);
+        out[1] = Math.min(a[1], b[1]);
+        out[2] = Math.min(a[2], b[2]);
+        return out;
+    }
+
+    /**
+     * Returns the maximum of two Vec3's
+     */
+    export function max(out: Vec3, a: Vec3, b: Vec3) {
+        out[0] = Math.max(a[0], b[0]);
+        out[1] = Math.max(a[1], b[1]);
+        out[2] = Math.max(a[2], b[2]);
+        return out;
+    }
+
     export function distance(a: Vec3, b: Vec3) {
         const x = b[0] - a[0],
             y = b[1] - a[1],

+ 127 - 0
src/mol-model/structure/structure/carbohydrates/compute.ts

@@ -0,0 +1,127 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import Unit from '../unit';
+import { ResidueIndex } from '../../model';
+import { Interval, Segmentation } from 'mol-data/int';
+import Structure from '../structure';
+import { Carbohydrates, CarbohydrateLink, CarbohydrateTerminalLink, CarbohydrateElement } from './data';
+import { SaccharideNameMap } from './constants';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { getCenterAndRadius } from '../../util';
+
+function getResidueIndex(elementIndex: number, unit: Unit.Atomic) {
+    return unit.model.atomicHierarchy.residueAtomSegments.index[unit.elements[elementIndex]]
+}
+
+function getRingIndices(unit: Unit.Atomic, rI: ResidueIndex) {
+    const { offsets } = unit.model.atomicHierarchy.residueAtomSegments
+    const { elements } = unit
+    const interval = Interval.ofBounds(offsets[rI], offsets[rI + 1])
+    const rings = unit.rings.byFingerprint.get('C-C-C-C-C-O') || unit.rings.byFingerprint.get('C-C-C-C-O')
+    if (rings) {
+        for (let i = 0, il = rings.length; i < il; ++i) {
+            let withinIntervalCount = 0
+            const ring = unit.rings.all[rings[i]]
+            for (let j = 0, jl = ring.length; j < jl; ++j) {
+                if (Interval.has(interval, elements[ring[j]])) ++withinIntervalCount
+            }
+            if (withinIntervalCount === ring.length) return ring
+        }
+    }
+}
+
+export function computeCarbohydrates(structure: Structure): Carbohydrates {
+    const links: CarbohydrateLink[] = []
+    const terminalLinks: CarbohydrateTerminalLink[] = []
+    const elements: CarbohydrateElement[] = []
+
+    const elementsMap = new Map<string, number>()
+
+    function elementKey(residueIndex: number, unitId: number) {
+        return `${residueIndex}|${unitId}`
+    }
+
+    for (let i = 0, il = structure.units.length; i < il; ++i) {
+        const unit = structure.units[i]
+        if (!Unit.isAtomic(unit)) continue
+
+        const { model } = unit
+        const { chainAtomSegments, residueAtomSegments, residues } = model.atomicHierarchy
+        const { label_comp_id } = residues
+
+        const chainIt = Segmentation.transientSegments(chainAtomSegments, unit.elements)
+        const residueIt = Segmentation.transientSegments(residueAtomSegments, unit.elements)
+
+        while (chainIt.hasNext) {
+            residueIt.setSegment(chainIt.move());
+
+            while (residueIt.hasNext) {
+                const { index: residueIndex } = residueIt.move();
+
+                const saccharideComp = SaccharideNameMap.get(label_comp_id.value(residueIndex))
+                if (!saccharideComp) continue
+
+                const ringIndices = getRingIndices(unit, residueIndex)
+                if (ringIndices) {
+                    const center = Vec3.zero()
+                    const normal = Vec3.zero()
+                    const direction = Vec3.zero()
+                    const ringRadius = getCenterAndRadius(center, unit, ringIndices)
+                    console.log(ringRadius, center)
+
+                    elementsMap.set(elementKey(residueIndex, unit.id), elements.length)
+                    elements.push({ center, normal, direction, unit, residueIndex, component: saccharideComp })
+                } else {
+                    console.warn('No ring found for carbohydrate')
+                }
+            }
+        }
+    }
+
+    elementsMap.forEach((elementIndex, key) => {
+        const unit = elements[elementIndex].unit
+        structure.links.getLinkedUnits(unit).forEach(pairBonds => {
+            pairBonds.linkedElementIndices.forEach(indexA => {
+                pairBonds.getBonds(indexA).forEach(bondInfo => {
+                    let { unitA, unitB } = pairBonds
+                    let indexB = bondInfo.indexB
+                    let residueIndexA = getResidueIndex(indexA, unitA)
+                    let residueIndexB = getResidueIndex(indexB, unitB)
+                    let keyA = elementKey(residueIndexA, unitA.id)
+                    let keyB = elementKey(residueIndexB, unitB.id)
+                    if (key === keyB) {
+                        [keyB, keyA] = [keyA, keyB];
+                        [indexB, indexA] = [indexA, indexB];
+                        [unitB, unitA] = [unitA, unitB];
+                    }
+                    const elementIndexB = elementsMap.get(keyB)
+                    if (elementIndexB !== undefined) {
+                        links.push({
+                            carbohydrateIndexA: elementIndex,
+                            carbohydrateIndexB: elementIndexB
+                        })
+                    } else {
+                        terminalLinks.push({
+                            carbohydrateIndex: elementIndex,
+                            elementIndex: indexB,
+                            elementUnit: unitB,
+                            fromCarbohydrate: true
+                        })
+                        terminalLinks.push({
+                            carbohydrateIndex: elementIndex,
+                            elementIndex: indexB,
+                            elementUnit: unitB,
+                            fromCarbohydrate: false
+                        })
+                    }
+                })
+            })
+        })
+    })
+
+    return { links, terminalLinks, elements }
+}

+ 270 - 0
src/mol-model/structure/structure/carbohydrates/constants.ts

@@ -0,0 +1,270 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+// https://www.ncbi.nlm.nih.gov/glycans/snfg.html
+
+export const enum SaccharideShapes {
+    FilledSphere, FilledCube, CrossedCube, DividedDiamond, FilledCone, DevidedCone,
+    FlatBox, FilledStar, FilledDiamond, FlatDiamond, FlatHexagon, Pentagon
+}
+
+// TODO move to theme
+const enum SaccharideColors {
+    Blue = 0x0090bc,
+    Green =	0x00a651,
+    Yellow = 0xffd400,
+    Orange = 0xf47920,
+    Pink = 0xf69ea1,
+    Purple = 0xa54399,
+    LightBlue = 0x8fcce9,
+    Brown = 0xa17a4d,
+    Red = 0xed1c24,
+
+    Secondary = 0xf1ece1
+}
+
+export const enum SaccharideType {
+    Hexose, HexNAc, Hexosamine, Hexuronate, Deoxyhexose, DeoxyhexNAc, DiDeoxyhexose,
+    Pentose, Deoxynonulosonate, DiDeoxynonulosonate, Unknown, Assigned
+}
+
+const SaccharideTypeNameMap = {
+    [SaccharideType.Hexose]: 'Hexose',
+    [SaccharideType.HexNAc]: 'HexNAc',
+    [SaccharideType.Hexosamine]: 'Hexosamine',
+    [SaccharideType.Hexuronate]: 'Hexuronate',
+    [SaccharideType.Deoxyhexose]: 'Deoxyhexose',
+    [SaccharideType.DeoxyhexNAc]: 'DeoxyhexNAc',
+    [SaccharideType.DiDeoxyhexose]: 'Di-deoxyhexose',
+    [SaccharideType.Pentose]: 'Pentose',
+    [SaccharideType.Deoxynonulosonate]: 'Deoxynonulosonate',
+    [SaccharideType.DiDeoxynonulosonate]: 'Di-deoxynonulosonate',
+    [SaccharideType.Unknown]: 'Unknown',
+    [SaccharideType.Assigned]: 'Assigned',
+}
+
+export function getSaccharideName(type: SaccharideType) {
+    return SaccharideTypeNameMap[type]
+}
+
+const SaccharideTypeShapeMap = {
+    [SaccharideType.Hexose]: SaccharideShapes.FilledSphere,
+    [SaccharideType.HexNAc]: SaccharideShapes.FilledCube,
+    [SaccharideType.Hexosamine]: SaccharideShapes.CrossedCube,
+    [SaccharideType.Hexuronate]: SaccharideShapes.DividedDiamond,
+    [SaccharideType.Deoxyhexose]: SaccharideShapes.FilledCone,
+    [SaccharideType.DeoxyhexNAc]: SaccharideShapes.DevidedCone,
+    [SaccharideType.DiDeoxyhexose]: SaccharideShapes.FlatBox,
+    [SaccharideType.Pentose]: SaccharideShapes.FilledStar,
+    [SaccharideType.Deoxynonulosonate]: SaccharideShapes.FilledDiamond,
+    [SaccharideType.DiDeoxynonulosonate]: SaccharideShapes.FlatDiamond,
+    [SaccharideType.Unknown]: SaccharideShapes.FlatHexagon,
+    [SaccharideType.Assigned]: SaccharideShapes.Pentagon,
+}
+
+export function getSaccharideShape(type: SaccharideType) {
+    return SaccharideTypeShapeMap[type]
+}
+
+export type SaccharideComponent = {
+    abbr: string
+    name: string
+    color: SaccharideColors
+    type: SaccharideType
+}
+
+const Monosaccharides: SaccharideComponent[] = [
+    { abbr: 'Glc', name: 'Glucose', color: SaccharideColors.Blue, type: SaccharideType.Hexose },
+    { abbr: 'Man', name: 'Mannose', color: SaccharideColors.Green, type: SaccharideType.Hexose },
+    { abbr: 'Gal', name: 'Galactose', color: SaccharideColors.Yellow, type: SaccharideType.Hexose },
+    { abbr: 'Gul', name: 'Gulose', color: SaccharideColors.Orange, type: SaccharideType.Hexose },
+    { abbr: 'Alt', name: 'Altrose', color: SaccharideColors.Pink, type: SaccharideType.Hexose },
+    { abbr: 'All', name: 'Allose', color: SaccharideColors.Purple, type: SaccharideType.Hexose },
+    { abbr: 'Tal', name: 'Talose', color: SaccharideColors.LightBlue, type: SaccharideType.Hexose },
+    { abbr: 'Ido', name: 'Idose', color: SaccharideColors.Brown, type: SaccharideType.Hexose },
+
+    { abbr: 'GlcNAc', name: 'N-Acetyl Glucosamine', color: SaccharideColors.Blue, type: SaccharideType.HexNAc },
+    { abbr: 'ManNAc', name: 'N-Acetyl Mannosamine', color: SaccharideColors.Green, type: SaccharideType.HexNAc },
+    { abbr: 'GalNAc', name: 'N-Acetyl Galactosamine', color: SaccharideColors.Yellow, type: SaccharideType.HexNAc },
+    { abbr: 'GulNAc', name: 'N-Acetyl Gulosamine', color: SaccharideColors.Orange, type: SaccharideType.HexNAc },
+    { abbr: 'AltNAc', name: 'N-Acetyl Altrosamine', color: SaccharideColors.Pink, type: SaccharideType.HexNAc },
+    { abbr: 'AllNAc', name: 'N-Acetyl Allosamine', color: SaccharideColors.Purple, type: SaccharideType.HexNAc },
+    { abbr: 'TalNAc', name: 'N-Acetyl Talosamine', color: SaccharideColors.LightBlue, type: SaccharideType.HexNAc },
+    { abbr: 'IdoNAc', name: 'N-Acetyl Idosamine', color: SaccharideColors.Brown, type: SaccharideType.HexNAc },
+
+    { abbr: 'GlcN', name: 'Glucosamine', color: SaccharideColors.Blue, type: SaccharideType.Hexosamine },
+    { abbr: 'ManN', name: 'Mannosamine', color: SaccharideColors.Green, type: SaccharideType.Hexosamine },
+    { abbr: 'GalN', name: 'Galactosamine', color: SaccharideColors.Yellow, type: SaccharideType.Hexosamine },
+    { abbr: 'GulN', name: 'Gulosamine', color: SaccharideColors.Orange, type: SaccharideType.Hexosamine },
+    { abbr: 'AltN', name: 'Altrosamine', color: SaccharideColors.Pink, type: SaccharideType.Hexosamine },
+    { abbr: 'AllN', name: 'Allosamine', color: SaccharideColors.Purple, type: SaccharideType.Hexosamine },
+    { abbr: 'TalN', name: 'Talosamine', color: SaccharideColors.LightBlue, type: SaccharideType.Hexosamine },
+    { abbr: 'IdoN', name: 'Idosamine', color: SaccharideColors.Brown, type: SaccharideType.Hexosamine },
+
+    { abbr: 'GlcA', name: 'Glucuronic Acid', color: SaccharideColors.Blue, type: SaccharideType.Hexuronate },
+    { abbr: 'ManA', name: 'Mannuronic Acid', color: SaccharideColors.Green, type: SaccharideType.Hexuronate },
+    { abbr: 'GalA', name: 'Galacturonic Acid', color: SaccharideColors.Yellow, type: SaccharideType.Hexuronate },
+    { abbr: 'GulA', name: 'Guluronic Acid', color: SaccharideColors.Orange, type: SaccharideType.Hexuronate },
+    { abbr: 'AltA', name: 'Altruronic Acid', color: SaccharideColors.Pink, type: SaccharideType.Hexuronate },
+    { abbr: 'AllA', name: 'Alluronic Acid', color: SaccharideColors.Purple, type: SaccharideType.Hexuronate },
+    { abbr: 'TalA', name: 'Taluronic Acid', color: SaccharideColors.LightBlue, type: SaccharideType.Hexuronate },
+    { abbr: 'IdoA', name: 'Iduronic Acid', color: SaccharideColors.Brown, type: SaccharideType.Hexuronate },
+
+    { abbr: 'Qui', name: 'Quinovose', color: SaccharideColors.Blue, type: SaccharideType.Deoxyhexose },
+    { abbr: 'Rha', name: 'Rhamnose', color: SaccharideColors.Green, type: SaccharideType.Deoxyhexose },
+    { abbr: '6dGul', name: '6-Deoxy Gulose', color: SaccharideColors.Orange, type: SaccharideType.Deoxyhexose },
+    { abbr: '6dAlt', name: '6-Deoxy Altrose', color: SaccharideColors.Pink, type: SaccharideType.Deoxyhexose },
+    { abbr: '6dTal', name: '6-Deoxy Talose', color: SaccharideColors.LightBlue, type: SaccharideType.Deoxyhexose },
+    { abbr: 'Fuc', name: 'Fucose', color: SaccharideColors.Red, type: SaccharideType.Deoxyhexose },
+
+    { abbr: 'QuiNAc', name: 'N-Acetyl Quinovosamine', color: SaccharideColors.Blue, type: SaccharideType.DeoxyhexNAc },
+    { abbr: 'RhaNAc', name: 'N-Acetyl Rhamnosamine', color: SaccharideColors.Green, type: SaccharideType.DeoxyhexNAc },
+    { abbr: '6dAltNAc', name: 'N-Acetyl 6-Deoxy Altrosamine', color: SaccharideColors.Pink, type: SaccharideType.DeoxyhexNAc },
+    { abbr: '6dTalNAc', name: 'N-Acetyl 6-Deoxy Talosamine', color: SaccharideColors.LightBlue, type: SaccharideType.DeoxyhexNAc },
+    { abbr: 'FucNAc', name: 'N-Acetyl Fucosamine', color: SaccharideColors.Red, type: SaccharideType.DeoxyhexNAc },
+
+    { abbr: 'Oli', name: 'Olivose', color: SaccharideColors.Blue, type: SaccharideType.DiDeoxyhexose },
+    { abbr: 'Tyv', name: 'Tyvelose', color: SaccharideColors.Green, type: SaccharideType.DiDeoxyhexose },
+    { abbr: 'Abe', name: 'Abequose', color: SaccharideColors.Orange, type: SaccharideType.DiDeoxyhexose },
+    { abbr: 'Par', name: 'Paratose', color: SaccharideColors.Pink, type: SaccharideType.DiDeoxyhexose },
+    { abbr: 'Dig', name: 'Digitoxose', color: SaccharideColors.Purple, type: SaccharideType.DiDeoxyhexose },
+    { abbr: 'Col', name: 'Colitose', color: SaccharideColors.LightBlue, type: SaccharideType.DiDeoxyhexose },
+
+    { abbr: 'Ara', name: 'Arabinose', color: SaccharideColors.Green, type: SaccharideType.Pentose },
+    { abbr: 'Lyx', name: 'Lyxose', color: SaccharideColors.Yellow, type: SaccharideType.Pentose },
+    { abbr: 'Xyl', name: 'Xylose', color: SaccharideColors.Orange, type: SaccharideType.Pentose },
+    { abbr: 'Rib', name: 'Ribose', color: SaccharideColors.Pink, type: SaccharideType.Pentose },
+
+    { abbr: 'Kdn', name: 'Keto-Deoxy Nonulonic Acid', color: SaccharideColors.Green, type: SaccharideType.Deoxynonulosonate },
+    { abbr: 'Neu5Ac', name: 'N-Acetyl Neuraminic Acid', color: SaccharideColors.Purple, type: SaccharideType.Deoxynonulosonate },
+    { abbr: 'Neu5Gc', name: 'N-Glycolyl Neuraminic Acid', color: SaccharideColors.LightBlue, type: SaccharideType.Deoxynonulosonate },
+    { abbr: 'Neu', name: 'Neuraminic Acid', color: SaccharideColors.Brown, type: SaccharideType.Deoxynonulosonate },
+    { abbr: 'Sia', name: 'Sialic acid', color: SaccharideColors.Red, type: SaccharideType.Deoxynonulosonate },
+
+    { abbr: 'Pse', name: 'Pseudaminic Acid', color: SaccharideColors.Green, type: SaccharideType.DiDeoxynonulosonate },
+    { abbr: 'Leg', name: 'Legionaminic Acid', color: SaccharideColors.Yellow, type: SaccharideType.DiDeoxynonulosonate },
+    { abbr: 'Aci', name: 'Acinetaminic Acid', color: SaccharideColors.Pink, type: SaccharideType.DiDeoxynonulosonate },
+    { abbr: '4eLeg', name: '4-Epilegionaminic Acid', color: SaccharideColors.LightBlue, type: SaccharideType.DiDeoxynonulosonate },
+
+    { abbr: 'Bac', name: 'Bacillosamine', color: SaccharideColors.Blue, type: SaccharideType.Unknown },
+    { abbr: 'LDManHep', name: 'L-Glycero-D-Manno Heptose', color: SaccharideColors.Green, type: SaccharideType.Unknown },
+    { abbr: 'Kdo', name: 'Keto-Deoxy Octulonic Acid', color: SaccharideColors.Yellow, type: SaccharideType.Unknown },
+    { abbr: 'Dha', name: '3-Deoxy Lyxo-Heptulosaric Acid', color: SaccharideColors.Orange, type: SaccharideType.Unknown },
+    { abbr: 'DDManHep', name: 'D-Glycero-D-Manno-Heptose', color: SaccharideColors.Pink, type: SaccharideType.Unknown },
+    { abbr: 'MurNAc', name: 'N-Acetyl Muramic Acid', color: SaccharideColors.Purple, type: SaccharideType.Unknown },
+    { abbr: 'MurNGc', name: 'N-Glycolyl Muramic Acid', color: SaccharideColors.LightBlue, type: SaccharideType.Unknown },
+    { abbr: 'Mur', name: 'Muramic Acid', color: SaccharideColors.Brown, type: SaccharideType.Unknown },
+
+    { abbr: 'Api', name: 'Apicose', color: SaccharideColors.Green, type: SaccharideType.Assigned },
+    { abbr: 'Fru', name: 'Fructose', color: SaccharideColors.Green, type: SaccharideType.Assigned },
+    { abbr: 'Tag', name: 'Tagatose', color: SaccharideColors.Yellow, type: SaccharideType.Assigned },
+    { abbr: 'Sor', name: 'Sorbose', color: SaccharideColors.Orange, type: SaccharideType.Assigned },
+    { abbr: 'Psi', name: 'Psicose', color: SaccharideColors.Pink, type: SaccharideType.Assigned },
+]
+
+const CommonSaccharideNames: { [k: string]: string[] } = {
+    // Hexose
+    Glc: ['GLC', 'BGC'],
+    Man: ['MAN', 'BMA'],
+    Gal: ['GAL', 'GLA'],
+    Gul: ['GUP', 'GL0'],
+    Alt: ['ALT'],
+    All: ['ALL', 'AFD'],
+    Tal: ['TAL'],
+    Ido: ['4N2'],
+    // HexNAc
+    GlcNAc: ['NAG', 'NDG'],
+    ManNAc: ['NGA', 'A2G'],
+    GulNAc: [],
+    AltNAc: [],
+    AllNAc: ['NAA'],
+    TalNAc: [],
+    IdoNAc: ['HSQ'],
+    // Hexosamine
+    GlcN: ['GCS', 'PA1'],
+    ManN: ['95Z'],
+    GalN: ['X6X', '1GN'],
+    GulN: [],
+    AltN: [],
+    AllN: [],
+    TalN: [],
+    IdoN: [],
+    // Hexuronate
+    GlcA: ['GCU', 'BDP'],
+    ManA: ['MAV', 'BEM'],
+    GalA: ['ADA', 'GTR'],
+    GulA: ['LGU'],
+    AltA: [],
+    AllA: [],
+    TalA: ['X0X', 'X1X'],
+    IdoA: ['IDR'],
+    // Deoxyhexose
+    Qui: ['G6D'],
+    Rha: ['RAM', 'RM4'],
+    '6dGul': [],
+    '6dAlt': [],
+    '6dTal': [],
+    Fuc: ['FUC', 'FUL'],
+    // DeoxyhexNAc
+    QuiNAc: [],
+    RhaNAc: [],
+    '6dAltNAc': [],
+    '6dTalNAc': [],
+    FucNAc: [],
+    // Di-deoxyhexose
+    Oli: ['DDA'],
+    Tyv: ['TYV'],
+    Abe: ['ABE'],
+    Par: ['PZU'],
+    Dig: [],
+    Col: [],
+    // Pentose
+    Ara: ['ARA', 'ARB'],
+    Lyx: ['LDY'],
+    Xyl: ['XYS', 'XYP'],
+    Rib: ['RIP', '0MK'],
+    // Deoxynonulosonate
+    Kdn: ['KDN', 'KDM'],
+    Neu5Ac: ['SIA', 'SLB'],
+    Neu5Gc: ['NGC', 'NGE'],
+    Neu: [],
+    Sia: [],
+    // Di-deoxynonulosonate
+    Pse: ['6PZ'],
+    Leg: [],
+    Aci: [],
+    '4eLeg': [],
+    // Unknown
+    Bac: ['B6D'],
+    LDManHep: ['GMH'],
+    Kdo: ['KDO'],
+    Dha: [],
+    DDManHep: [],
+    MurNAc: ['AMU'],
+    MurNGc: [],
+    Mur: ['MUR'],
+    // Assigned
+    Api: ['XXM'],
+    Fru: ['BDF'],
+    Tag: ['T6T'],
+    Sor: ['SOE'],
+    Psi: [],
+}
+
+export const SaccharideNameMap = (function () {
+    const map = new Map<string, SaccharideComponent>()
+    for (let i = 0, il = Monosaccharides.length; i < il; ++i) {
+        const saccharide = Monosaccharides[i]
+        const names = CommonSaccharideNames[saccharide.abbr]
+        if (names) {
+            for (let j = 0, jl = names.length; j < jl; ++j) {
+                map.set(names[j], saccharide)
+            }
+        }
+    }
+    return map
+})()

+ 38 - 0
src/mol-model/structure/structure/carbohydrates/data.ts

@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import Unit from '../unit';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { ResidueIndex } from '../../model';
+import { SaccharideComponent } from './constants';
+
+export interface CarbohydrateLink {
+    readonly carbohydrateIndexA: number
+    readonly carbohydrateIndexB: number
+}
+
+export interface CarbohydrateTerminalLink {
+    readonly carbohydrateIndex: number
+    readonly elementIndex: number
+    readonly elementUnit: Unit
+    /** specifies direction of the link */
+    readonly fromCarbohydrate: boolean
+}
+
+export interface CarbohydrateElement {
+    readonly center: Vec3,
+    readonly normal: Vec3,
+    readonly direction: Vec3,
+    readonly unit: Unit.Atomic
+    readonly residueIndex: ResidueIndex
+    readonly component: SaccharideComponent
+}
+
+export interface Carbohydrates {
+    links: ReadonlyArray<CarbohydrateLink>
+    terminalLinks: ReadonlyArray<CarbohydrateTerminalLink>
+    elements: ReadonlyArray<CarbohydrateElement>
+}

+ 9 - 0
src/mol-model/structure/structure/structure.ts

@@ -19,6 +19,8 @@ import { CrossLinkRestraints, extractCrossLinkRestraints } from './unit/pair-res
 import StructureSymmetry from './symmetry';
 import StructureProperties from './properties';
 import { ResidueIndex } from '../model/indexing';
+import { Carbohydrates } from './carbohydrates/data';
+import { computeCarbohydrates } from './carbohydrates/compute';
 
 class Structure {
     readonly unitMap: IntMap<Unit>;
@@ -86,6 +88,13 @@ class Structure {
         return this._unitSymmetryGroups;
     }
 
+    private _carbohydrates?: Carbohydrates = void 0;
+    get carbohydrates(): Carbohydrates {
+        if (this._carbohydrates) return this._carbohydrates;
+        this._carbohydrates = computeCarbohydrates(this);
+        return this._carbohydrates;
+    }
+
     constructor(units: ArrayLike<Unit>) {
         const map = IntMap.Mutable<Unit>();
         let elementCount = 0;

+ 17 - 0
src/mol-model/structure/util.ts

@@ -6,6 +6,8 @@
 
 import { Model, ResidueIndex, ElementIndex } from './model';
 import { MoleculeType, AtomRole, MoleculeTypeAtomRoleId } from './model/types';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { Unit } from './structure';
 
 export function getMoleculeType(model: Model, rI: ResidueIndex) {
     const compId = model.atomicHierarchy.residues.label_comp_id.value(rI)
@@ -43,4 +45,19 @@ export function residueLabel(model: Model, rI: number) {
     const { label_asym_id } = chains
     const cI = chainAtomSegments.index[residueAtomSegments.offsets[rI]]
     return `${label_asym_id.value(cI)} ${label_comp_id.value(rI)} ${label_seq_id.value(rI)}`
+}
+
+const centerPos = Vec3.zero()
+const centerMin = Vec3.zero()
+export function getCenterAndRadius(centroid: Vec3, unit: Unit, indices: ArrayLike<number>) {
+    const pos = unit.conformation.position
+    const { elements } = unit
+    Vec3.set(centroid, 0, 0, 0)
+    for (let i = 0, il = indices.length; i < il; ++i) {
+        pos(elements[indices[i]], centerPos)
+        Vec3.add(centroid, centroid, centerPos)
+        Vec3.min(centerMin, centerMin, centerPos)
+    }
+    Vec3.scale(centroid, centroid, 1/indices.length)
+    return Vec3.distance(centerMin, centroid)
 }

+ 8 - 6
src/mol-view/stage.ts

@@ -27,8 +27,8 @@ const spacefillProps: SpacefillProps = {
 const ballAndStickProps: BallAndStickProps = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
-    sizeTheme: { name: 'uniform', value: 0.05 },
-    linkRadius: 0.05,
+    sizeTheme: { name: 'uniform', value: 0.15 },
+    linkRadius: 0.15,
     quality: 'auto',
     useFog: false
 }
@@ -78,7 +78,7 @@ export class Stage {
         // this.loadPdbid('1hrv') // viral assembly
         // this.loadPdbid('1rb8') // virus
         // this.loadPdbid('1blu') // metal coordination
-        // this.loadPdbid('3pqr') // inter unit bonds, two polymer chains, ligands, water
+        this.loadPdbid('3pqr') // inter unit bonds, two polymer chains, ligands, water, carbohydrates linked to protein
         // this.loadPdbid('4v5a') // ribosome
         // this.loadPdbid('3j3q') // ...
         // this.loadPdbid('2np2') // dna
@@ -92,8 +92,10 @@ export class Stage {
         // this.loadPdbid('1xv6') // rna, modified nucleotides
         // this.loadPdbid('3bbm') // rna with linker
         // this.loadPdbid('1gfl') // GFP, flourophore has carbonyl oxygen removed
-        this.loadPdbid('1sfi') // contains cyclic peptid
+        // this.loadPdbid('1sfi') // contains cyclic peptid
         // this.loadPdbid('3sn6') // discontinuous chains
+        // this.loadPdbid('2zex') // small, contains carbohydrate polymer
+        // this.loadPdbid('2b5t') // contains large carbohydrate polymer
         // this.loadMmcifUrl(`../../examples/1cbs_full.bcif`)
         // this.loadMmcifUrl(`../../examples/1cbs_updated.cif`)
         // this.loadMmcifUrl(`../../examples/1crn.cif`)
@@ -119,10 +121,10 @@ export class Stage {
         console.log(modelEntity.value)
         const structureEntity = await ModelToStructure.apply(this.ctx, modelEntity)
 
-        StructureToBallAndStick.apply(this.ctx, structureEntity, { ...ballAndStickProps, visible: false })
+        StructureToBallAndStick.apply(this.ctx, structureEntity, { ...ballAndStickProps, visible: true })
         StructureToSpacefill.apply(this.ctx, structureEntity, { ...spacefillProps, visible: false })
         StructureToDistanceRestraint.apply(this.ctx, structureEntity, { ...distanceRestraintProps, visible: false })
-        StructureToBackbone.apply(this.ctx, structureEntity, { ...backboneProps, visible: true })
+        StructureToBackbone.apply(this.ctx, structureEntity, { ...backboneProps, visible: false })
         StructureToCartoon.apply(this.ctx, structureEntity, { ...cartoonProps, visible: true })
         StructureCenter.apply(this.ctx, structureEntity)