label.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. /**
  2. * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. * @author David Sehnal <david.sehnal@gmail.com>
  6. */
  7. import { Unit, StructureElement, StructureProperties as Props, Bond } from '../mol-model/structure';
  8. import { Loci } from '../mol-model/loci';
  9. import { OrderedSet } from '../mol-data/int';
  10. import { capitalize, stripTags } from '../mol-util/string';
  11. import { Column } from '../mol-data/db';
  12. import { Interactions } from '../mol-model-props/computed/interactions/interactions';
  13. import { interactionTypeLabel } from '../mol-model-props/computed/interactions/common';
  14. export type LabelGranularity = 'element' | 'conformation' | 'residue' | 'chain' | 'structure'
  15. export const DefaultLabelOptions = {
  16. granularity: 'element' as LabelGranularity,
  17. countsOnly: false,
  18. hidePrefix: false,
  19. htmlStyling: true,
  20. }
  21. export type LabelOptions = typeof DefaultLabelOptions
  22. export function lociLabel(loci: Loci, options: Partial<LabelOptions> = {}): string {
  23. switch (loci.kind) {
  24. case 'structure-loci':
  25. return loci.structure.models.map(m => m.entry).join(', ')
  26. case 'element-loci':
  27. return structureElementStatsLabel(StructureElement.Stats.ofLoci(loci), options)
  28. case 'bond-loci':
  29. const bond = loci.bonds[0]
  30. return bond ? bondLabel(bond) : 'Unknown'
  31. case 'interaction-loci':
  32. const link = loci.links[0]
  33. return link ? interactionLabel(Interactions.Location(loci.interactions, link.unitA, link.indexA, link.unitB, link.indexB)) : 'Unknown'
  34. case 'shape-loci':
  35. return loci.shape.name
  36. case 'group-loci':
  37. const g = loci.groups[0]
  38. return g ? loci.shape.getLabel(OrderedSet.start(g.ids), loci.instance) : 'Unknown'
  39. case 'every-loci':
  40. return 'Everything'
  41. case 'empty-loci':
  42. return 'Nothing'
  43. case 'data-loci':
  44. return ''
  45. }
  46. }
  47. function countLabel(count: number, label: string) {
  48. return count === 1 ? `1 ${label}` : `${count} ${label}s`
  49. }
  50. function otherLabel(count: number, location: StructureElement.Location, granularity: LabelGranularity, hidePrefix: boolean) {
  51. return `${elementLabel(location, { granularity, hidePrefix })} <small>[+ ${countLabel(count - 1, `other ${capitalize(granularity)}`)}]</small>`
  52. }
  53. /** Gets residue count of the model chain segments the unit is a subset of */
  54. function getResidueCount(unit: Unit.Atomic) {
  55. const { elements, model } = unit
  56. const { chainAtomSegments, residueAtomSegments } = model.atomicHierarchy
  57. const elementStart = chainAtomSegments.offsets[chainAtomSegments.index[elements[0]]]
  58. const elementEnd = chainAtomSegments.offsets[chainAtomSegments.index[elements[elements.length - 1]] + 1]
  59. return residueAtomSegments.index[elementEnd] - residueAtomSegments.index[elementStart]
  60. }
  61. export function structureElementStatsLabel(stats: StructureElement.Stats, options: Partial<LabelOptions> = {}): string {
  62. const o = { ...DefaultLabelOptions, ...options }
  63. const label = _structureElementStatsLabel(stats, o.countsOnly, o.hidePrefix)
  64. return o.htmlStyling ? label : stripTags(label)
  65. }
  66. function _structureElementStatsLabel(stats: StructureElement.Stats, countsOnly = false, hidePrefix = false): string {
  67. const { structureCount, chainCount, residueCount, conformationCount, elementCount } = stats
  68. if (!countsOnly && elementCount === 1 && residueCount === 0 && chainCount === 0) {
  69. return elementLabel(stats.firstElementLoc, { hidePrefix, granularity: 'element' })
  70. } else if (!countsOnly && elementCount === 0 && residueCount === 1 && chainCount === 0) {
  71. return elementLabel(stats.firstResidueLoc, { hidePrefix, granularity: 'residue' })
  72. } else if (!countsOnly && elementCount === 0 && residueCount === 0 && chainCount === 1) {
  73. const { unit } = stats.firstChainLoc
  74. const granularity = (Unit.isAtomic(unit) && getResidueCount(unit) === 1) ? 'residue' : 'chain'
  75. return elementLabel(stats.firstChainLoc, { hidePrefix, granularity })
  76. } else if (!countsOnly) {
  77. const label: string[] = []
  78. if (structureCount > 0) {
  79. label.push(structureCount === 1 ? elementLabel(stats.firstStructureLoc, { hidePrefix, granularity: 'structure' }) : otherLabel(structureCount, stats.firstStructureLoc, 'structure', false))
  80. }
  81. if (chainCount > 0) {
  82. label.push(chainCount === 1 ? elementLabel(stats.firstChainLoc, { granularity: 'chain' }) : otherLabel(chainCount, stats.firstChainLoc, 'chain', false))
  83. hidePrefix = true;
  84. }
  85. if (residueCount > 0) {
  86. label.push(residueCount === 1 ? elementLabel(stats.firstResidueLoc, { granularity: 'residue', hidePrefix }) : otherLabel(residueCount, stats.firstResidueLoc, 'residue', hidePrefix))
  87. hidePrefix = true;
  88. }
  89. if (conformationCount > 0) {
  90. label.push(conformationCount === 1 ? elementLabel(stats.firstConformationLoc, { granularity: 'conformation', hidePrefix }) : otherLabel(conformationCount, stats.firstConformationLoc, 'conformation', hidePrefix))
  91. hidePrefix = true;
  92. }
  93. if (elementCount > 0) {
  94. label.push(elementCount === 1 ? elementLabel(stats.firstElementLoc, { granularity: 'element', hidePrefix }) : otherLabel(elementCount, stats.firstElementLoc, 'element', hidePrefix))
  95. }
  96. return label.join('<small> + </small>')
  97. } else {
  98. const label: string[] = []
  99. if (structureCount > 0) label.push(countLabel(structureCount, 'Structure'))
  100. if (chainCount > 0) label.push(countLabel(chainCount, 'Chain'))
  101. if (residueCount > 0) label.push(countLabel(residueCount, 'Residue'))
  102. if (conformationCount > 0) label.push(countLabel(conformationCount, 'Conformation'))
  103. if (elementCount > 0) label.push(countLabel(elementCount, 'Element'))
  104. return label.join('<small> + </small>')
  105. }
  106. }
  107. export function bondLabel(bond: Bond.Location): string {
  108. const locA = StructureElement.Location.create(bond.aUnit, bond.aUnit.elements[bond.aIndex])
  109. const locB = StructureElement.Location.create(bond.bUnit, bond.bUnit.elements[bond.bIndex])
  110. const labelA = _elementLabel(locA)
  111. const labelB = _elementLabel(locB)
  112. let offset = 0
  113. for (let i = 0, il = Math.min(labelA.length, labelB.length); i < il; ++i) {
  114. if (labelA[i] === labelB[i]) offset += 1
  115. else break
  116. }
  117. return `${labelA.join(' | ')} \u2014 ${labelB.slice(offset).join(' | ')}`
  118. }
  119. export function interactionLabel(location: Interactions.Location): string {
  120. const { interactions, unitA, indexA, unitB, indexB } = location
  121. if (location.unitA === location.unitB) {
  122. const links = interactions.unitsLinks.get(location.unitA.id)
  123. const idx = links.getDirectedEdgeIndex(location.indexA, location.indexB)
  124. return interactionTypeLabel(links.edgeProps.type[idx])
  125. } else {
  126. const idx = interactions.links.getEdgeIndex(indexA, unitA, indexB, unitB)
  127. return interactionTypeLabel(interactions.links.edges[idx].props.type)
  128. }
  129. }
  130. export function elementLabel(location: StructureElement.Location, options: Partial<LabelOptions> = {}): string {
  131. const o = { ...DefaultLabelOptions, ...options }
  132. const label = _elementLabel(location, o.granularity, o.hidePrefix).join(' | ')
  133. return o.htmlStyling ? label : stripTags(label)
  134. }
  135. function _elementLabel(location: StructureElement.Location, granularity: LabelGranularity = 'element', hidePrefix = false): string[] {
  136. const label: string[] = [];
  137. if (!hidePrefix) {
  138. label.push(`<small>${location.unit.model.entry}</small>`) // entry
  139. if (granularity !== 'structure') {
  140. label.push(`<small>Model ${location.unit.model.modelNum}</small>`) // model
  141. label.push(`<small>Instance ${location.unit.conformation.operator.name}</small>`) // instance
  142. }
  143. }
  144. if (Unit.isAtomic(location.unit)) {
  145. label.push(..._atomicElementLabel(location as StructureElement.Location<Unit.Atomic>, granularity))
  146. } else if (Unit.isCoarse(location.unit)) {
  147. label.push(..._coarseElementLabel(location as StructureElement.Location<Unit.Spheres | Unit.Gaussians>, granularity))
  148. } else {
  149. label.push('Unknown')
  150. }
  151. return label
  152. }
  153. function _atomicElementLabel(location: StructureElement.Location<Unit.Atomic>, granularity: LabelGranularity): string[] {
  154. const label_asym_id = Props.chain.label_asym_id(location)
  155. const auth_asym_id = Props.chain.auth_asym_id(location)
  156. const has_label_seq_id = location.unit.model.atomicHierarchy.residues.label_seq_id.valueKind(location.element) === Column.ValueKind.Present;
  157. const label_seq_id = Props.residue.label_seq_id(location)
  158. const auth_seq_id = Props.residue.auth_seq_id(location)
  159. const ins_code = Props.residue.pdbx_PDB_ins_code(location)
  160. const comp_id = Props.residue.label_comp_id(location)
  161. const atom_id = Props.atom.label_atom_id(location)
  162. const alt_id = Props.atom.label_alt_id(location)
  163. const microHetCompIds = Props.residue.microheterogeneityCompIds(location)
  164. const compId = granularity === 'residue' && microHetCompIds.length > 1 ?
  165. `(${microHetCompIds.join('|')})` : comp_id
  166. const label: string[] = []
  167. switch (granularity) {
  168. case 'element':
  169. label.push(`<b>${atom_id}</b>${alt_id ? `%${alt_id}` : ''}`)
  170. case 'conformation':
  171. if (granularity === 'conformation' && alt_id) {
  172. label.push(`<small>Conformation</small> <b>${alt_id}</b>`)
  173. }
  174. case 'residue':
  175. label.push(`<b>${compId}${has_label_seq_id ? ` ${label_seq_id}` : ''}</b>${label_seq_id !== auth_seq_id ? ` <small>[auth</small> <b>${auth_seq_id}</b><small>]</small>` : ''}<b>${ins_code ? ins_code : ''}</b>`)
  176. case 'chain':
  177. if (label_asym_id === auth_asym_id) {
  178. label.push(`<b>${label_asym_id}</b>`)
  179. } else {
  180. if (granularity === 'chain' && Unit.Traits.is(location.unit.traits, Unit.Trait.MultiChain)) {
  181. label.push(`<small>[auth</small> <b>${auth_asym_id}</b><small>]</small>`)
  182. } else {
  183. label.push(`<b>${label_asym_id}</b> <small>[auth</small> <b>${auth_asym_id}</b><small>]</small>`)
  184. }
  185. }
  186. }
  187. return label.reverse()
  188. }
  189. function _coarseElementLabel(location: StructureElement.Location<Unit.Spheres | Unit.Gaussians>, granularity: LabelGranularity): string[] {
  190. const asym_id = Props.coarse.asym_id(location)
  191. const seq_id_begin = Props.coarse.seq_id_begin(location)
  192. const seq_id_end = Props.coarse.seq_id_end(location)
  193. const label: string[] = []
  194. switch (granularity) {
  195. case 'element':
  196. case 'conformation':
  197. case 'residue':
  198. if (seq_id_begin === seq_id_end) {
  199. const entityIndex = Props.coarse.entityKey(location)
  200. const seq = location.unit.model.sequence.byEntityKey[entityIndex]
  201. const comp_id = seq.sequence.compId.value(seq_id_begin - 1) // 1-indexed
  202. label.push(`<b>${comp_id} ${seq_id_begin}-${seq_id_end}</b>`)
  203. } else {
  204. label.push(`<b>${seq_id_begin}-${seq_id_end}</b>`)
  205. }
  206. case 'chain':
  207. label.push(`<b>${asym_id}</b>`)
  208. }
  209. return label.reverse()
  210. }