label.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. /**
  2. * Copyright (c) 2018-2020 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 { Vec3 } from '../mol-math/linear-algebra';
  13. import { radToDeg } from '../mol-math/misc';
  14. export type LabelGranularity = 'element' | 'conformation' | 'residue' | 'chain' | 'structure'
  15. export const DefaultLabelOptions = {
  16. granularity: 'element' as LabelGranularity,
  17. condensed: false,
  18. reverse: false,
  19. countsOnly: false,
  20. hidePrefix: false,
  21. htmlStyling: true,
  22. }
  23. export type LabelOptions = typeof DefaultLabelOptions
  24. export function lociLabel(loci: Loci, options: Partial<LabelOptions> = {}): string {
  25. switch (loci.kind) {
  26. case 'structure-loci':
  27. return loci.structure.models.map(m => m.entry).join(', ')
  28. case 'element-loci':
  29. return structureElementStatsLabel(StructureElement.Stats.ofLoci(loci), options)
  30. case 'bond-loci':
  31. const bond = loci.bonds[0]
  32. return bond ? bondLabel(bond) : ''
  33. case 'shape-loci':
  34. return loci.shape.name
  35. case 'group-loci':
  36. const g = loci.groups[0]
  37. return g ? loci.shape.getLabel(OrderedSet.start(g.ids), g.instance) : ''
  38. case 'every-loci':
  39. return 'Everything'
  40. case 'empty-loci':
  41. return 'Nothing'
  42. case 'data-loci':
  43. return loci.getLabel()
  44. }
  45. }
  46. function countLabel(count: number, label: string) {
  47. return count === 1 ? `1 ${label}` : `${count} ${label}s`
  48. }
  49. function otherLabel(count: number, location: StructureElement.Location, granularity: LabelGranularity, hidePrefix: boolean, reverse: boolean, condensed: boolean) {
  50. return `${elementLabel(location, { granularity, hidePrefix, reverse })} <small>[+ ${countLabel(count - 1, `other ${capitalize(granularity)}`)}]</small>`
  51. }
  52. /** Gets residue count of the model chain segments the unit is a subset of */
  53. function getResidueCount(unit: Unit.Atomic) {
  54. const { elements, model } = unit
  55. const { chainAtomSegments, residueAtomSegments } = model.atomicHierarchy
  56. const elementStart = chainAtomSegments.offsets[chainAtomSegments.index[elements[0]]]
  57. const elementEnd = chainAtomSegments.offsets[chainAtomSegments.index[elements[elements.length - 1]] + 1]
  58. return residueAtomSegments.index[elementEnd] - residueAtomSegments.index[elementStart]
  59. }
  60. export function structureElementStatsLabel(stats: StructureElement.Stats, options: Partial<LabelOptions> = {}): string {
  61. const o = { ...DefaultLabelOptions, ...options }
  62. const label = _structureElementStatsLabel(stats, o.countsOnly, o.hidePrefix, o.condensed, o.reverse)
  63. return o.htmlStyling ? label : stripTags(label)
  64. }
  65. function _structureElementStatsLabel(stats: StructureElement.Stats, countsOnly = false, hidePrefix = false, condensed = false, reverse = false): string {
  66. const { structureCount, chainCount, residueCount, conformationCount, elementCount } = stats
  67. if (!countsOnly && elementCount === 1 && residueCount === 0 && chainCount === 0) {
  68. return elementLabel(stats.firstElementLoc, { hidePrefix, condensed, granularity: 'element', reverse })
  69. } else if (!countsOnly && elementCount === 0 && residueCount === 1 && chainCount === 0) {
  70. return elementLabel(stats.firstResidueLoc, { hidePrefix, condensed, granularity: 'residue', reverse })
  71. } else if (!countsOnly && elementCount === 0 && residueCount === 0 && chainCount === 1) {
  72. const { unit } = stats.firstChainLoc
  73. const granularity = (Unit.isAtomic(unit) && getResidueCount(unit) === 1) ? 'residue' : 'chain'
  74. return elementLabel(stats.firstChainLoc, { hidePrefix, condensed, granularity, reverse })
  75. } else if (!countsOnly) {
  76. const label: string[] = []
  77. if (structureCount > 0) {
  78. label.push(structureCount === 1 ? elementLabel(stats.firstStructureLoc, { hidePrefix, condensed, granularity: 'structure', reverse }) : otherLabel(structureCount, stats.firstStructureLoc, 'structure', false, reverse, condensed))
  79. }
  80. if (chainCount > 0) {
  81. label.push(chainCount === 1 ? elementLabel(stats.firstChainLoc, { condensed, granularity: 'chain', reverse }) : otherLabel(chainCount, stats.firstChainLoc, 'chain', false, reverse, condensed))
  82. hidePrefix = true;
  83. }
  84. if (residueCount > 0) {
  85. label.push(residueCount === 1 ? elementLabel(stats.firstResidueLoc, { condensed, granularity: 'residue', hidePrefix, reverse }) : otherLabel(residueCount, stats.firstResidueLoc, 'residue', hidePrefix, reverse, condensed))
  86. hidePrefix = true;
  87. }
  88. if (conformationCount > 0) {
  89. label.push(conformationCount === 1 ? elementLabel(stats.firstConformationLoc, { condensed, granularity: 'conformation', hidePrefix, reverse }) : otherLabel(conformationCount, stats.firstConformationLoc, 'conformation', hidePrefix, reverse, condensed))
  90. hidePrefix = true;
  91. }
  92. if (elementCount > 0) {
  93. label.push(elementCount === 1 ? elementLabel(stats.firstElementLoc, { condensed, granularity: 'element', hidePrefix, reverse }) : otherLabel(elementCount, stats.firstElementLoc, 'element', hidePrefix, reverse, condensed))
  94. }
  95. return label.join('<small> + </small>')
  96. } else {
  97. const label: string[] = []
  98. if (structureCount > 0) label.push(countLabel(structureCount, 'Structure'))
  99. if (chainCount > 0) label.push(countLabel(chainCount, 'Chain'))
  100. if (residueCount > 0) label.push(countLabel(residueCount, 'Residue'))
  101. if (conformationCount > 0) label.push(countLabel(conformationCount, 'Conformation'))
  102. if (elementCount > 0) label.push(countLabel(elementCount, 'Element'))
  103. return label.join('<small> + </small>')
  104. }
  105. }
  106. export function bondLabel(bond: Bond.Location, options: Partial<LabelOptions> = {}): string {
  107. return bundleLabel({ loci: [
  108. StructureElement.Loci(bond.aStructure, [{ unit: bond.aUnit, indices: OrderedSet.ofSingleton(bond.aIndex) }]),
  109. StructureElement.Loci(bond.bStructure, [{ unit: bond.bUnit, indices: OrderedSet.ofSingleton(bond.bIndex) }])
  110. ]}, options)
  111. }
  112. export function bundleLabel(bundle: Loci.Bundle<any>, options: Partial<LabelOptions> = {}): string {
  113. const o = { ...DefaultLabelOptions, ...options }
  114. const label = _bundleLabel(bundle, o)
  115. return o.htmlStyling ? label : stripTags(label)
  116. }
  117. export function _bundleLabel(bundle: Loci.Bundle<any>, options: LabelOptions) {
  118. const { granularity, hidePrefix, reverse, condensed } = options
  119. let isSingleElements = true
  120. for (const l of bundle.loci) {
  121. if (!StructureElement.Loci.is(l) || StructureElement.Loci.size(l) !== 1) {
  122. isSingleElements = false
  123. break
  124. }
  125. }
  126. if (isSingleElements) {
  127. const locations = (bundle.loci as StructureElement.Loci[]).map(l => {
  128. const { unit, indices } = l.elements[0]
  129. return StructureElement.Location.create(l.structure, unit, unit.elements[OrderedSet.start(indices)])
  130. })
  131. const labels = locations.map(l => _elementLabel(l, granularity, hidePrefix, reverse || condensed))
  132. if (condensed) {
  133. return labels.map(l => l[0].replace(/\[.*\]/g, '').trim()).join(' \u2014 ')
  134. }
  135. let offset = 0
  136. for (let i = 0, il = Math.min(...labels.map(l => l.length)); i < il; ++i) {
  137. let areIdentical = true
  138. for (let j = 1, jl = labels.length; j < jl; ++j) {
  139. if (labels[0][i] !== labels[j][i]) {
  140. areIdentical = false
  141. break
  142. }
  143. }
  144. if (areIdentical) offset += 1
  145. else break
  146. }
  147. if (offset > 0) {
  148. const offsetLabels = [labels[0].join(' | ')]
  149. for (let j = 1, jl = labels.length; j < jl; ++j) {
  150. offsetLabels.push(labels[j].slice(offset).join(' | '))
  151. }
  152. return offsetLabels.join(' \u2014 ')
  153. } else {
  154. return labels.map(l => l.join(' | ')).join('</br>')
  155. }
  156. } else {
  157. const labels = bundle.loci.map(l => lociLabel(l, options))
  158. return labels.join(condensed ? ' \u2014 ' : '</br>')
  159. }
  160. }
  161. export function elementLabel(location: StructureElement.Location, options: Partial<LabelOptions> = {}): string {
  162. const o = { ...DefaultLabelOptions, ...options }
  163. const _label = _elementLabel(location, o.granularity, o.hidePrefix, o.reverse || o.condensed)
  164. const label = o.condensed ? _label[0].replace(/\[.*\]/g, '').trim() : _label.join(' | ')
  165. return o.htmlStyling ? label : stripTags(label)
  166. }
  167. function _elementLabel(location: StructureElement.Location, granularity: LabelGranularity = 'element', hidePrefix = false, reverse = false): string[] {
  168. const label: string[] = [];
  169. if (!hidePrefix) {
  170. let entry = location.unit.model.entry;
  171. if (entry.length > 30) entry = entry.substr(0, 27) + '\u2026'; // ellipsis
  172. label.push(`<small>${entry}</small>`) // entry
  173. if (granularity !== 'structure') {
  174. label.push(`<small>Model ${location.unit.model.modelNum}</small>`) // model
  175. label.push(`<small>Instance ${location.unit.conformation.operator.name}</small>`) // instance
  176. }
  177. }
  178. if (Unit.isAtomic(location.unit)) {
  179. label.push(..._atomicElementLabel(location as StructureElement.Location<Unit.Atomic>, granularity))
  180. } else if (Unit.isCoarse(location.unit)) {
  181. label.push(..._coarseElementLabel(location as StructureElement.Location<Unit.Spheres | Unit.Gaussians>, granularity))
  182. } else {
  183. label.push('Unknown')
  184. }
  185. return reverse ? label.reverse() : label
  186. }
  187. function _atomicElementLabel(location: StructureElement.Location<Unit.Atomic>, granularity: LabelGranularity): string[] {
  188. const rI = StructureElement.Location.residueIndex(location);
  189. const label_asym_id = Props.chain.label_asym_id(location)
  190. const auth_asym_id = Props.chain.auth_asym_id(location)
  191. const has_label_seq_id = location.unit.model.atomicHierarchy.residues.label_seq_id.valueKind(rI) === Column.ValueKind.Present;
  192. const label_seq_id = Props.residue.label_seq_id(location)
  193. const auth_seq_id = Props.residue.auth_seq_id(location)
  194. const ins_code = Props.residue.pdbx_PDB_ins_code(location)
  195. const comp_id = Props.residue.label_comp_id(location)
  196. const atom_id = Props.atom.label_atom_id(location)
  197. const alt_id = Props.atom.label_alt_id(location)
  198. const occupancy = Props.atom.occupancy(location);
  199. const microHetCompIds = Props.residue.microheterogeneityCompIds(location)
  200. const compId = granularity === 'residue' && microHetCompIds.length > 1 ?
  201. `(${microHetCompIds.join('|')})` : comp_id
  202. const label: string[] = []
  203. switch (granularity) {
  204. case 'element':
  205. label.push(`<b>${atom_id}</b>${alt_id ? `%${alt_id}` : ''}`)
  206. case 'conformation':
  207. if (granularity === 'conformation' && alt_id) {
  208. label.push(`<small>Conformation</small> <b>${alt_id}</b>`)
  209. }
  210. case 'residue':
  211. 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>`)
  212. case 'chain':
  213. if (label_asym_id === auth_asym_id) {
  214. label.push(`<b>${label_asym_id}</b>`)
  215. } else {
  216. if (granularity === 'chain' && Unit.Traits.is(location.unit.traits, Unit.Trait.MultiChain)) {
  217. label.push(`<small>[auth</small> <b>${auth_asym_id}</b><small>]</small>`)
  218. } else {
  219. label.push(`<b>${label_asym_id}</b> <small>[auth</small> <b>${auth_asym_id}</b><small>]</small>`)
  220. }
  221. }
  222. }
  223. if (label.length > 0 && occupancy !== 1) {
  224. label[0] = `${label[0]} <small>[occupancy</small> <b>${Math.round(100 * occupancy) / 100}</b><small>]</small>`;
  225. }
  226. return label.reverse()
  227. }
  228. function _coarseElementLabel(location: StructureElement.Location<Unit.Spheres | Unit.Gaussians>, granularity: LabelGranularity): string[] {
  229. const asym_id = Props.coarse.asym_id(location)
  230. const seq_id_begin = Props.coarse.seq_id_begin(location)
  231. const seq_id_end = Props.coarse.seq_id_end(location)
  232. const label: string[] = []
  233. switch (granularity) {
  234. case 'element':
  235. case 'conformation':
  236. case 'residue':
  237. if (seq_id_begin === seq_id_end) {
  238. const entityIndex = Props.coarse.entityKey(location)
  239. const seq = location.unit.model.sequence.byEntityKey[entityIndex]
  240. const comp_id = seq.sequence.compId.value(seq_id_begin - 1) // 1-indexed
  241. label.push(`<b>${comp_id} ${seq_id_begin}-${seq_id_end}</b>`)
  242. } else {
  243. label.push(`<b>${seq_id_begin}-${seq_id_end}</b>`)
  244. }
  245. case 'chain':
  246. label.push(`<b>${asym_id}</b>`)
  247. }
  248. return label.reverse()
  249. }
  250. //
  251. export function distanceLabel(pair: Loci.Bundle<2>, options: Partial<LabelOptions & { measureOnly: boolean, unitLabel: string }> = {}) {
  252. const o = { ...DefaultLabelOptions, measureOnly: false, unitLabel: '\u212B', ...options }
  253. const [cA, cB] = pair.loci.map(l => Loci.getCenter(l)!)
  254. const distance = `${Vec3.distance(cA, cB).toFixed(2)} ${o.unitLabel}`
  255. if (o.measureOnly) return distance
  256. const label = bundleLabel(pair, o)
  257. return o.condensed ? `${distance} | ${label}` : `Distance ${distance}</br>${label}`
  258. }
  259. export function angleLabel(triple: Loci.Bundle<3>, options: Partial<LabelOptions & { measureOnly: boolean }> = {}) {
  260. const o = { ...DefaultLabelOptions, measureOnly: false, ...options }
  261. const [cA, cB, cC] = triple.loci.map(l => Loci.getCenter(l)!)
  262. const vAB = Vec3.sub(Vec3(), cA, cB)
  263. const vCB = Vec3.sub(Vec3(), cC, cB)
  264. const angle = `${radToDeg(Vec3.angle(vAB, vCB)).toFixed(2)}\u00B0`
  265. if (o.measureOnly) return angle
  266. const label = bundleLabel(triple, o)
  267. return o.condensed ? `${angle} | ${label}` : `Angle ${angle}</br>${label}`
  268. }
  269. export function dihedralLabel(quad: Loci.Bundle<4>, options: Partial<LabelOptions & { measureOnly: boolean }> = {}) {
  270. const o = { ...DefaultLabelOptions, measureOnly: false, ...options }
  271. const [cA, cB, cC, cD] = quad.loci.map(l => Loci.getCenter(l)!)
  272. const dihedral = `${radToDeg(Vec3.dihedralAngle(cA, cB, cC, cD)).toFixed(2)}\u00B0`
  273. if (o.measureOnly) return dihedral
  274. const label = bundleLabel(quad, o)
  275. return o.condensed ? `${dihedral} | ${label}` : `Dihedral ${dihedral}</br>${label}`
  276. }