label.ts 10 KB

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