label.ts 17 KB

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