1 //
2 // Copyright (C) 2014-2020 David Cosgrove and Greg Landrum
3 //
4 // @@ All Rights Reserved @@
5 // This file is part of the RDKit.
6 // The contents are covered by the terms of the BSD license
7 // which is included in the file license.txt, found at the root
8 // of the RDKit source tree.
9 //
10 // Original author: David Cosgrove (AstraZeneca)
11 // 27th May 2014
12 //
13 // Extensively modified by Greg Landrum
14 //
15
16 #include <GraphMol/QueryOps.h>
17 #include <GraphMol/MolDraw2D/DrawText.h>
18 #include <GraphMol/MolDraw2D/MolDraw2D.h>
19 #include <GraphMol/MolDraw2D/MolDraw2DDetails.h>
20 #include <GraphMol/MolDraw2D/MolDraw2DUtils.h>
21 #include <GraphMol/ChemReactions/ReactionParser.h>
22 #include <GraphMol/FileParsers/MolSGroupParsing.h>
23 #include <GraphMol/Depictor/RDDepictor.h>
24 #include <Geometry/point.h>
25 #include <Geometry/Transform2D.h>
26 #include <Numerics/SquareMatrix.h>
27 #include <Numerics/Matrix.h>
28
29 #include <GraphMol/MolTransforms/MolTransforms.h>
30 #include <GraphMol/FileParsers/FileParserUtils.h>
31 #include <GraphMol/MolEnumerator/LinkNode.h>
32
33 #include <Geometry/Transform3D.h>
34
35 #include <algorithm>
36 #include <cstdlib>
37 #include <cmath>
38 #include <limits>
39 #include <memory>
40
41 #include <boost/lexical_cast.hpp>
42 #include <boost/tuple/tuple_comparison.hpp>
43 #include <boost/assign/list_of.hpp>
44 #include <boost/format.hpp>
45
46 using namespace boost;
47 using namespace std;
48
49 namespace RDKit {
50
51 namespace {
52 // ****************************************************************************
53 // calculate normalised perpendicular to vector between two coords
calcPerpendicular(const Point2D & cds1,const Point2D & cds2)54 Point2D calcPerpendicular(const Point2D &cds1, const Point2D &cds2) {
55 double bv[2] = {cds1.x - cds2.x, cds1.y - cds2.y};
56 double perp[2] = {-bv[1], bv[0]};
57 double perp_len = sqrt(perp[0] * perp[0] + perp[1] * perp[1]);
58 perp[0] /= perp_len;
59 perp[1] /= perp_len;
60
61 return Point2D(perp[0], perp[1]);
62 }
63
64 // ****************************************************************************
65 // calculate normalised perpendicular to vector between two coords, such that
66 // it's inside the angle made between (1 and 2) and (2 and 3).
calcInnerPerpendicular(const Point2D & cds1,const Point2D & cds2,const Point2D & cds3)67 Point2D calcInnerPerpendicular(const Point2D &cds1, const Point2D &cds2,
68 const Point2D &cds3) {
69 Point2D perp = calcPerpendicular(cds1, cds2);
70 double v1[2] = {cds1.x - cds2.x, cds1.y - cds2.y};
71 double v2[2] = {cds2.x - cds3.x, cds2.y - cds3.y};
72 double obv[2] = {v1[0] - v2[0], v1[1] - v2[1]};
73
74 // if dot product of centre_dir and perp < 0.0, they're pointing in opposite
75 // directions, so reverse perp
76 if (obv[0] * perp.x + obv[1] * perp.y < 0.0) {
77 perp.x *= -1.0;
78 perp.y *= -1.0;
79 }
80
81 return perp;
82 }
83
84 // ****************************************************************************
85 // cds1 and cds2 are 2 atoms in a ring. Returns the perpendicular pointing
86 // into the ring
bondInsideRing(const ROMol & mol,const Bond & bond,const Point2D & cds1,const Point2D & cds2,const std::vector<Point2D> & at_cds)87 Point2D bondInsideRing(const ROMol &mol, const Bond &bond, const Point2D &cds1,
88 const Point2D &cds2,
89 const std::vector<Point2D> &at_cds) {
90 vector<size_t> bond_in_rings;
91 auto bond_rings = mol.getRingInfo()->bondRings();
92 for (size_t i = 0; i < bond_rings.size(); ++i) {
93 if (find(bond_rings[i].begin(), bond_rings[i].end(), bond.getIdx()) !=
94 bond_rings[i].end()) {
95 bond_in_rings.push_back(i);
96 }
97 }
98
99 // find another bond in the ring connected to bond, use the
100 // other end of it as the 3rd atom.
101 auto calc_perp = [&](const Bond *bond, const INT_VECT &ring) -> Point2D * {
102 Atom *bgn_atom = bond->getBeginAtom();
103 for (const auto &nbri2 : make_iterator_range(mol.getAtomBonds(bgn_atom))) {
104 const Bond *bond2 = mol[nbri2];
105 if (bond2 == bond) {
106 continue;
107 }
108 if (find(ring.begin(), ring.end(), bond2->getIdx()) != ring.end()) {
109 int atom3 = bond2->getOtherAtomIdx(bond->getBeginAtomIdx());
110 Point2D *ret = new Point2D;
111 *ret = calcInnerPerpendicular(cds1, cds2, at_cds[atom3]);
112 return ret;
113 }
114 }
115 return nullptr;
116 };
117
118 if (bond_in_rings.size() > 1) {
119 // bond is in more than 1 ring. Choose one that is the same aromaticity
120 // as the bond, so that if bond is aromatic, the double bond is inside
121 // the aromatic ring. This is important for morphine, for example,
122 // where there are fused aromatic and aliphatic rings.
123 // morphine: CN1CC[C@]23c4c5ccc(O)c4O[C@H]2[C@@H](O)C=C[C@H]3[C@H]1C5
124 for (size_t i = 0; i < bond_in_rings.size(); ++i) {
125 auto ring = bond_rings[bond_in_rings[i]];
126 bool ring_ok = true;
127 for (auto bond_idx : ring) {
128 const Bond *bond2 = mol.getBondWithIdx(bond_idx);
129 if (bond.getIsAromatic() != bond2->getIsAromatic()) {
130 ring_ok = false;
131 break;
132 }
133 }
134 if (!ring_ok) {
135 continue;
136 }
137 Point2D *ret = calc_perp(&bond, ring);
138 if (ret) {
139 Point2D real_ret(*ret);
140 delete ret;
141 return real_ret;
142 }
143 }
144 }
145
146 // either bond is in 1 ring, or we couldn't decide above, so just use the
147 // first one
148 auto ring = bond_rings[bond_in_rings.front()];
149 Point2D *ret = calc_perp(&bond, ring);
150 if (ret) {
151 Point2D real_ret(*ret);
152 delete ret;
153 return real_ret;
154 }
155
156 // failsafe that it will hopefully never see.
157 return calcPerpendicular(cds1, cds2);
158 }
159
160 // ****************************************************************************
isLinearAtom(const Atom & atom,const std::vector<Point2D> & at_cds)161 bool isLinearAtom(const Atom &atom, const std::vector<Point2D> &at_cds) {
162 if (atom.getDegree() == 2) {
163 Point2D bond_vecs[2];
164 Bond::BondType bts[2];
165 Point2D const &at1_cds = at_cds[atom.getIdx()];
166 ROMol const &mol = atom.getOwningMol();
167 int i = 0;
168 for (const auto &nbr : make_iterator_range(mol.getAtomNeighbors(&atom))) {
169 Point2D bond_vec = at1_cds.directionVector(at_cds[nbr]);
170 bond_vec.normalize();
171 bond_vecs[i] = bond_vec;
172 bts[i] = mol.getBondBetweenAtoms(atom.getIdx(), nbr)->getBondType();
173 ++i;
174 }
175 return (bts[0] == bts[1] && bond_vecs[0].dotProduct(bond_vecs[1]) < -0.95);
176 }
177 return false;
178 }
179
180 // ****************************************************************************
181 // cds1 and cds2 are 2 atoms in a chain double bond. Returns the
182 // perpendicular pointing into the inside of the bond
bondInsideDoubleBond(const ROMol & mol,const Bond & bond,const std::vector<Point2D> & at_cds)183 Point2D bondInsideDoubleBond(const ROMol &mol, const Bond &bond,
184 const std::vector<Point2D> &at_cds) {
185 // a chain double bond, where it looks nicer IMO if the 2nd line is inside
186 // the angle of outgoing bond. Unless it's an allene, where nothing
187 // looks great.
188 const Atom *at1 = bond.getBeginAtom();
189 const Atom *at2 = bond.getEndAtom();
190 const Atom *bond_atom, *end_atom;
191 if (at1->getDegree() > 1) {
192 bond_atom = at1;
193 end_atom = at2;
194 } else {
195 bond_atom = at2;
196 end_atom = at1;
197 }
198 int at3 = -1; // to stop the compiler whinging.
199 for (const auto &nbri2 : make_iterator_range(mol.getAtomBonds(bond_atom))) {
200 const Bond *bond2 = mol[nbri2];
201 if (&bond != bond2) {
202 at3 = bond2->getOtherAtomIdx(bond_atom->getIdx());
203 break;
204 }
205 }
206
207 return calcInnerPerpendicular(at_cds[end_atom->getIdx()],
208 at_cds[bond_atom->getIdx()], at_cds[at3]);
209 }
210
211 // ****************************************************************************
calcDoubleBondLines(const ROMol & mol,double offset,const Bond & bond,const Point2D & at1_cds,const Point2D & at2_cds,const std::vector<Point2D> & at_cds,Point2D & l1s,Point2D & l1f,Point2D & l2s,Point2D & l2f)212 void calcDoubleBondLines(const ROMol &mol, double offset, const Bond &bond,
213 const Point2D &at1_cds, const Point2D &at2_cds,
214 const std::vector<Point2D> &at_cds, Point2D &l1s,
215 Point2D &l1f, Point2D &l2s, Point2D &l2f) {
216 // the percent shorter that the extra bonds in a double bond are
217 const double multipleBondTruncation = 0.15;
218 Atom *at1 = bond.getBeginAtom();
219 Atom *at2 = bond.getEndAtom();
220 Point2D perp;
221 if (1 == at1->getDegree() || 1 == at2->getDegree() ||
222 isLinearAtom(*at1, at_cds) || isLinearAtom(*at2, at_cds)) {
223 perp = calcPerpendicular(at1_cds, at2_cds) * offset;
224 l1s = at1_cds + perp;
225 l1f = at2_cds + perp;
226 l2s = at1_cds - perp;
227 l2f = at2_cds - perp;
228 } else if ((Bond::EITHERDOUBLE == bond.getBondDir()) ||
229 (Bond::STEREOANY == bond.getStereo())) {
230 // crossed bond
231 perp = calcPerpendicular(at1_cds, at2_cds) * offset;
232 l1s = at1_cds + perp;
233 l1f = at2_cds - perp;
234 l2s = at1_cds - perp;
235 l2f = at2_cds + perp;
236 } else {
237 l1s = at1_cds;
238 l1f = at2_cds;
239 offset *= 2.0;
240 if (mol.getRingInfo()->numBondRings(bond.getIdx())) {
241 // in a ring, we need to draw the bond inside the ring
242 perp = bondInsideRing(mol, bond, at1_cds, at2_cds, at_cds);
243 } else {
244 perp = bondInsideDoubleBond(mol, bond, at_cds);
245 }
246 Point2D bv = at1_cds - at2_cds;
247 l2s = at1_cds - bv * multipleBondTruncation + perp * offset;
248 l2f = at2_cds + bv * multipleBondTruncation + perp * offset;
249 }
250 }
251
252 // ****************************************************************************
calcTripleBondLines(double offset,const Bond & bond,const Point2D & at1_cds,const Point2D & at2_cds,Point2D & l1s,Point2D & l1f,Point2D & l2s,Point2D & l2f)253 void calcTripleBondLines(double offset, const Bond &bond,
254 const Point2D &at1_cds, const Point2D &at2_cds,
255 Point2D &l1s, Point2D &l1f, Point2D &l2s,
256 Point2D &l2f) {
257 // the percent shorter that the extra bonds in a double bond are
258 const double multipleBondTruncation = 0.15;
259
260 Atom *at1 = bond.getBeginAtom();
261 Atom *at2 = bond.getEndAtom();
262
263 // 2 lines, a bit shorter and offset on the perpendicular
264 double dbo = 2.0 * offset;
265 Point2D perp = calcPerpendicular(at1_cds, at2_cds);
266 double end1_trunc = 1 == at1->getDegree() ? 0.0 : multipleBondTruncation;
267 double end2_trunc = 1 == at2->getDegree() ? 0.0 : multipleBondTruncation;
268 Point2D bv = at1_cds - at2_cds;
269 l1s = at1_cds - (bv * end1_trunc) + perp * dbo;
270 l1f = at2_cds + (bv * end2_trunc) + perp * dbo;
271 l2s = at1_cds - (bv * end1_trunc) - perp * dbo;
272 l2f = at2_cds + (bv * end2_trunc) - perp * dbo;
273 }
274
getBondHighlightsForAtoms(const ROMol & mol,const vector<int> & highlight_atoms,vector<int> & highlight_bonds)275 void getBondHighlightsForAtoms(const ROMol &mol,
276 const vector<int> &highlight_atoms,
277 vector<int> &highlight_bonds) {
278 highlight_bonds.clear();
279 for (auto ai = highlight_atoms.begin(); ai != highlight_atoms.end(); ++ai) {
280 for (auto aj = ai + 1; aj != highlight_atoms.end(); ++aj) {
281 const Bond *bnd = mol.getBondBetweenAtoms(*ai, *aj);
282 if (bnd) {
283 highlight_bonds.push_back(bnd->getIdx());
284 }
285 }
286 }
287 }
centerMolForDrawing(RWMol & mol,int confId)288 void centerMolForDrawing(RWMol &mol, int confId) {
289 auto &conf = mol.getConformer(confId);
290 RDGeom::Transform3D tf;
291 auto centroid = MolTransforms::computeCentroid(conf);
292 centroid *= -1;
293 tf.SetTranslation(centroid);
294 MolTransforms::transformConformer(conf, tf);
295 MolTransforms::transformMolSubstanceGroups(mol, tf);
296 }
297 } // namespace
298
299 // ****************************************************************************
MolDraw2D(int width,int height,int panelWidth,int panelHeight)300 MolDraw2D::MolDraw2D(int width, int height, int panelWidth, int panelHeight)
301 : needs_scale_(true),
302 width_(width),
303 height_(height),
304 panel_width_(panelWidth > 0 ? panelWidth : width),
305 panel_height_(panelHeight > 0 ? panelHeight : height),
306 legend_height_(0),
307 scale_(1.0),
308 x_min_(0.0),
309 y_min_(0.0),
310 x_range_(0.0),
311 y_range_(0.0),
312 x_trans_(0.0),
313 y_trans_(0.0),
314 x_offset_(0),
315 y_offset_(0),
316 fill_polys_(true),
317 activeMolIdx_(-1),
318 activeAtmIdx1_(-1),
319 activeAtmIdx2_(-1) {}
320
321 // ****************************************************************************
~MolDraw2D()322 MolDraw2D::~MolDraw2D() {}
323
324 // ****************************************************************************
drawMolecule(const ROMol & mol,const vector<int> * highlight_atoms,const map<int,DrawColour> * highlight_atom_map,const std::map<int,double> * highlight_radii,int confId)325 void MolDraw2D::drawMolecule(const ROMol &mol,
326 const vector<int> *highlight_atoms,
327 const map<int, DrawColour> *highlight_atom_map,
328 const std::map<int, double> *highlight_radii,
329 int confId) {
330 drawMolecule(mol, "", highlight_atoms, highlight_atom_map, highlight_radii,
331 confId);
332 }
333
334 // ****************************************************************************
drawMolecule(const ROMol & mol,const std::string & legend,const vector<int> * highlight_atoms,const map<int,DrawColour> * highlight_atom_map,const std::map<int,double> * highlight_radii,int confId)335 void MolDraw2D::drawMolecule(const ROMol &mol, const std::string &legend,
336 const vector<int> *highlight_atoms,
337 const map<int, DrawColour> *highlight_atom_map,
338 const std::map<int, double> *highlight_radii,
339 int confId) {
340 vector<int> highlight_bonds;
341 if (highlight_atoms) {
342 getBondHighlightsForAtoms(mol, *highlight_atoms, highlight_bonds);
343 }
344 drawMolecule(mol, legend, highlight_atoms, &highlight_bonds,
345 highlight_atom_map, nullptr, highlight_radii, confId);
346 }
347
348 // ****************************************************************************
doContinuousHighlighting(const ROMol & mol,const vector<int> * highlight_atoms,const vector<int> * highlight_bonds,const map<int,DrawColour> * highlight_atom_map,const map<int,DrawColour> * highlight_bond_map,const std::map<int,double> * highlight_radii)349 void MolDraw2D::doContinuousHighlighting(
350 const ROMol &mol, const vector<int> *highlight_atoms,
351 const vector<int> *highlight_bonds,
352 const map<int, DrawColour> *highlight_atom_map,
353 const map<int, DrawColour> *highlight_bond_map,
354 const std::map<int, double> *highlight_radii) {
355 PRECONDITION(activeMolIdx_ >= 0, "bad active mol");
356
357 int orig_lw = lineWidth();
358 int tgt_lw = getHighlightBondWidth(-1, nullptr);
359 if (tgt_lw < 2) {
360 tgt_lw = 2;
361 }
362
363 bool orig_fp = fillPolys();
364 if (highlight_bonds) {
365 for (auto this_at : mol.atoms()) {
366 int this_idx = this_at->getIdx();
367 for (const auto &nbri : make_iterator_range(mol.getAtomBonds(this_at))) {
368 const Bond *bond = mol[nbri];
369 int nbr_idx = bond->getOtherAtomIdx(this_idx);
370 if (nbr_idx < static_cast<int>(at_cds_[activeMolIdx_].size()) &&
371 nbr_idx > this_idx) {
372 if (std::find(highlight_bonds->begin(), highlight_bonds->end(),
373 bond->getIdx()) != highlight_bonds->end()) {
374 DrawColour col = drawOptions().highlightColour;
375 if (highlight_bond_map &&
376 highlight_bond_map->find(bond->getIdx()) !=
377 highlight_bond_map->end()) {
378 col = highlight_bond_map->find(bond->getIdx())->second;
379 }
380 setLineWidth(tgt_lw);
381 Point2D at1_cds = at_cds_[activeMolIdx_][this_idx];
382 Point2D at2_cds = at_cds_[activeMolIdx_][nbr_idx];
383 bool orig_slw = drawOptions().scaleBondWidth;
384 drawOptions().scaleBondWidth =
385 drawOptions().scaleHighlightBondWidth;
386 drawLine(at1_cds, at2_cds, col, col);
387 drawOptions().scaleBondWidth = orig_slw;
388 }
389 }
390 }
391 }
392 }
393 if (highlight_atoms) {
394 if (!drawOptions().fillHighlights) {
395 // we need a narrower circle
396 setLineWidth(tgt_lw / 2);
397 }
398 for (auto this_at : mol.atoms()) {
399 int this_idx = this_at->getIdx();
400 if (std::find(highlight_atoms->begin(), highlight_atoms->end(),
401 this_idx) != highlight_atoms->end()) {
402 DrawColour col = drawOptions().highlightColour;
403 if (highlight_atom_map &&
404 highlight_atom_map->find(this_idx) != highlight_atom_map->end()) {
405 col = highlight_atom_map->find(this_idx)->second;
406 }
407 vector<DrawColour> cols(1, col);
408 drawHighlightedAtom(this_idx, cols, highlight_radii);
409 }
410 }
411 }
412 setLineWidth(orig_lw);
413 setFillPolys(orig_fp);
414 }
415
416 // ****************************************************************************
drawMolecule(const ROMol & mol,const vector<int> * highlight_atoms,const vector<int> * highlight_bonds,const map<int,DrawColour> * highlight_atom_map,const map<int,DrawColour> * highlight_bond_map,const std::map<int,double> * highlight_radii,int confId)417 void MolDraw2D::drawMolecule(const ROMol &mol,
418 const vector<int> *highlight_atoms,
419 const vector<int> *highlight_bonds,
420 const map<int, DrawColour> *highlight_atom_map,
421 const map<int, DrawColour> *highlight_bond_map,
422 const std::map<int, double> *highlight_radii,
423 int confId) {
424 int origWidth = lineWidth();
425 pushDrawDetails();
426 setupTextDrawer();
427
428 unique_ptr<RWMol> rwmol =
429 setupMoleculeDraw(mol, highlight_atoms, highlight_radii, confId);
430 ROMol const &draw_mol = rwmol ? *(rwmol) : mol;
431 if (!draw_mol.getNumConformers()) {
432 // clearly, the molecule is in a sorry state.
433 return;
434 }
435
436 if (!pre_shapes_[activeMolIdx_].empty()) {
437 MolDraw2D_detail::drawShapes(*this, pre_shapes_[activeMolIdx_]);
438 }
439
440 if (drawOptions().continuousHighlight) {
441 // if we're doing continuous highlighting, start by drawing the highlights
442 doContinuousHighlighting(draw_mol, highlight_atoms, highlight_bonds,
443 highlight_atom_map, highlight_bond_map,
444 highlight_radii);
445 // at this point we shouldn't be doing any more highlighting, so blow out
446 // those variables. This alters the behaviour of drawBonds below.
447 highlight_bonds = nullptr;
448 highlight_atoms = nullptr;
449 } else if (drawOptions().circleAtoms && highlight_atoms) {
450 setFillPolys(drawOptions().fillHighlights);
451 for (auto this_at : draw_mol.atoms()) {
452 int this_idx = this_at->getIdx();
453 if (std::find(highlight_atoms->begin(), highlight_atoms->end(),
454 this_idx) != highlight_atoms->end()) {
455 if (highlight_atom_map &&
456 highlight_atom_map->find(this_idx) != highlight_atom_map->end()) {
457 setColour(highlight_atom_map->find(this_idx)->second);
458 } else {
459 setColour(drawOptions().highlightColour);
460 }
461 Point2D p1 = at_cds_[activeMolIdx_][this_idx];
462 Point2D p2 = at_cds_[activeMolIdx_][this_idx];
463 double radius = drawOptions().highlightRadius;
464 if (highlight_radii &&
465 highlight_radii->find(this_idx) != highlight_radii->end()) {
466 radius = highlight_radii->find(this_idx)->second;
467 }
468 Point2D offset(radius, radius);
469 p1 -= offset;
470 p2 += offset;
471 drawEllipse(p1, p2);
472 }
473 }
474 setFillPolys(true);
475 }
476
477 drawBonds(draw_mol, highlight_atoms, highlight_atom_map, highlight_bonds,
478 highlight_bond_map);
479
480 vector<DrawColour> atom_colours;
481 for (auto this_at : draw_mol.atoms()) {
482 atom_colours.emplace_back(
483 getColour(this_at->getIdx(), highlight_atoms, highlight_atom_map));
484 }
485
486 finishMoleculeDraw(draw_mol, atom_colours);
487 // popDrawDetails();
488 setLineWidth(origWidth);
489
490 if (drawOptions().includeMetadata) {
491 this->updateMetadata(draw_mol, confId);
492 }
493 // {
494 // Point2D p1(x_min_, y_min_), p2(x_min_ + x_range_, y_min_ + y_range_);
495 // setColour(DrawColour(0, 0, 0));
496 // setFillPolys(false);
497 // drawRect(p1, p2);
498 // }
499 }
500
501 // ****************************************************************************
drawMolecule(const ROMol & mol,const std::string & legend,const vector<int> * highlight_atoms,const vector<int> * highlight_bonds,const map<int,DrawColour> * highlight_atom_map,const map<int,DrawColour> * highlight_bond_map,const std::map<int,double> * highlight_radii,int confId)502 void MolDraw2D::drawMolecule(const ROMol &mol, const std::string &legend,
503 const vector<int> *highlight_atoms,
504 const vector<int> *highlight_bonds,
505 const map<int, DrawColour> *highlight_atom_map,
506 const map<int, DrawColour> *highlight_bond_map,
507 const std::map<int, double> *highlight_radii,
508 int confId) {
509 if (!legend.empty()) {
510 legend_height_ = int(0.05 * double(panelHeight()));
511 if (legend_height_ < 20) {
512 legend_height_ = 20;
513 }
514 } else {
515 legend_height_ = 0;
516 }
517 drawMolecule(mol, highlight_atoms, highlight_bonds, highlight_atom_map,
518 highlight_bond_map, highlight_radii, confId);
519 drawLegend(legend);
520 }
521
522 // ****************************************************************************
drawMoleculeWithHighlights(const ROMol & mol,const string & legend,const map<int,vector<DrawColour>> & highlight_atom_map,const map<int,vector<DrawColour>> & highlight_bond_map,const map<int,double> & highlight_radii,const map<int,int> & highlight_linewidth_multipliers,int confId)523 void MolDraw2D::drawMoleculeWithHighlights(
524 const ROMol &mol, const string &legend,
525 const map<int, vector<DrawColour>> &highlight_atom_map,
526 const map<int, vector<DrawColour>> &highlight_bond_map,
527 const map<int, double> &highlight_radii,
528 const map<int, int> &highlight_linewidth_multipliers, int confId) {
529 int origWidth = lineWidth();
530 vector<int> highlight_atoms;
531 for (auto ha : highlight_atom_map) {
532 highlight_atoms.emplace_back(ha.first);
533 }
534
535 if (!legend.empty()) {
536 legend_height_ = int(0.05 * double(panelHeight()));
537 } else {
538 legend_height_ = 0;
539 }
540 pushDrawDetails();
541 unique_ptr<RWMol> rwmol =
542 setupMoleculeDraw(mol, &highlight_atoms, &highlight_radii, confId);
543 ROMol const &draw_mol = rwmol ? *(rwmol) : mol;
544 if (!draw_mol.getNumConformers()) {
545 // clearly, the molecule is in a sorry state.
546 return;
547 }
548
549 if (!pre_shapes_[activeMolIdx_].empty()) {
550 MolDraw2D_detail::drawShapes(*this, pre_shapes_[activeMolIdx_]);
551 }
552
553 bool orig_fp = fillPolys();
554 setFillPolys(drawOptions().fillHighlights);
555
556 // draw the highlighted bonds first, so the atoms hide the ragged
557 // ends. This only works with filled highlighting, obs. If not, we need
558 // the highlight radii to work out the intersection of the bond highlight
559 // with the atom highlight.
560 drawHighlightedBonds(draw_mol, highlight_bond_map,
561 highlight_linewidth_multipliers, &highlight_radii);
562
563 for (auto ha : highlight_atom_map) {
564 // cout << "highlighting atom " << ha.first << " with " << ha.second.size()
565 // << " colours" << endl;
566 drawHighlightedAtom(ha.first, ha.second, &highlight_radii);
567 }
568 setFillPolys(orig_fp);
569
570 // draw plain bonds on top of highlights. Use black if either highlight
571 // colour is the same as the colour it would have been.
572 vector<pair<DrawColour, DrawColour>> bond_colours;
573 for (auto bond : draw_mol.bonds()) {
574 int beg_at = bond->getBeginAtomIdx();
575 DrawColour col1 = getColour(beg_at);
576 int end_at = bond->getEndAtomIdx();
577 DrawColour col2 = getColour(end_at);
578 auto hb = highlight_bond_map.find(bond->getIdx());
579 if (hb != highlight_bond_map.end()) {
580 const vector<DrawColour> &cols = hb->second;
581 if (find(cols.begin(), cols.end(), col1) == cols.end() ||
582 find(cols.begin(), cols.end(), col2) == cols.end()) {
583 col1 = DrawColour(0.0, 0.0, 0.0);
584 col2 = col1;
585 }
586 }
587 bond_colours.emplace_back(make_pair(col1, col2));
588 }
589 drawBonds(draw_mol, nullptr, nullptr, nullptr, nullptr, &bond_colours);
590
591 vector<DrawColour> atom_colours;
592 for (auto this_at : draw_mol.atoms()) {
593 // Get colours together for the atom labels.
594 // Passing nullptr means that we'll get a colour based on atomic number
595 // only.
596 atom_colours.emplace_back(getColour(this_at->getIdx(), nullptr, nullptr));
597 // if the chosen colour is a highlight colour for this atom, choose black
598 // instead so it is still visible.
599 auto ha = highlight_atom_map.find(this_at->getIdx());
600 if (ha != highlight_atom_map.end()) {
601 if (find(ha->second.begin(), ha->second.end(), atom_colours.back()) !=
602 ha->second.end()) {
603 atom_colours.back() = DrawColour(0.0, 0.0, 0.0);
604 }
605 }
606 }
607
608 // this puts on atom labels and such
609 finishMoleculeDraw(draw_mol, atom_colours);
610 setLineWidth(origWidth);
611
612 drawLegend(legend);
613 popDrawDetails();
614 }
615
616 // ****************************************************************************
get2DCoordsMol(RWMol & mol,double & offset,double spacing,double & maxY,double & minY,int confId,bool shiftAgents,double coordScale)617 void MolDraw2D::get2DCoordsMol(RWMol &mol, double &offset, double spacing,
618 double &maxY, double &minY, int confId,
619 bool shiftAgents, double coordScale) {
620 if (drawOptions().prepareMolsBeforeDrawing) {
621 mol.updatePropertyCache(false);
622 try {
623 RDLog::BlockLogs blocker;
624 MolOps::Kekulize(mol, false); // kekulize, but keep the aromatic flags!
625 } catch (const MolSanitizeException &) {
626 // don't need to do anything
627 }
628 MolOps::setHybridization(mol);
629 }
630 if (!mol.getNumConformers()) {
631 const bool canonOrient = true;
632 RDDepict::compute2DCoords(mol, nullptr, canonOrient);
633 } else {
634 // we need to center the molecule
635 centerMolForDrawing(mol, confId);
636 }
637 // when preparing a reaction component to be drawn we should neither kekulize
638 // (we did that above if required) nor add chiralHs
639 const bool kekulize = false;
640 const bool addChiralHs = false;
641 MolDraw2DUtils::prepareMolForDrawing(mol, kekulize, addChiralHs);
642 double minX = 1e8;
643 double maxX = -1e8;
644 double vShift = 0;
645 if (shiftAgents) {
646 vShift = 1.1 * maxY / 2;
647 }
648
649 pushDrawDetails();
650
651 extractAtomCoords(mol, confId, false);
652 extractAtomSymbols(mol);
653 for (unsigned int i = 0; i < mol.getNumAtoms(); ++i) {
654 RDGeom::Point2D p = at_cds_[activeMolIdx_][i];
655 Atom *at = mol.getAtomWithIdx(i);
656 // allow for the width of the atom label.
657 auto at_lab = getAtomSymbolAndOrientation(*at);
658 double width = 0.0, height = 0.0;
659 if (!at_lab.first.empty()) {
660 getLabelSize(at_lab.first, at_lab.second, width, height);
661 }
662 if (at_lab.second == OrientType::W) {
663 p.x -= width;
664 } else {
665 p.x -= width / 2;
666 }
667 p *= coordScale;
668 minX = std::min(minX, p.x);
669 }
670 offset += fabs(minX);
671 Conformer &conf = mol.getConformer(confId);
672 for (unsigned int i = 0; i < mol.getNumAtoms(); ++i) {
673 RDGeom::Point2D p = at_cds_[activeMolIdx_][i];
674 p.y = p.y * coordScale + vShift;
675 Atom *at = mol.getAtomWithIdx(i);
676 // allow for the width of the atom label.
677 auto at_lab = getAtomSymbolAndOrientation(*at);
678 double width = 0.0, height = 0.0;
679 if (!at_lab.first.empty()) {
680 getLabelSize(at_lab.first, at_lab.second, width, height);
681 }
682 height /= 2.0;
683 if (at_lab.second != OrientType::E) {
684 width /= 2.0;
685 }
686 if (!shiftAgents) {
687 maxY = std::max(p.y + height, maxY);
688 minY = std::min(p.y - height, minY);
689 }
690 p.x = p.x * coordScale + offset;
691 maxX = std::max(p.x + width, maxX);
692
693 // now copy the transformed coords back to the actual
694 // molecules. The initial calculations were done on the
695 // copies taken by extractAtomCoords, and that was so
696 // we could re-use existing code for scaling the picture
697 // including labels.
698 RDGeom::Point3D &at_cds = conf.getAtomPos(i);
699 at_cds.x = p.x;
700 at_cds.y = p.y;
701 }
702 offset = maxX + spacing;
703 popDrawDetails();
704 }
705
706 // ****************************************************************************
get2DCoordsForReaction(ChemicalReaction & rxn,Point2D & arrowBegin,Point2D & arrowEnd,std::vector<double> & plusLocs,double spacing,const std::vector<int> * confIds)707 void MolDraw2D::get2DCoordsForReaction(ChemicalReaction &rxn,
708 Point2D &arrowBegin, Point2D &arrowEnd,
709 std::vector<double> &plusLocs,
710 double spacing,
711 const std::vector<int> *confIds) {
712 plusLocs.resize(0);
713 double maxY = -1e8, minY = 1e8;
714 double offset = 0.0;
715
716 // reactants
717 for (unsigned int midx = 0; midx < rxn.getNumReactantTemplates(); ++midx) {
718 // add space for the "+" if required
719 if (midx > 0) {
720 plusLocs.push_back(offset);
721 offset += spacing;
722 }
723 ROMOL_SPTR reactant = rxn.getReactants()[midx];
724 int cid = -1;
725 if (confIds) {
726 cid = (*confIds)[midx];
727 }
728 get2DCoordsMol(*(RWMol *)reactant.get(), offset, spacing, maxY, minY, cid,
729 false, 1.0);
730 }
731 arrowBegin.x = offset;
732
733 offset += spacing;
734
735 double begAgentOffset = offset;
736
737 // we need to do the products now so that we know the full y range.
738 // these will have the wrong X coordinates, but we'll fix that later.
739 offset = 0;
740 for (unsigned int midx = 0; midx < rxn.getNumProductTemplates(); ++midx) {
741 // add space for the "+" if required
742 if (midx > 0) {
743 plusLocs.push_back(offset);
744 offset += spacing;
745 }
746 ROMOL_SPTR product = rxn.getProducts()[midx];
747 int cid = -1;
748 if (confIds) {
749 cid = (*confIds)[rxn.getNumReactantTemplates() +
750 rxn.getNumAgentTemplates() + midx];
751 }
752 get2DCoordsMol(*(RWMol *)product.get(), offset, spacing, maxY, minY, cid,
753 false, 1.0);
754 }
755
756 offset = begAgentOffset;
757 // agents
758 for (unsigned int midx = 0; midx < rxn.getNumAgentTemplates(); ++midx) {
759 ROMOL_SPTR agent = rxn.getAgents()[midx];
760 int cid = -1;
761 if (confIds) {
762 cid = (*confIds)[rxn.getNumReactantTemplates() + midx];
763 }
764 get2DCoordsMol(*(RWMol *)agent.get(), offset, spacing, maxY, minY, cid,
765 true, 0.45);
766 }
767 if (rxn.getNumAgentTemplates()) {
768 arrowEnd.x = offset; //- spacing;
769 } else {
770 arrowEnd.x = offset + 3 * spacing;
771 }
772 offset = arrowEnd.x + 1.5 * spacing;
773
774 // now translate the products over
775 for (unsigned int midx = 0; midx < rxn.getNumProductTemplates(); ++midx) {
776 ROMOL_SPTR product = rxn.getProducts()[midx];
777 int cid = -1;
778 if (confIds) {
779 cid = (*confIds)[rxn.getNumReactantTemplates() +
780 rxn.getNumAgentTemplates() + midx];
781 }
782 Conformer &conf = product->getConformer(cid);
783 for (unsigned int aidx = 0; aidx < product->getNumAtoms(); ++aidx) {
784 conf.getAtomPos(aidx).x += offset;
785 }
786 }
787
788 // fix the plus signs too
789 unsigned int startP = 0;
790 if (rxn.getNumReactantTemplates() > 1) {
791 startP = rxn.getNumReactantTemplates() - 1;
792 }
793 for (unsigned int pidx = startP; pidx < plusLocs.size(); ++pidx) {
794 plusLocs[pidx] += offset;
795 }
796
797 arrowBegin.y = arrowEnd.y = minY + (maxY - minY) / 2;
798 }
799
800 // ****************************************************************************
drawReaction(const ChemicalReaction & rxn,bool highlightByReactant,const std::vector<DrawColour> * highlightColorsReactants,const std::vector<int> * confIds)801 void MolDraw2D::drawReaction(
802 const ChemicalReaction &rxn, bool highlightByReactant,
803 const std::vector<DrawColour> *highlightColorsReactants,
804 const std::vector<int> *confIds) {
805 ChemicalReaction nrxn(rxn);
806 double spacing = 1.0;
807 Point2D arrowBegin, arrowEnd;
808 std::vector<double> plusLocs;
809 get2DCoordsForReaction(nrxn, arrowBegin, arrowEnd, plusLocs, spacing,
810 confIds);
811
812 MolDrawOptions origDrawOptions = drawOptions();
813 drawOptions().prepareMolsBeforeDrawing = false;
814 drawOptions().includeMetadata = false;
815
816 ROMol *tmol = ChemicalReactionToRxnMol(nrxn);
817 MolOps::findSSSR(*tmol);
818
819 if (needs_scale_ &&
820 (!nrxn.getNumReactantTemplates() || !nrxn.getNumProductTemplates())) {
821 // drawMolecule() will figure out the scaling so that the molecule
822 // fits the drawing pane. In order to ensure that we have space for the
823 // arrow, we need to figure out the scaling on our own.
824 RWMol tmol2;
825 tmol2.addAtom(new Atom(0), true, true);
826 tmol2.addAtom(new Atom(0), true, true);
827 tmol2.addConformer(new Conformer(2), true);
828 tmol2.getConformer().getAtomPos(0) =
829 RDGeom::Point3D(arrowBegin.x, arrowBegin.y, 0);
830 tmol2.getConformer().getAtomPos(1) =
831 RDGeom::Point3D(arrowEnd.x, arrowEnd.y, 0);
832
833 for (auto atom : tmol2.atoms()) {
834 atom->calcImplicitValence();
835 }
836
837 tmol2.insertMol(*tmol);
838 pushDrawDetails();
839 extractAtomCoords(tmol2, 0, true);
840 extractAtomSymbols(tmol2);
841 calculateScale(panelWidth(), drawHeight(), tmol2);
842 needs_scale_ = false;
843 popDrawDetails();
844 }
845
846 std::vector<int> *atom_highlights = nullptr;
847 std::map<int, DrawColour> *atom_highlight_colors = nullptr;
848 std::vector<int> *bond_highlights = nullptr;
849 std::map<int, DrawColour> *bond_highlight_colors = nullptr;
850 if (highlightByReactant) {
851 const std::vector<DrawColour> *colors =
852 &drawOptions().highlightColourPalette;
853 if (highlightColorsReactants) {
854 colors = highlightColorsReactants;
855 }
856 std::vector<int> atomfragmap;
857 MolOps::getMolFrags(*tmol, atomfragmap);
858
859 atom_highlights = new std::vector<int>();
860 atom_highlight_colors = new std::map<int, DrawColour>();
861 bond_highlights = new std::vector<int>();
862 bond_highlight_colors = new std::map<int, DrawColour>();
863 std::map<int, int> atommap_fragmap;
864 for (unsigned int aidx = 0; aidx < tmol->getNumAtoms(); ++aidx) {
865 int atomRole = -1;
866 Atom *atom = tmol->getAtomWithIdx(aidx);
867 if (atom->getPropIfPresent("molRxnRole", atomRole) && atomRole == 1 &&
868 atom->getAtomMapNum()) {
869 atommap_fragmap[atom->getAtomMapNum()] = atomfragmap[aidx];
870 atom_highlights->push_back(aidx);
871 (*atom_highlight_colors)[aidx] =
872 (*colors)[atomfragmap[aidx] % colors->size()];
873
874 atom->setAtomMapNum(0);
875 // add highlighted bonds to lower-numbered
876 // (and thus already covered) neighbors
877 for (const auto &nbri :
878 make_iterator_range(tmol->getAtomNeighbors(atom))) {
879 const Atom *nbr = (*tmol)[nbri];
880 if (nbr->getIdx() < aidx &&
881 atomfragmap[nbr->getIdx()] == atomfragmap[aidx]) {
882 int bondIdx =
883 tmol->getBondBetweenAtoms(aidx, nbr->getIdx())->getIdx();
884 bond_highlights->push_back(bondIdx);
885 (*bond_highlight_colors)[bondIdx] = (*atom_highlight_colors)[aidx];
886 }
887 }
888 }
889 }
890 for (unsigned int aidx = 0; aidx < tmol->getNumAtoms(); ++aidx) {
891 int atomRole = -1;
892 Atom *atom = tmol->getAtomWithIdx(aidx);
893 if (atom->getPropIfPresent("molRxnRole", atomRole) && atomRole == 2 &&
894 atom->getAtomMapNum() &&
895 atommap_fragmap.find(atom->getAtomMapNum()) !=
896 atommap_fragmap.end()) {
897 atom_highlights->push_back(aidx);
898 (*atom_highlight_colors)[aidx] =
899 (*colors)[atommap_fragmap[atom->getAtomMapNum()] % colors->size()];
900
901 atom->setAtomMapNum(0);
902 // add highlighted bonds to lower-numbered
903 // (and thus already covered) neighbors
904 for (const auto &nbri :
905 make_iterator_range(tmol->getAtomNeighbors(atom))) {
906 const Atom *nbr = (*tmol)[nbri];
907 if (nbr->getIdx() < aidx && (*atom_highlight_colors)[nbr->getIdx()] ==
908 (*atom_highlight_colors)[aidx]) {
909 int bondIdx =
910 tmol->getBondBetweenAtoms(aidx, nbr->getIdx())->getIdx();
911 bond_highlights->push_back(bondIdx);
912 (*bond_highlight_colors)[bondIdx] = (*atom_highlight_colors)[aidx];
913 }
914 }
915 }
916 }
917 }
918
919 drawMolecule(*tmol, "", atom_highlights, bond_highlights,
920 atom_highlight_colors, bond_highlight_colors);
921
922 delete tmol;
923 delete atom_highlights;
924 delete atom_highlight_colors;
925 delete bond_highlights;
926 delete bond_highlight_colors;
927
928 double o_font_scale = text_drawer_->fontScale();
929 double fsize = text_drawer_->fontSize();
930 double new_font_scale =
931 2.0 * o_font_scale * drawOptions().legendFontSize / fsize;
932 text_drawer_->setFontScale(new_font_scale);
933
934 DrawColour odc = colour();
935 setColour(options_.symbolColour);
936
937 // now add the symbols
938 for (auto plusLoc : plusLocs) {
939 Point2D loc(plusLoc, arrowBegin.y);
940 drawString("+", loc);
941 }
942
943 // The arrow:
944 drawArrow(arrowBegin, arrowEnd);
945
946 if (origDrawOptions.includeMetadata) {
947 this->updateMetadata(nrxn);
948 }
949
950 setColour(odc);
951 text_drawer_->setFontScale(o_font_scale);
952 drawOptions() = origDrawOptions;
953 }
954
955 // ****************************************************************************
drawMolecules(const std::vector<ROMol * > & mols,const std::vector<std::string> * legends,const std::vector<std::vector<int>> * highlight_atoms,const std::vector<std::vector<int>> * highlight_bonds,const std::vector<std::map<int,DrawColour>> * highlight_atom_maps,const std::vector<std::map<int,DrawColour>> * highlight_bond_maps,const std::vector<std::map<int,double>> * highlight_radii,const std::vector<int> * confIds)956 void MolDraw2D::drawMolecules(
957 const std::vector<ROMol *> &mols, const std::vector<std::string> *legends,
958 const std::vector<std::vector<int>> *highlight_atoms,
959 const std::vector<std::vector<int>> *highlight_bonds,
960 const std::vector<std::map<int, DrawColour>> *highlight_atom_maps,
961 const std::vector<std::map<int, DrawColour>> *highlight_bond_maps,
962 const std::vector<std::map<int, double>> *highlight_radii,
963 const std::vector<int> *confIds) {
964 PRECONDITION(!legends || legends->size() == mols.size(), "bad size");
965 PRECONDITION(!highlight_atoms || highlight_atoms->size() == mols.size(),
966 "bad size");
967 PRECONDITION(!highlight_bonds || highlight_bonds->size() == mols.size(),
968 "bad size");
969 PRECONDITION(
970 !highlight_atom_maps || highlight_atom_maps->size() == mols.size(),
971 "bad size");
972 PRECONDITION(
973 !highlight_bond_maps || highlight_bond_maps->size() == mols.size(),
974 "bad size");
975 PRECONDITION(!highlight_radii || highlight_radii->size() == mols.size(),
976 "bad size");
977 PRECONDITION(!confIds || confIds->size() == mols.size(), "bad size");
978 PRECONDITION(panel_width_ != 0, "panel width cannot be zero");
979 PRECONDITION(panel_height_ != 0, "panel height cannot be zero");
980 if (!mols.size()) {
981 return;
982 }
983
984 setupTextDrawer();
985 vector<unique_ptr<RWMol>> tmols;
986 calculateScale(panelWidth(), drawHeight(), mols, highlight_atoms,
987 highlight_radii, confIds, tmols);
988 // so drawMolecule doesn't recalculate the scale each time, and
989 // undo all the good work.
990 needs_scale_ = false;
991
992 int nCols = width() / panelWidth();
993 int nRows = height() / panelHeight();
994 for (unsigned int i = 0; i < mols.size(); ++i) {
995 if (!mols[i]) {
996 continue;
997 }
998
999 int row = 0;
1000 // note that this also works when no panel size is specified since
1001 // the panel dimensions defaults to -1
1002 if (nRows > 1) {
1003 row = i / nCols;
1004 }
1005 int col = 0;
1006 if (nCols > 1) {
1007 col = i % nCols;
1008 }
1009 setOffset(col * panelWidth(), row * panelHeight());
1010
1011 ROMol *draw_mol = tmols[i] ? tmols[i].get() : mols[i];
1012 unique_ptr<vector<int>> lhighlight_bonds;
1013 if (highlight_bonds) {
1014 lhighlight_bonds.reset(new std::vector<int>((*highlight_bonds)[i]));
1015 } else if (drawOptions().continuousHighlight && highlight_atoms) {
1016 lhighlight_bonds.reset(new vector<int>());
1017 getBondHighlightsForAtoms(*draw_mol, (*highlight_atoms)[i],
1018 *lhighlight_bonds);
1019 };
1020
1021 drawMolecule(*draw_mol, legends ? (*legends)[i] : "",
1022 highlight_atoms ? &(*highlight_atoms)[i] : nullptr,
1023 lhighlight_bonds.get(),
1024 highlight_atom_maps ? &(*highlight_atom_maps)[i] : nullptr,
1025 highlight_bond_maps ? &(*highlight_bond_maps)[i] : nullptr,
1026 highlight_radii ? &(*highlight_radii)[i] : nullptr,
1027 confIds ? (*confIds)[i] : -1);
1028 // save the drawn positions of the atoms on the molecule. This is the only
1029 // way that we can later add metadata
1030 auto tag = boost::str(boost::format("_atomdrawpos_%d") %
1031 (confIds ? (*confIds)[i] : -1));
1032 for (unsigned int j = 0; j < mols[i]->getNumAtoms(); ++j) {
1033 auto pt = getDrawCoords(j);
1034 mols[i]->getAtomWithIdx(j)->setProp(tag, pt, true);
1035 }
1036 }
1037 }
1038
1039 // ****************************************************************************
highlightCloseContacts()1040 void MolDraw2D::highlightCloseContacts() {
1041 if (drawOptions().flagCloseContactsDist < 0) {
1042 return;
1043 }
1044 int tol =
1045 drawOptions().flagCloseContactsDist * drawOptions().flagCloseContactsDist;
1046 boost::dynamic_bitset<> flagged(at_cds_[activeMolIdx_].size());
1047 for (unsigned int i = 0; i < at_cds_[activeMolIdx_].size(); ++i) {
1048 if (flagged[i]) {
1049 continue;
1050 }
1051 Point2D ci = getDrawCoords(at_cds_[activeMolIdx_][i]);
1052 for (unsigned int j = i + 1; j < at_cds_[activeMolIdx_].size(); ++j) {
1053 if (flagged[j]) {
1054 continue;
1055 }
1056 Point2D cj = getDrawCoords(at_cds_[activeMolIdx_][j]);
1057 double d = (cj - ci).lengthSq();
1058 if (d <= tol) {
1059 flagged.set(i);
1060 flagged.set(j);
1061 break;
1062 }
1063 }
1064 if (flagged[i]) {
1065 Point2D p1 = at_cds_[activeMolIdx_][i];
1066 Point2D p2 = p1;
1067 Point2D offset(0.1, 0.1);
1068 p1 -= offset;
1069 p2 += offset;
1070 bool ofp = fillPolys();
1071 setFillPolys(false);
1072 DrawColour odc = colour();
1073 setColour(DrawColour(1, 0, 0));
1074 drawRect(p1, p2);
1075 setColour(odc);
1076 setFillPolys(ofp);
1077 }
1078 }
1079 }
1080
1081 // ****************************************************************************
1082 // transform a set of coords in the molecule's coordinate system
1083 // to drawing system coordinates
getDrawCoords(const Point2D & mol_cds) const1084 Point2D MolDraw2D::getDrawCoords(const Point2D &mol_cds) const {
1085 double x = scale_ * (mol_cds.x - x_min_ + x_trans_);
1086 double y = scale_ * (mol_cds.y - y_min_ + y_trans_);
1087 // y is now the distance from the top of the image, we need to
1088 // invert that:
1089 x += x_offset_;
1090 y -= y_offset_;
1091 y = panelHeight() - legend_height_ - y;
1092 return Point2D(x, y);
1093 }
1094
1095 // ****************************************************************************
getDrawCoords(int at_num) const1096 Point2D MolDraw2D::getDrawCoords(int at_num) const {
1097 PRECONDITION(activeMolIdx_ >= 0, "bad mol idx");
1098 return getDrawCoords(at_cds_[activeMolIdx_][at_num]);
1099 }
1100
1101 // ****************************************************************************
getAtomCoords(const pair<int,int> & screen_cds) const1102 Point2D MolDraw2D::getAtomCoords(const pair<int, int> &screen_cds) const {
1103 return getAtomCoords(
1104 make_pair(double(screen_cds.first), double(screen_cds.second)));
1105 }
1106
getAtomCoords(const pair<double,double> & screen_cds) const1107 Point2D MolDraw2D::getAtomCoords(const pair<double, double> &screen_cds) const {
1108 double screen_x = screen_cds.first - x_offset_;
1109 double screen_y = screen_cds.second - y_offset_;
1110 auto x = double(screen_x / scale_ + x_min_ - x_trans_);
1111 auto y = double(y_min_ - y_trans_ -
1112 (screen_y - panelHeight() + legend_height_) / scale_);
1113 return Point2D(x, y);
1114 }
1115
1116 // ****************************************************************************
getAtomCoords(int at_num) const1117 Point2D MolDraw2D::getAtomCoords(int at_num) const {
1118 PRECONDITION(activeMolIdx_ >= 0, "bad active mol");
1119 return at_cds_[activeMolIdx_][at_num];
1120 }
1121
1122 // ****************************************************************************
fontSize() const1123 double MolDraw2D::fontSize() const { return text_drawer_->fontSize(); }
1124
1125 // ****************************************************************************
setFontSize(double new_size)1126 void MolDraw2D::setFontSize(double new_size) {
1127 text_drawer_->setFontSize(new_size);
1128 }
1129
1130 // ****************************************************************************
setScale(int width,int height,const Point2D & minv,const Point2D & maxv,const ROMol * mol)1131 void MolDraw2D::setScale(int width, int height, const Point2D &minv,
1132 const Point2D &maxv, const ROMol *mol) {
1133 PRECONDITION(width > 0, "bad width");
1134 PRECONDITION(height > 0, "bad height");
1135
1136 double x_max, y_max;
1137 if (mol) {
1138 pushDrawDetails();
1139 unique_ptr<RWMol> tmol =
1140 setupDrawMolecule(*mol, nullptr, nullptr, -1, width, height);
1141 calculateScale(height, width, *tmol);
1142 popDrawDetails();
1143 x_min_ = min(minv.x, x_min_);
1144 y_min_ = min(minv.y, y_min_);
1145 x_max = max(maxv.x, x_range_ + x_min_);
1146 y_max = max(maxv.y, y_range_ + y_min_);
1147 } else {
1148 x_min_ = minv.x;
1149 y_min_ = minv.y;
1150 x_max = maxv.x;
1151 y_max = maxv.y;
1152 }
1153
1154 x_range_ = x_max - x_min_;
1155 y_range_ = y_max - y_min_;
1156
1157 needs_scale_ = false;
1158
1159 if (x_range_ < 1.0e-4) {
1160 x_range_ = 1.0;
1161 x_min_ = -0.5;
1162 }
1163 if (y_range_ < 1.0e-4) {
1164 y_range_ = 1.0;
1165 y_min_ = -0.5;
1166 }
1167
1168 // put a buffer round the drawing and calculate a final scale
1169 x_min_ -= drawOptions().padding * x_range_;
1170 x_range_ *= 1 + 2 * drawOptions().padding;
1171 y_min_ -= drawOptions().padding * y_range_;
1172 y_range_ *= 1 + 2 * drawOptions().padding;
1173
1174 scale_ = std::min(double(width) / x_range_, double(height) / y_range_);
1175 text_drawer_->setFontScale(scale_);
1176 double y_mid = y_min_ + 0.5 * y_range_;
1177 double x_mid = x_min_ + 0.5 * x_range_;
1178 x_trans_ = y_trans_ = 0.0; // getDrawCoords uses [xy_]trans_
1179 Point2D mid = getDrawCoords(Point2D(x_mid, y_mid));
1180 // that used the offset, we need to remove that:
1181 mid.x -= x_offset_;
1182 mid.y += y_offset_;
1183 x_trans_ = (width / 2 - mid.x) / scale_;
1184 y_trans_ = (mid.y - height / 2) / scale_;
1185 }
1186
1187 // ****************************************************************************
calculateScale(int width,int height,const ROMol & mol,const std::vector<int> * highlight_atoms,const std::map<int,double> * highlight_radii,int confId)1188 void MolDraw2D::calculateScale(int width, int height, const ROMol &mol,
1189 const std::vector<int> *highlight_atoms,
1190 const std::map<int, double> *highlight_radii,
1191 int confId) {
1192 PRECONDITION(width > 0, "bad width");
1193 PRECONDITION(height > 0, "bad height");
1194 PRECONDITION(activeMolIdx_ >= 0, "bad active mol");
1195
1196 // cout << "calculateScale width = " << width << " height = " << height
1197 // << endl;
1198
1199 x_min_ = y_min_ = numeric_limits<double>::max();
1200 double x_max(-x_min_), y_max(-y_min_);
1201
1202 // first find the bounding box defined by the atoms
1203 for (const auto &pt : at_cds_[activeMolIdx_]) {
1204 x_min_ = std::min(pt.x, x_min_);
1205 y_min_ = std::min(pt.y, y_min_);
1206 x_max = std::max(pt.x, x_max);
1207 y_max = std::max(pt.y, y_max);
1208 }
1209
1210 // adjust based on the shapes (if any)
1211 for (const auto &shp : pre_shapes_[activeMolIdx_]) {
1212 for (const auto &pt : shp.points) {
1213 x_min_ = std::min(pt.x, x_min_);
1214 y_min_ = std::min(pt.y, y_min_);
1215 x_max = std::max(pt.x, x_max);
1216 y_max = std::max(pt.y, y_max);
1217 }
1218 }
1219 for (const auto &shp : post_shapes_[activeMolIdx_]) {
1220 for (const auto &pt : shp.points) {
1221 x_min_ = std::min(pt.x, x_min_);
1222 y_min_ = std::min(pt.y, y_min_);
1223 x_max = std::max(pt.x, x_max);
1224 y_max = std::max(pt.y, y_max);
1225 }
1226 }
1227
1228 // calculate the x and y spans
1229 x_range_ = x_max - x_min_;
1230 y_range_ = y_max - y_min_;
1231 if (x_range_ < 1e-4) {
1232 x_range_ = 2.0;
1233 x_min_ -= 1.0;
1234 x_max += 1.0;
1235 }
1236 if (y_range_ < 1e-4) {
1237 y_range_ = 2.0;
1238 y_min_ -= 1.0;
1239 y_max += 1.0;
1240 }
1241
1242 scale_ = std::min(double(width) / x_range_, double(height) / y_range_);
1243 // we may need to adjust the scale if there are atom symbols that go off
1244 // the edges, and we probably need to do it iteratively because
1245 // get_string_size uses the current value of scale_.
1246 // We also need to adjust for highlighted atoms if there are any.
1247 // And now we need to take account of strings with N/S orientation
1248 // as well.
1249 while (scale_ > 1e-4) {
1250 text_drawer_->setFontScale(scale_);
1251 adjustScaleForAtomLabels(highlight_atoms, highlight_radii);
1252 adjustScaleForRadicals(mol);
1253 if (supportsAnnotations() && !annotations_.empty() &&
1254 !annotations_[activeMolIdx_].empty()) {
1255 adjustScaleForAnnotation(annotations_[activeMolIdx_]);
1256 }
1257 double old_scale = scale_;
1258 scale_ = std::min(double(width) / x_range_, double(height) / y_range_);
1259 if (fabs(scale_ - old_scale) < 0.1) {
1260 break;
1261 }
1262 }
1263
1264 // put a 5% buffer round the drawing and calculate a final scale
1265 x_min_ -= drawOptions().padding * x_range_;
1266 x_range_ *= 1 + 2 * drawOptions().padding;
1267 y_min_ -= drawOptions().padding * y_range_;
1268 y_range_ *= 1 + 2 * drawOptions().padding;
1269
1270 if (x_range_ > 1e-4 || y_range_ > 1e-4) {
1271 scale_ = std::min(double(width) / x_range_, double(height) / y_range_);
1272 double fix_scale = scale_;
1273 // after all that, use the fixed scale unless it's too big, in which case
1274 // scale the drawing down to fit.
1275 // fixedScale takes precedence if both it and fixedBondLength are given.
1276 if (drawOptions().fixedBondLength > 0.0) {
1277 fix_scale = drawOptions().fixedBondLength;
1278 }
1279 if (drawOptions().fixedScale > 0.0) {
1280 fix_scale = double(width) * drawOptions().fixedScale;
1281 }
1282 if (scale_ > fix_scale) {
1283 scale_ = fix_scale;
1284 }
1285 centrePicture(width, height);
1286 } else {
1287 scale_ = 1;
1288 x_trans_ = 0.;
1289 y_trans_ = 0.;
1290 }
1291
1292 const auto &conf = mol.getConformer(confId);
1293 double meanBondLength = 0.0;
1294 unsigned int nBonds = 0;
1295 for (const auto &bond : mol.bonds()) {
1296 meanBondLength += (conf.getAtomPos(bond->getBeginAtomIdx()) -
1297 conf.getAtomPos(bond->getEndAtomIdx()))
1298 .length();
1299 ++nBonds;
1300 }
1301 meanBondLength /= nBonds;
1302 // the rdkit depictor sets bond lengths to be like covalent bond lengths
1303 // but many others set them to a smaller base value
1304 // In this case the fonts will be too big, so add a correction there.
1305 // both the 1.0 and the 0.75 are empirical
1306 if (meanBondLength < 1.0) {
1307 text_drawer_->setBaseFontSize(text_drawer_->baseFontSize() * 0.75);
1308 }
1309 text_drawer_->setFontScale(scale_);
1310 // cout << "leaving calculateScale" << endl;
1311 // cout << "final scale : " << scale_ << endl;
1312 }
1313
1314 // ****************************************************************************
calculateScale(int width,int height,const vector<ROMol * > & mols,const vector<vector<int>> * highlight_atoms,const vector<map<int,double>> * highlight_radii,const vector<int> * confIds,vector<unique_ptr<RWMol>> & tmols)1315 void MolDraw2D::calculateScale(int width, int height,
1316 const vector<ROMol *> &mols,
1317 const vector<vector<int>> *highlight_atoms,
1318 const vector<map<int, double>> *highlight_radii,
1319 const vector<int> *confIds,
1320 vector<unique_ptr<RWMol>> &tmols) {
1321 double global_x_min, global_x_max, global_y_min, global_y_max;
1322 global_x_min = global_y_min = numeric_limits<double>::max();
1323 global_x_max = global_y_max = -numeric_limits<double>::max();
1324
1325 double meanBondLength = 0.0;
1326 unsigned int nBonds = 0;
1327 for (size_t i = 0; i < mols.size(); ++i) {
1328 tabulaRasa();
1329 if (!mols[i]) {
1330 tmols.emplace_back(unique_ptr<RWMol>(new RWMol));
1331 continue;
1332 }
1333 const vector<int> *ha = highlight_atoms ? &(*highlight_atoms)[i] : nullptr;
1334 const map<int, double> *hr =
1335 highlight_radii ? &(*highlight_radii)[i] : nullptr;
1336 int id = confIds ? (*confIds)[i] : -1;
1337
1338 pushDrawDetails();
1339 needs_scale_ = true;
1340 unique_ptr<RWMol> rwmol =
1341 setupDrawMolecule(*mols[i], ha, hr, id, width, height);
1342 double x_max = x_min_ + x_range_;
1343 double y_max = y_min_ + y_range_;
1344 global_x_min = min(x_min_, global_x_min);
1345 global_x_max = max(x_max, global_x_max);
1346 global_y_min = min(y_min_, global_y_min);
1347 global_y_max = max(y_max, global_y_max);
1348
1349 const auto &conf = rwmol->getConformer(id);
1350 for (const auto &bond : rwmol->bonds()) {
1351 meanBondLength += (conf.getAtomPos(bond->getBeginAtomIdx()) -
1352 conf.getAtomPos(bond->getEndAtomIdx()))
1353 .length();
1354 ++nBonds;
1355 }
1356
1357 tmols.emplace_back(std::move(rwmol));
1358 popDrawDetails();
1359 }
1360 meanBondLength /= nBonds;
1361 // the rdkit depictor sets bond lengths to be like covalent bond lengths
1362 // but many others set them to a smaller base value
1363 // In this case the fonts will be too big, so add a correction there.
1364 // both the 1.0 and the 0.75 are empirical
1365 if (meanBondLength < 1.0) {
1366 text_drawer_->setBaseFontSize(text_drawer_->baseFontSize() * 0.75);
1367 }
1368
1369 x_min_ = global_x_min;
1370 y_min_ = global_y_min;
1371 x_range_ = global_x_max - global_x_min;
1372 y_range_ = global_y_max - global_y_min;
1373 scale_ = std::min(double(width) / x_range_, double(height) / y_range_);
1374 text_drawer_->setFontScale(scale_);
1375 centrePicture(width, height);
1376 }
1377
1378 // ****************************************************************************
centrePicture(int width,int height)1379 void MolDraw2D::centrePicture(int width, int height) {
1380 double y_mid = y_min_ + 0.5 * y_range_;
1381 double x_mid = x_min_ + 0.5 * x_range_;
1382 Point2D mid;
1383 // this is getDrawCoords() but using height rather than height()
1384 // to turn round the y coord and not using x_trans_ and y_trans_
1385 // which we are trying to calculate at this point.
1386 mid.x = scale_ * (x_mid - x_min_);
1387 mid.y = scale_ * (y_mid - y_min_);
1388 // y is now the distance from the top of the image, we need to
1389 // invert that:
1390 mid.x += x_offset_;
1391 mid.y -= y_offset_;
1392 mid.y = height - mid.y;
1393
1394 // that used the offset, we need to remove that:
1395 mid.x -= x_offset_;
1396 mid.y += y_offset_;
1397 x_trans_ = (width / 2 - mid.x) / scale_;
1398 y_trans_ = (mid.y - height / 2) / scale_;
1399 };
1400
1401 namespace {} // namespace
1402
1403 // ****************************************************************************
drawLine(const Point2D & cds1,const Point2D & cds2,const DrawColour & col1,const DrawColour & col2)1404 void MolDraw2D::drawLine(const Point2D &cds1, const Point2D &cds2,
1405 const DrawColour &col1, const DrawColour &col2) {
1406 if (drawOptions().comicMode) {
1407 setFillPolys(false);
1408 if (col1 == col2) {
1409 setColour(col1);
1410 auto pts =
1411 MolDraw2D_detail::handdrawnLine(cds1, cds2, scale_, true, true);
1412 drawPolygon(pts);
1413 } else {
1414 Point2D mid = (cds1 + cds2) * 0.5;
1415 setColour(col1);
1416 auto pts =
1417 MolDraw2D_detail::handdrawnLine(cds1, mid, scale_, true, false);
1418 drawPolygon(pts);
1419 setColour(col2);
1420 auto pts2 =
1421 MolDraw2D_detail::handdrawnLine(mid, cds2, scale_, false, true);
1422 drawPolygon(pts2);
1423 }
1424 } else {
1425 if (col1 == col2) {
1426 setColour(col1);
1427 drawLine(cds1, cds2);
1428 } else {
1429 Point2D mid = (cds1 + cds2) * 0.5;
1430 setColour(col1);
1431 drawLine(cds1, mid);
1432 setColour(col2);
1433 drawLine(mid, cds2);
1434 }
1435 }
1436 }
1437
1438 // ****************************************************************************
getStringSize(const std::string & label,double & label_width,double & label_height) const1439 void MolDraw2D::getStringSize(const std::string &label, double &label_width,
1440 double &label_height) const {
1441 text_drawer_->getStringSize(label, label_width, label_height);
1442 label_width /= scale();
1443 label_height /= scale();
1444
1445 // cout << label << " : " << label_width << " by " << label_height
1446 // << " : " << scale() << endl;
1447 }
1448
1449 // ****************************************************************************
getLabelSize(const string & label,OrientType orient,double & label_width,double & label_height) const1450 void MolDraw2D::getLabelSize(const string &label, OrientType orient,
1451 double &label_width, double &label_height) const {
1452 if (orient == OrientType::N || orient == OrientType::S) {
1453 label_height = 0.0;
1454 label_width = 0.0;
1455 vector<string> sym_bits = atomLabelToPieces(label, orient);
1456 double height, width;
1457 for (auto bit : sym_bits) {
1458 getStringSize(bit, width, height);
1459 if (width > label_width) {
1460 label_width = width;
1461 }
1462 label_height += height;
1463 }
1464 } else {
1465 getStringSize(label, label_width, label_height);
1466 }
1467 }
1468
1469 // ****************************************************************************
getStringExtremes(const string & label,OrientType orient,const Point2D & cds,double & x_min,double & y_min,double & x_max,double & y_max) const1470 void MolDraw2D::getStringExtremes(const string &label, OrientType orient,
1471 const Point2D &cds, double &x_min,
1472 double &y_min, double &x_max,
1473 double &y_max) const {
1474 text_drawer_->getStringExtremes(label, orient, x_min, y_min, x_max, y_max);
1475 Point2D draw_cds = getDrawCoords(cds);
1476 x_min += draw_cds.x;
1477 x_max += draw_cds.x;
1478 y_min += draw_cds.y;
1479 y_max += draw_cds.y;
1480
1481 Point2D new_mins = getAtomCoords(make_pair(x_min, y_min));
1482 Point2D new_maxs = getAtomCoords(make_pair(x_max, y_max));
1483 x_min = new_mins.x;
1484 y_min = new_mins.y;
1485 x_max = new_maxs.x;
1486 y_max = new_maxs.y;
1487
1488 // draw coords to atom coords reverses y
1489 if (y_min > y_max) {
1490 swap(y_min, y_max);
1491 }
1492 }
1493
1494 // ****************************************************************************
1495 // draws the string centred on cds
drawString(const string & str,const Point2D & cds)1496 void MolDraw2D::drawString(const string &str, const Point2D &cds) {
1497 Point2D draw_cds = getDrawCoords(cds);
1498 text_drawer_->drawString(str, draw_cds, OrientType::N);
1499 // int olw = lineWidth();
1500 // setLineWidth(0);
1501 // text_drawer_->drawStringRects(str, OrientType::N, draw_cds, *this);
1502 // setLineWidth(olw);
1503 }
1504
1505 // ****************************************************************************
drawString(const std::string & str,const Point2D & cds,TextAlignType talign)1506 void MolDraw2D::drawString(const std::string &str, const Point2D &cds,
1507 TextAlignType talign) {
1508 Point2D draw_cds = getDrawCoords(cds);
1509 text_drawer_->drawString(str, draw_cds, talign);
1510 }
1511
1512 // ****************************************************************************
getColour(int atom_idx,const std::vector<int> * highlight_atoms,const std::map<int,DrawColour> * highlight_map)1513 DrawColour MolDraw2D::getColour(
1514 int atom_idx, const std::vector<int> *highlight_atoms,
1515 const std::map<int, DrawColour> *highlight_map) {
1516 PRECONDITION(activeMolIdx_ >= 0, "bad mol idx");
1517 PRECONDITION(atom_idx >= 0, "bad atom_idx");
1518 PRECONDITION(rdcast<int>(atomic_nums_[activeMolIdx_].size()) > atom_idx,
1519 "bad atom_idx");
1520 DrawColour retval =
1521 getColourByAtomicNum(atomic_nums_[activeMolIdx_][atom_idx]);
1522
1523 // set contents of highlight_atoms to red
1524 if (!drawOptions().circleAtoms && !drawOptions().continuousHighlight) {
1525 if (highlight_atoms &&
1526 highlight_atoms->end() !=
1527 find(highlight_atoms->begin(), highlight_atoms->end(), atom_idx)) {
1528 retval = drawOptions().highlightColour;
1529 }
1530 // over-ride with explicit colour from highlight_map if there is one
1531 if (highlight_map) {
1532 auto p = highlight_map->find(atom_idx);
1533 if (p != highlight_map->end()) {
1534 retval = p->second;
1535 }
1536 }
1537 }
1538 return retval;
1539 }
1540
1541 // ****************************************************************************
getColourByAtomicNum(int atomic_num)1542 DrawColour MolDraw2D::getColourByAtomicNum(int atomic_num) {
1543 DrawColour res;
1544 if (drawOptions().atomColourPalette.find(atomic_num) !=
1545 drawOptions().atomColourPalette.end()) {
1546 res = drawOptions().atomColourPalette[atomic_num];
1547 } else if (atomic_num != -1 && drawOptions().atomColourPalette.find(-1) !=
1548 drawOptions().atomColourPalette.end()) {
1549 // if -1 is in the palette, we use that for undefined colors
1550 res = drawOptions().atomColourPalette[-1];
1551 } else {
1552 // if all else fails, default to black:
1553 res = DrawColour(0, 0, 0);
1554 }
1555 return res;
1556 }
1557
1558 // ****************************************************************************
setupDrawMolecule(const ROMol & mol,const vector<int> * highlight_atoms,const map<int,double> * highlight_radii,int confId,int width,int height)1559 unique_ptr<RWMol> MolDraw2D::setupDrawMolecule(
1560 const ROMol &mol, const vector<int> *highlight_atoms,
1561 const map<int, double> *highlight_radii, int confId, int width,
1562 int height) {
1563 // prepareMolForDrawing needs a RWMol but don't copy the original mol
1564 // if we don't need to
1565 unique_ptr<RWMol> rwmol;
1566 if (drawOptions().prepareMolsBeforeDrawing || !mol.getNumConformers()) {
1567 rwmol.reset(new RWMol(mol));
1568 MolDraw2DUtils::prepareMolForDrawing(*rwmol);
1569 }
1570 if (drawOptions().centreMoleculesBeforeDrawing) {
1571 if (!rwmol) rwmol.reset(new RWMol(mol));
1572 if (rwmol->getNumConformers()) {
1573 centerMolForDrawing(*rwmol, confId);
1574 }
1575 }
1576 if (drawOptions().simplifiedStereoGroupLabel &&
1577 !mol.hasProp(common_properties::molNote)) {
1578 auto sgs = mol.getStereoGroups();
1579 if (sgs.size() == 1) {
1580 boost::dynamic_bitset<> chiralAts(mol.getNumAtoms());
1581 for (const auto atom : mol.atoms()) {
1582 if (atom->getChiralTag() > Atom::ChiralType::CHI_UNSPECIFIED &&
1583 atom->getChiralTag() < Atom::ChiralType::CHI_OTHER) {
1584 chiralAts.set(atom->getIdx(), 1);
1585 }
1586 }
1587 for (const auto atm : sgs[0].getAtoms()) {
1588 chiralAts.set(atm->getIdx(), 0);
1589 }
1590 if (chiralAts.none()) {
1591 // all specified chiral centers are accounted for by this StereoGroup.
1592 if (sgs[0].getGroupType() == StereoGroupType::STEREO_OR ||
1593 sgs[0].getGroupType() == StereoGroupType::STEREO_AND) {
1594 if (!rwmol) rwmol.reset(new RWMol(mol));
1595 std::vector<StereoGroup> empty;
1596 rwmol->setStereoGroups(std::move(empty));
1597 std::string label =
1598 sgs[0].getGroupType() == StereoGroupType::STEREO_OR
1599 ? "OR enantiomer"
1600 : "AND enantiomer";
1601 rwmol->setProp(common_properties::molNote, label);
1602 }
1603 // clear the chiral codes on the atoms so that we don't
1604 // inadvertently draw them later
1605 for (const auto atm : sgs[0].getAtoms()) {
1606 rwmol->getAtomWithIdx(atm->getIdx())
1607 ->clearProp(common_properties::_CIPCode);
1608 }
1609 }
1610 }
1611 }
1612 ROMol const &draw_mol = rwmol ? *(rwmol) : mol;
1613 if (!draw_mol.getNumConformers()) {
1614 // clearly, the molecule is in a sorry state.
1615 return rwmol;
1616 }
1617
1618 if (drawOptions().addStereoAnnotation) {
1619 MolDraw2D_detail::addStereoAnnotation(draw_mol);
1620 }
1621 if (drawOptions().addAtomIndices) {
1622 MolDraw2D_detail::addAtomIndices(draw_mol);
1623 }
1624 if (drawOptions().addBondIndices) {
1625 MolDraw2D_detail::addBondIndices(draw_mol);
1626 }
1627 if (!activeMolIdx_) {
1628 if (drawOptions().clearBackground) {
1629 clearDrawing();
1630 }
1631 }
1632 bool updateBBox = !activeMolIdx_;
1633 extractAtomCoords(draw_mol, confId, updateBBox);
1634 extractAtomSymbols(draw_mol);
1635 extractAtomNotes(draw_mol);
1636 extractBondNotes(draw_mol);
1637 extractRadicals(draw_mol);
1638 if (activeMolIdx_ >= 0 &&
1639 post_shapes_.size() > static_cast<size_t>(activeMolIdx_) &&
1640 pre_shapes_.size() > static_cast<size_t>(activeMolIdx_)) {
1641 post_shapes_[activeMolIdx_].clear();
1642 pre_shapes_[activeMolIdx_].clear();
1643 }
1644 extractSGroupData(draw_mol);
1645 extractVariableBonds(draw_mol);
1646 extractBrackets(draw_mol);
1647 extractMolNotes(draw_mol);
1648 extractLinkNodes(draw_mol);
1649
1650 if (!activeMolIdx_ && needs_scale_) {
1651 calculateScale(width, height, draw_mol, highlight_atoms, highlight_radii,
1652 confId);
1653 needs_scale_ = false;
1654 }
1655
1656 return rwmol;
1657 }
1658
1659 // ****************************************************************************
pushDrawDetails()1660 void MolDraw2D::pushDrawDetails() {
1661 at_cds_.push_back(std::vector<Point2D>());
1662 atomic_nums_.push_back(std::vector<int>());
1663 atom_syms_.push_back(std::vector<std::pair<std::string, OrientType>>());
1664 annotations_.push_back(std::vector<AnnotationType>());
1665 pre_shapes_.push_back(std::vector<MolDrawShape>());
1666 post_shapes_.push_back(std::vector<MolDrawShape>());
1667 radicals_.push_back(
1668 std::vector<std::pair<std::shared_ptr<StringRect>, OrientType>>());
1669 activeMolIdx_++;
1670 }
1671
1672 // ****************************************************************************
popDrawDetails()1673 void MolDraw2D::popDrawDetails() {
1674 activeMolIdx_--;
1675 annotations_.pop_back();
1676 pre_shapes_.pop_back();
1677 post_shapes_.pop_back();
1678 atom_syms_.pop_back();
1679 atomic_nums_.pop_back();
1680 radicals_.pop_back();
1681 at_cds_.pop_back();
1682 }
1683
1684 // ****************************************************************************
setupMoleculeDraw(const ROMol & mol,const vector<int> * highlight_atoms,const map<int,double> * highlight_radii,int confId)1685 unique_ptr<RWMol> MolDraw2D::setupMoleculeDraw(
1686 const ROMol &mol, const vector<int> *highlight_atoms,
1687 const map<int, double> *highlight_radii, int confId) {
1688 unique_ptr<RWMol> rwmol =
1689 setupDrawMolecule(mol, highlight_atoms, highlight_radii, confId,
1690 panelWidth(), drawHeight());
1691 ROMol const &draw_mol = rwmol ? *(rwmol) : mol;
1692
1693 if (drawOptions().includeAtomTags) {
1694 tagAtoms(draw_mol);
1695 }
1696 if (drawOptions().atomRegions.size()) {
1697 for (const std::vector<int> ®ion : drawOptions().atomRegions) {
1698 if (region.size() > 1) {
1699 Point2D minv = at_cds_[activeMolIdx_][region[0]];
1700 Point2D maxv = at_cds_[activeMolIdx_][region[0]];
1701 for (int idx : region) {
1702 const Point2D &pt = at_cds_[activeMolIdx_][idx];
1703 minv.x = std::min(minv.x, pt.x);
1704 minv.y = std::min(minv.y, pt.y);
1705 maxv.x = std::max(maxv.x, pt.x);
1706 maxv.y = std::max(maxv.y, pt.y);
1707 }
1708 Point2D center = (maxv + minv) / 2;
1709 Point2D size = (maxv - minv);
1710 size *= 0.2;
1711 minv -= size / 2;
1712 maxv += size / 2;
1713 setColour(DrawColour(.8, .8, .8));
1714 // drawEllipse(minv,maxv);
1715 drawRect(minv, maxv);
1716 }
1717 }
1718 }
1719
1720 return rwmol;
1721 }
1722
1723 // ****************************************************************************
setupTextDrawer()1724 void MolDraw2D::setupTextDrawer() {
1725 text_drawer_->setMaxFontSize(drawOptions().maxFontSize);
1726 text_drawer_->setMinFontSize(drawOptions().minFontSize);
1727 try {
1728 text_drawer_->setFontFile(drawOptions().fontFile);
1729 } catch (std::runtime_error &e) {
1730 BOOST_LOG(rdWarningLog) << e.what() << std::endl;
1731 text_drawer_->setFontFile("");
1732 BOOST_LOG(rdWarningLog) << "Falling back to original font file "
1733 << text_drawer_->getFontFile() << "." << std::endl;
1734 }
1735 }
1736
1737 // ****************************************************************************
drawBonds(const ROMol & draw_mol,const vector<int> * highlight_atoms,const map<int,DrawColour> * highlight_atom_map,const vector<int> * highlight_bonds,const map<int,DrawColour> * highlight_bond_map,const std::vector<std::pair<DrawColour,DrawColour>> * bond_colours)1738 void MolDraw2D::drawBonds(
1739 const ROMol &draw_mol, const vector<int> *highlight_atoms,
1740 const map<int, DrawColour> *highlight_atom_map,
1741 const vector<int> *highlight_bonds,
1742 const map<int, DrawColour> *highlight_bond_map,
1743 const std::vector<std::pair<DrawColour, DrawColour>> *bond_colours) {
1744 for (auto this_at : draw_mol.atoms()) {
1745 int this_idx = this_at->getIdx();
1746 for (const auto &nbri :
1747 make_iterator_range(draw_mol.getAtomBonds(this_at))) {
1748 const Bond *bond = draw_mol[nbri];
1749 int nbr_idx = bond->getOtherAtomIdx(this_idx);
1750 if (nbr_idx < static_cast<int>(at_cds_[activeMolIdx_].size()) &&
1751 nbr_idx > this_idx) {
1752 drawBond(draw_mol, bond, this_idx, nbr_idx, highlight_atoms,
1753 highlight_atom_map, highlight_bonds, highlight_bond_map,
1754 bond_colours);
1755 }
1756 }
1757 }
1758 }
1759
1760 // ****************************************************************************
finishMoleculeDraw(const RDKit::ROMol & draw_mol,const vector<DrawColour> & atom_colours)1761 void MolDraw2D::finishMoleculeDraw(const RDKit::ROMol &draw_mol,
1762 const vector<DrawColour> &atom_colours) {
1763 if (drawOptions().dummiesAreAttachments) {
1764 for (auto at1 : draw_mol.atoms()) {
1765 if (at1->hasProp(common_properties::atomLabel) ||
1766 drawOptions().atomLabels.find(at1->getIdx()) !=
1767 drawOptions().atomLabels.end()) {
1768 // skip dummies that explicitly have a label provided
1769 continue;
1770 }
1771 if (at1->getAtomicNum() == 0 && at1->getDegree() == 1) {
1772 Point2D &at1_cds = at_cds_[activeMolIdx_][at1->getIdx()];
1773 const auto &iter_pair = draw_mol.getAtomNeighbors(at1);
1774 const Atom *at2 = draw_mol[*iter_pair.first];
1775 Point2D &at2_cds = at_cds_[activeMolIdx_][at2->getIdx()];
1776 drawAttachmentLine(at2_cds, at1_cds, DrawColour(.5, .5, .5));
1777 }
1778 }
1779 }
1780
1781 for (int i = 0, is = atom_syms_[activeMolIdx_].size(); i < is; ++i) {
1782 if (!atom_syms_[activeMolIdx_][i].first.empty()) {
1783 drawAtomLabel(i, atom_colours[i]);
1784 }
1785 }
1786 text_drawer_->setColour(drawOptions().annotationColour);
1787 if (!supportsAnnotations() && !annotations_.empty()) {
1788 BOOST_LOG(rdWarningLog) << "annotations not currently supported for this "
1789 "MolDraw2D class, they will be ignored."
1790 << std::endl;
1791 } else {
1792 for (const auto &annotation : annotations_[activeMolIdx_]) {
1793 drawAnnotation(annotation);
1794 }
1795 }
1796
1797 if (drawOptions().includeRadicals) {
1798 drawRadicals(draw_mol);
1799 }
1800
1801 if (!post_shapes_[activeMolIdx_].empty()) {
1802 MolDraw2D_detail::drawShapes(*this, post_shapes_[activeMolIdx_]);
1803 }
1804
1805 if (drawOptions().flagCloseContactsDist >= 0) {
1806 highlightCloseContacts();
1807 }
1808 }
1809
1810 // ****************************************************************************
drawLegend(const string & legend)1811 void MolDraw2D::drawLegend(const string &legend) {
1812 int olh = legend_height_;
1813 legend_height_ = 0; // so we use the whole panel
1814
1815 auto calc_legend_height = [&](const std::vector<std::string> &legend_bits,
1816 double &total_width, double &total_height) {
1817 total_width = total_height = 0;
1818 for (auto bit : legend_bits) {
1819 double x_min, y_min, x_max, y_max;
1820 text_drawer_->getStringExtremes(bit, OrientType::N, x_min, y_min, x_max,
1821 y_max, true);
1822 total_height += y_max - y_min;
1823 total_width = std::max(total_width, x_max - x_min);
1824 }
1825 };
1826
1827 if (!legend.empty()) {
1828 std::vector<std::string> legend_bits;
1829 // split any strings on newlines
1830 string next_piece;
1831 for (auto c : legend) {
1832 if (c == '\n') {
1833 if (!next_piece.empty()) {
1834 legend_bits.push_back(next_piece);
1835 }
1836 next_piece = "";
1837 } else {
1838 next_piece += c;
1839 }
1840 }
1841 if (!next_piece.empty()) {
1842 legend_bits.push_back(next_piece);
1843 }
1844 double ominfs = text_drawer_->minFontSize();
1845 text_drawer_->setMinFontSize(-1);
1846
1847 double o_font_scale = text_drawer_->fontScale();
1848 double fsize = text_drawer_->fontSize();
1849 double new_font_scale = o_font_scale * drawOptions().legendFontSize / fsize;
1850 text_drawer_->setFontScale(new_font_scale);
1851 double total_width, total_height;
1852 calc_legend_height(legend_bits, total_width, total_height);
1853 if (total_height > olh) {
1854 new_font_scale *= double(olh) / total_height;
1855 text_drawer_->setFontScale(new_font_scale);
1856 calc_legend_height(legend_bits, total_width, total_height);
1857 }
1858 if (total_width > panelWidth()) {
1859 new_font_scale *= double(panelWidth()) / total_width;
1860 text_drawer_->setFontScale(new_font_scale);
1861 calc_legend_height(legend_bits, total_width, total_height);
1862 }
1863
1864 text_drawer_->setColour(drawOptions().legendColour);
1865 Point2D loc(x_offset_ + panelWidth() / 2,
1866 y_offset_ + panelHeight() - total_height);
1867 for (auto bit : legend_bits) {
1868 text_drawer_->drawString(bit, loc, TextAlignType::MIDDLE);
1869 double x_min, y_min, x_max, y_max;
1870 text_drawer_->getStringExtremes(bit, OrientType::N, x_min, y_min, x_max,
1871 y_max, true);
1872 loc.y += y_max - y_min;
1873 }
1874 text_drawer_->setMinFontSize(ominfs);
1875 text_drawer_->setFontScale(o_font_scale);
1876 }
1877
1878 legend_height_ = olh;
1879 }
1880
1881 // ****************************************************************************
drawHighlightedAtom(int atom_idx,const vector<DrawColour> & colours,const map<int,double> * highlight_radii)1882 void MolDraw2D::drawHighlightedAtom(int atom_idx,
1883 const vector<DrawColour> &colours,
1884 const map<int, double> *highlight_radii) {
1885 double xradius, yradius;
1886 Point2D centre;
1887
1888 calcLabelEllipse(atom_idx, highlight_radii, centre, xradius, yradius);
1889
1890 int orig_lw = lineWidth();
1891 bool orig_fp = fillPolys();
1892 if (!drawOptions().fillHighlights) {
1893 setLineWidth(getHighlightBondWidth(-1, nullptr));
1894 setFillPolys(false);
1895 } else {
1896 setFillPolys(true);
1897 }
1898 if (colours.size() == 1) {
1899 setColour(colours.front());
1900 Point2D offset(xradius, yradius);
1901 Point2D p1 = centre - offset;
1902 Point2D p2 = centre + offset;
1903 if (fillPolys()) {
1904 setLineWidth(1);
1905 }
1906 drawEllipse(p1, p2);
1907
1908 // drawArc(centre, xradius, yradius, 0.0, 360.0);
1909 } else {
1910 double arc_size = 360.0 / double(colours.size());
1911 double arc_start = -90.0;
1912 for (size_t i = 0; i < colours.size(); ++i) {
1913 setColour(colours[i]);
1914 drawArc(centre, xradius, yradius, arc_start, arc_start + arc_size);
1915 arc_start += arc_size;
1916 }
1917 }
1918
1919 setFillPolys(orig_fp);
1920 setLineWidth(orig_lw);
1921 }
1922
1923 // ****************************************************************************
calcLabelEllipse(int atom_idx,const map<int,double> * highlight_radii,Point2D & centre,double & xradius,double & yradius) const1924 void MolDraw2D::calcLabelEllipse(int atom_idx,
1925 const map<int, double> *highlight_radii,
1926 Point2D ¢re, double &xradius,
1927 double &yradius) const {
1928 centre = at_cds_[activeMolIdx_][atom_idx];
1929 xradius = drawOptions().highlightRadius;
1930 yradius = xradius;
1931 if (highlight_radii &&
1932 highlight_radii->find(atom_idx) != highlight_radii->end()) {
1933 xradius = highlight_radii->find(atom_idx)->second;
1934 yradius = xradius;
1935 }
1936
1937 if (drawOptions().atomHighlightsAreCircles ||
1938 atom_syms_[activeMolIdx_][atom_idx].first.empty()) {
1939 return;
1940 }
1941
1942 string atsym = atom_syms_[activeMolIdx_][atom_idx].first;
1943 OrientType orient = atom_syms_[activeMolIdx_][atom_idx].second;
1944 double x_min, y_min, x_max, y_max;
1945 getStringExtremes(atsym, orient, centre, x_min, y_min, x_max, y_max);
1946
1947 static const double root_2 = sqrt(2.0);
1948 xradius = max(xradius, root_2 * 0.5 * (x_max - x_min));
1949 yradius = max(yradius, root_2 * 0.5 * (y_max - y_min));
1950 centre.x = 0.5 * (x_max + x_min);
1951 centre.y = 0.5 * (y_max + y_min);
1952 }
1953
1954 // ****************************************************************************
calcAnnotationPosition(const ROMol & mol,const std::string & note)1955 StringRect MolDraw2D::calcAnnotationPosition(const ROMol &mol,
1956 const std::string ¬e) {
1957 RDUNUSED_PARAM(mol);
1958 StringRect note_rect;
1959 if (note.empty()) {
1960 note_rect.width_ = -1.0; // so we know it's not valid.
1961 return note_rect;
1962 }
1963
1964 vector<std::shared_ptr<StringRect>> rects;
1965 vector<TextDrawType> draw_modes;
1966 vector<char> draw_chars;
1967
1968 // at this point, the scale() should still be 1, so min and max font sizes
1969 // don't make sense, as we're effectively operating on atom coords rather
1970 // than draw.
1971 double full_font_scale = text_drawer_->fontScale();
1972 double min_fs = text_drawer_->minFontSize();
1973 text_drawer_->setMinFontSize(-1);
1974 double max_fs = text_drawer_->maxFontSize();
1975 text_drawer_->setMaxFontSize(-1);
1976 // text_drawer_->setFontScale(drawOptions().annotationFontScale *
1977 // full_font_scale);
1978 text_drawer_->setFontScale(1);
1979 text_drawer_->getStringRects(note, OrientType::N, rects, draw_modes,
1980 draw_chars);
1981 text_drawer_->setFontScale(full_font_scale);
1982 text_drawer_->setMinFontSize(min_fs);
1983 text_drawer_->setMaxFontSize(max_fs);
1984 // accumulate the widths of the rectangles so that we have the overall width
1985 for (const auto &rect : rects) {
1986 note_rect.width_ += rect->width_;
1987 }
1988
1989 Point2D centroid{0., 0.};
1990 Point2D minPt{100000., 100000.};
1991 Point2D maxPt{-100000., -100000.};
1992 for (const auto &pt : at_cds_[activeMolIdx_]) {
1993 centroid += pt;
1994 minPt.x = std::min(pt.x, minPt.x);
1995 minPt.y = std::min(pt.y, minPt.y);
1996 maxPt.x = std::max(pt.x, maxPt.x);
1997 maxPt.y = std::max(pt.y, maxPt.y);
1998 }
1999 centroid /= at_cds_[activeMolIdx_].size();
2000
2001 auto vect = maxPt - centroid;
2002 auto loc = centroid + vect * 0.9;
2003 note_rect.trans_ = loc;
2004
2005 return note_rect;
2006 }
2007
2008 // ****************************************************************************
calcAnnotationPosition(const ROMol & mol,const Atom * atom,const std::string & note)2009 StringRect MolDraw2D::calcAnnotationPosition(const ROMol &mol, const Atom *atom,
2010 const std::string ¬e) {
2011 PRECONDITION(atom, "no atom");
2012 StringRect note_rect;
2013 if (note.empty()) {
2014 note_rect.width_ = -1.0; // so we know it's not valid.
2015 return note_rect;
2016 }
2017
2018 Point2D const &at_cds = at_cds_[activeMolIdx_][atom->getIdx()];
2019 note_rect.trans_.x = at_cds.x;
2020 note_rect.trans_.y = at_cds.y;
2021 double start_ang = getNoteStartAngle(mol, atom);
2022 calcAtomAnnotationPosition(mol, atom, start_ang, note_rect, note);
2023
2024 return note_rect;
2025 }
2026
2027 // ****************************************************************************
calcAnnotationPosition(const ROMol & mol,const Bond * bond,const std::string & note)2028 StringRect MolDraw2D::calcAnnotationPosition(const ROMol &mol, const Bond *bond,
2029 const std::string ¬e) {
2030 PRECONDITION(bond, "no bond");
2031 StringRect note_rect;
2032 if (note.empty()) {
2033 note_rect.width_ = -1.0; // so we know it's not valid.
2034 return note_rect;
2035 }
2036 vector<std::shared_ptr<StringRect>> rects;
2037 vector<TextDrawType> draw_modes;
2038 vector<char> draw_chars;
2039
2040 // at this point, the scale() should still be 1, so min and max font sizes
2041 // don't make sense, as we're effectively operating on atom coords rather
2042 // than draw.
2043 double full_font_scale = text_drawer_->fontScale();
2044 double min_fs = text_drawer_->minFontSize();
2045 text_drawer_->setMinFontSize(-1);
2046 double max_fs = text_drawer_->maxFontSize();
2047 text_drawer_->setMaxFontSize(-1);
2048 text_drawer_->setFontScale(drawOptions().annotationFontScale *
2049 full_font_scale);
2050 text_drawer_->getStringRects(note, OrientType::N, rects, draw_modes,
2051 draw_chars);
2052 text_drawer_->setFontScale(full_font_scale);
2053 text_drawer_->setMinFontSize(min_fs);
2054 text_drawer_->setMaxFontSize(max_fs);
2055
2056 Point2D const &at1_cds = at_cds_[activeMolIdx_][bond->getBeginAtomIdx()];
2057 Point2D const &at2_cds = at_cds_[activeMolIdx_][bond->getEndAtomIdx()];
2058 Point2D perp = calcPerpendicular(at1_cds, at2_cds);
2059 Point2D bond_vec = at1_cds.directionVector(at2_cds);
2060 double bond_len = (at1_cds - at2_cds).length();
2061 vector<double> mid_offsets{0.5, 0.33, 0.66, 0.25, 0.75};
2062 double offset_step = drawOptions().multipleBondOffset;
2063 StringRect least_worst_rect = StringRect();
2064 least_worst_rect.clash_score_ = 100;
2065 for (auto mo : mid_offsets) {
2066 Point2D mid = at1_cds + bond_vec * bond_len * mo;
2067 for (int j = 1; j < 6; ++j) {
2068 if (j == 1 && bond->getBondType() > 1) {
2069 continue; // multiple bonds will need a bigger offset.
2070 }
2071 double offset = j * offset_step;
2072 note_rect.trans_ = mid + perp * offset;
2073 StringRect tr(note_rect);
2074 tr.trans_ =
2075 getAtomCoords(make_pair(note_rect.trans_.x, note_rect.trans_.y));
2076 tr.width_ *= scale();
2077 tr.height_ *= scale();
2078
2079 if (!doesBondNoteClash(tr, rects, mol, bond)) {
2080 return note_rect;
2081 }
2082 if (note_rect.clash_score_ < least_worst_rect.clash_score_) {
2083 least_worst_rect = note_rect;
2084 }
2085 note_rect.trans_ = mid - perp * offset;
2086 tr.trans_ =
2087 getAtomCoords(make_pair(note_rect.trans_.x, note_rect.trans_.y));
2088 if (!doesBondNoteClash(tr, rects, mol, bond)) {
2089 return note_rect;
2090 }
2091 if (note_rect.clash_score_ < least_worst_rect.clash_score_) {
2092 least_worst_rect = note_rect;
2093 }
2094 }
2095 }
2096 return least_worst_rect;
2097 }
2098
2099 // ****************************************************************************
calcAtomAnnotationPosition(const ROMol & mol,const Atom * atom,double start_ang,StringRect & rect,const std::string & note)2100 void MolDraw2D::calcAtomAnnotationPosition(const ROMol &mol, const Atom *atom,
2101 double start_ang, StringRect &rect,
2102 const std::string ¬e) {
2103 Point2D const &at_cds = at_cds_[activeMolIdx_][atom->getIdx()];
2104 auto const &atsym = atom_syms_[activeMolIdx_][atom->getIdx()];
2105
2106 vector<std::shared_ptr<StringRect>> rects;
2107 vector<TextDrawType> draw_modes;
2108 vector<char> draw_chars;
2109
2110 // at this point, the scale() should still be 1, so min and max font sizes
2111 // don't make sense, as we're effectively operating on atom coords rather
2112 // than draw.
2113 double full_font_scale = text_drawer_->fontScale();
2114 double min_fs = text_drawer_->minFontSize();
2115 text_drawer_->setMinFontSize(-1);
2116 double max_fs = text_drawer_->maxFontSize();
2117 text_drawer_->setMaxFontSize(-1);
2118 text_drawer_->setFontScale(drawOptions().annotationFontScale *
2119 full_font_scale);
2120 text_drawer_->getStringRects(note, OrientType::N, rects, draw_modes,
2121 draw_chars);
2122 text_drawer_->setFontScale(full_font_scale);
2123 text_drawer_->setMinFontSize(min_fs);
2124 text_drawer_->setMaxFontSize(max_fs);
2125
2126 double rad_step = 0.25;
2127 StringRect least_worst_rect = StringRect();
2128 least_worst_rect.clash_score_ = 100;
2129 for (int j = 1; j < 4; ++j) {
2130 double note_rad = j * rad_step;
2131 // experience suggests if there's an atom symbol, the close in
2132 // radius won't work.
2133 if (j == 1 && !atsym.first.empty()) {
2134 continue;
2135 }
2136 // scan at 30 degree intervals around the atom looking for somewhere
2137 // clear for the annotation.
2138 for (int i = 0; i < 12; ++i) {
2139 double ang = start_ang + i * 30.0 * M_PI / 180.0;
2140 rect.trans_.x = at_cds.x + cos(ang) * note_rad;
2141 rect.trans_.y = at_cds.y + sin(ang) * note_rad;
2142 // doesAtomNoteClash expects the rect to be in draw coords
2143 StringRect tr(rect);
2144 tr.trans_ = getAtomCoords(make_pair(rect.trans_.x, rect.trans_.y));
2145 tr.width_ *= scale();
2146 tr.height_ *= scale();
2147 if (!doesAtomNoteClash(tr, rects, mol, atom->getIdx())) {
2148 return;
2149 } else {
2150 if (rect.clash_score_ < least_worst_rect.clash_score_) {
2151 least_worst_rect = rect;
2152 }
2153 }
2154 }
2155 }
2156 rect = least_worst_rect;
2157 }
2158
2159 // ****************************************************************************
drawHighlightedBonds(const RDKit::ROMol & mol,const map<int,vector<DrawColour>> & highlight_bond_map,const map<int,int> & highlight_linewidth_multipliers,const map<int,double> * highlight_radii)2160 void MolDraw2D::drawHighlightedBonds(
2161 const RDKit::ROMol &mol,
2162 const map<int, vector<DrawColour>> &highlight_bond_map,
2163 const map<int, int> &highlight_linewidth_multipliers,
2164 const map<int, double> *highlight_radii) {
2165 int orig_lw = lineWidth();
2166 for (auto hb : highlight_bond_map) {
2167 int bond_idx = hb.first;
2168 if (!drawOptions().fillHighlights) {
2169 setLineWidth(
2170 getHighlightBondWidth(bond_idx, &highlight_linewidth_multipliers));
2171 }
2172 auto bond = mol.getBondWithIdx(bond_idx);
2173 int at1_idx = bond->getBeginAtomIdx();
2174 int at2_idx = bond->getEndAtomIdx();
2175 Point2D at1_cds = at_cds_[activeMolIdx_][at1_idx];
2176 Point2D at2_cds = at_cds_[activeMolIdx_][at2_idx];
2177 Point2D perp = calcPerpendicular(at1_cds, at2_cds);
2178 double rad = 0.7 * drawOptions().highlightRadius;
2179 auto draw_adjusted_line = [&](Point2D p1, Point2D p2) {
2180 adjustLineEndForHighlight(at1_idx, highlight_radii, p2, p1);
2181 adjustLineEndForHighlight(at2_idx, highlight_radii, p1, p2);
2182 bool orig_lws = drawOptions().scaleBondWidth;
2183 drawOptions().scaleBondWidth = drawOptions().scaleHighlightBondWidth;
2184 drawLine(p1, p2);
2185 drawOptions().scaleBondWidth = orig_lws;
2186 };
2187
2188 if (hb.second.size() < 2) {
2189 DrawColour col;
2190 if (hb.second.empty()) {
2191 col = drawOptions().highlightColour;
2192 } else {
2193 col = hb.second.front();
2194 }
2195 setColour(col);
2196 if (drawOptions().fillHighlights) {
2197 vector<Point2D> line_pts;
2198 line_pts.emplace_back(at1_cds + perp * rad);
2199 line_pts.emplace_back(at2_cds + perp * rad);
2200 line_pts.emplace_back(at2_cds - perp * rad);
2201 line_pts.emplace_back(at1_cds - perp * rad);
2202 drawPolygon(line_pts);
2203 } else {
2204 draw_adjusted_line(at1_cds + perp * rad, at2_cds + perp * rad);
2205 draw_adjusted_line(at1_cds - perp * rad, at2_cds - perp * rad);
2206 }
2207 } else {
2208 double col_rad = 2.0 * rad / hb.second.size();
2209 if (drawOptions().fillHighlights) {
2210 Point2D p1 = at1_cds - perp * rad;
2211 Point2D p2 = at2_cds - perp * rad;
2212 vector<Point2D> line_pts;
2213 for (size_t i = 0; i < hb.second.size(); ++i) {
2214 setColour(hb.second[i]);
2215 line_pts.clear();
2216 line_pts.emplace_back(p1);
2217 line_pts.emplace_back(p1 + perp * col_rad);
2218 line_pts.emplace_back(p2 + perp * col_rad);
2219 line_pts.emplace_back(p2);
2220 drawPolygon(line_pts);
2221 p1 += perp * col_rad;
2222 p2 += perp * col_rad;
2223 }
2224 } else {
2225 int step = 0;
2226 for (size_t i = 0; i < hb.second.size(); ++i) {
2227 setColour(hb.second[i]);
2228 // draw even numbers from the bottom, odd from the top
2229 Point2D offset = perp * (rad - step * col_rad);
2230 if (!(i % 2)) {
2231 draw_adjusted_line(at1_cds - offset, at2_cds - offset);
2232 } else {
2233 draw_adjusted_line(at1_cds + offset, at2_cds + offset);
2234 step++;
2235 }
2236 }
2237 }
2238 }
2239 setLineWidth(orig_lw);
2240 }
2241 }
2242
2243 // ****************************************************************************
getHighlightBondWidth(int bond_idx,const map<int,int> * highlight_linewidth_multipliers) const2244 int MolDraw2D::getHighlightBondWidth(
2245 int bond_idx, const map<int, int> *highlight_linewidth_multipliers) const {
2246 int bwm = drawOptions().highlightBondWidthMultiplier;
2247 // if we're not doing filled highlights, the lines need to be narrower
2248 if (!drawOptions().fillHighlights) {
2249 bwm /= 2;
2250 if (bwm < 1) {
2251 bwm = 1;
2252 }
2253 }
2254
2255 if (highlight_linewidth_multipliers &&
2256 !highlight_linewidth_multipliers->empty()) {
2257 auto it = highlight_linewidth_multipliers->find(bond_idx);
2258 if (it != highlight_linewidth_multipliers->end()) {
2259 bwm = it->second;
2260 }
2261 }
2262 int tgt_lw = lineWidth() * bwm;
2263 return tgt_lw;
2264 }
2265
2266 // ****************************************************************************
adjustLineEndForHighlight(int at_idx,const map<int,double> * highlight_radii,Point2D p1,Point2D & p2) const2267 void MolDraw2D::adjustLineEndForHighlight(
2268 int at_idx, const map<int, double> *highlight_radii, Point2D p1,
2269 Point2D &p2) const {
2270 // this code is transliterated from
2271 // http://csharphelper.com/blog/2017/08/calculate-where-a-line-segment-and-an-ellipse-intersect-in-c/
2272 // which has it in C#
2273 double xradius, yradius;
2274 Point2D centre;
2275 calcLabelEllipse(at_idx, highlight_radii, centre, xradius, yradius);
2276 // cout << "ellipse is : " << centre.x << ", " << centre.y << " rads " <<
2277 // xradius << " and " << yradius << endl; cout << "p1 = " << p1.x << ", " <<
2278 // p1.y << endl << "p2 = " << p2.x << ", " << p2.y << endl;
2279 if (xradius < 1.0e-6 || yradius < 1.0e-6) {
2280 return;
2281 }
2282
2283 // move everything so the ellipse is centred on the origin.
2284 p1 -= centre;
2285 p2 -= centre;
2286 double a2 = xradius * xradius;
2287 double b2 = yradius * yradius;
2288 double A =
2289 (p2.x - p1.x) * (p2.x - p1.x) / a2 + (p2.y - p1.y) * (p2.y - p1.y) / b2;
2290 double B = 2.0 * p1.x * (p2.x - p1.x) / a2 + 2.0 * p1.y * (p2.y - p1.y) / b2;
2291 double C = p1.x * p1.x / a2 + p1.y * p1.y / b2 - 1.0;
2292
2293 auto t_to_point = [&](double t) -> Point2D {
2294 Point2D ret_val;
2295 ret_val.x = p1.x + (p2.x - p1.x) * t + centre.x;
2296 ret_val.y = p1.y + (p2.y - p1.y) * t + centre.y;
2297 return ret_val;
2298 };
2299
2300 double disc = B * B - 4.0 * A * C;
2301 if (disc < 0.0) {
2302 // no solutions, leave things as they are. Bit crap, though.
2303 return;
2304 } else if (fabs(disc) < 1.0e-6) {
2305 // 1 solution
2306 double t = -B / (2.0 * A);
2307 // cout << "t = " << t << endl;
2308 p2 = t_to_point(t);
2309 } else {
2310 // 2 solutions - take the one nearest p1.
2311 double disc_rt = sqrt(disc);
2312 double t1 = (-B + disc_rt) / (2.0 * A);
2313 double t2 = (-B - disc_rt) / (2.0 * A);
2314 // cout << "t1 = " << t1 << " t2 = " << t2 << endl;
2315 double t;
2316 // prefer the t between 0 and 1, as that must be between the original
2317 // points. If both are, prefer the lower, as that will be nearest p1,
2318 // so on the bit of the ellipse the line comes to first.
2319 bool t1_ok = (t1 >= 0.0 && t1 <= 1.0);
2320 bool t2_ok = (t2 >= 0.0 && t2 <= 1.0);
2321 if (t1_ok && !t2_ok) {
2322 t = t1;
2323 } else if (t2_ok && !t1_ok) {
2324 t = t2;
2325 } else if (t1_ok && t2_ok) {
2326 t = min(t1, t2);
2327 } else {
2328 // the intersections are both outside the line between p1 and p2
2329 // so don't do anything.
2330 return;
2331 }
2332 // cout << "using t = " << t << endl;
2333 p2 = t_to_point(t);
2334 }
2335 // cout << "p2 = " << p2.x << ", " << p2.y << endl;
2336 }
2337
2338 // ****************************************************************************
extractAtomCoords(const ROMol & mol,int confId,bool updateBBox)2339 void MolDraw2D::extractAtomCoords(const ROMol &mol, int confId,
2340 bool updateBBox) {
2341 PRECONDITION(activeMolIdx_ >= 0, "no mol id");
2342 PRECONDITION(static_cast<int>(at_cds_.size()) > activeMolIdx_, "no space");
2343 PRECONDITION(static_cast<int>(atomic_nums_.size()) > activeMolIdx_,
2344 "no space");
2345 PRECONDITION(static_cast<int>(mol.getNumConformers()) > 0, "no coords");
2346
2347 if (updateBBox) {
2348 bbox_[0].x = bbox_[0].y = numeric_limits<double>::max();
2349 bbox_[1].x = bbox_[1].y = -1 * numeric_limits<double>::max();
2350 }
2351 const RDGeom::POINT3D_VECT &locs = mol.getConformer(confId).getPositions();
2352
2353 // the transformation rotates anti-clockwise, as is conventional, but
2354 // probably not what our user expects.
2355 double rot = -drawOptions().rotate * M_PI / 180.0;
2356 // assuming that if drawOptions().rotate is set to 0.0, rot will be
2357 // exactly 0.0 without worrying about floating point number dust. Does
2358 // anyone know if this is true? It's not the end of the world if not,
2359 // as it's just an extra largely pointless rotation.
2360 // Floating point numbers are like piles of sand; every time you move
2361 // them around, you lose a little sand and pick up a little dirt.
2362 // — Brian Kernighan and P.J. Plauger
2363 // Nothing brings fear to my heart more than a floating point number.
2364 // — Gerald Jay Sussman
2365 // Some developers, when encountering a problem, say: “I know, I’ll
2366 // use floating-point numbers!” Now, they have 1.9999999997 problems.
2367 // — unknown
2368 RDGeom::Transform2D trans;
2369 trans.SetTransform(Point2D(0.0, 0.0), rot);
2370 at_cds_[activeMolIdx_].clear();
2371 for (auto this_at : mol.atoms()) {
2372 int this_idx = this_at->getIdx();
2373 Point2D pt(locs[this_idx].x, locs[this_idx].y);
2374 if (rot != 0.0) {
2375 trans.TransformPoint(pt);
2376 }
2377 at_cds_[activeMolIdx_].emplace_back(pt);
2378
2379 if (updateBBox) {
2380 bbox_[0].x = std::min(bbox_[0].x, pt.x);
2381 bbox_[0].y = std::min(bbox_[0].y, pt.y);
2382 bbox_[1].x = std::max(bbox_[1].x, pt.x);
2383 bbox_[1].y = std::max(bbox_[1].y, pt.y);
2384 }
2385 }
2386 }
2387
2388 // ****************************************************************************
extractAtomSymbols(const ROMol & mol)2389 void MolDraw2D::extractAtomSymbols(const ROMol &mol) {
2390 PRECONDITION(activeMolIdx_ >= 0, "no mol id");
2391 PRECONDITION(static_cast<int>(atom_syms_.size()) > activeMolIdx_, "no space");
2392 PRECONDITION(static_cast<int>(atomic_nums_.size()) > activeMolIdx_,
2393 "no space");
2394
2395 atomic_nums_[activeMolIdx_].clear();
2396 for (auto at1 : mol.atoms()) {
2397 atom_syms_[activeMolIdx_].emplace_back(getAtomSymbolAndOrientation(*at1));
2398 if (!isComplexQuery(at1)) {
2399 atomic_nums_[activeMolIdx_].emplace_back(at1->getAtomicNum());
2400 } else {
2401 atomic_nums_[activeMolIdx_].push_back(0);
2402 }
2403 }
2404 }
2405
2406 // ****************************************************************************
extractAtomNotes(const ROMol & mol)2407 void MolDraw2D::extractAtomNotes(const ROMol &mol) {
2408 PRECONDITION(activeMolIdx_ >= 0, "no mol id");
2409 PRECONDITION(static_cast<int>(annotations_.size()) > activeMolIdx_,
2410 "no space");
2411
2412 for (auto atom : mol.atoms()) {
2413 std::string note;
2414 if (atom->getPropIfPresent(common_properties::atomNote, note)) {
2415 if (!note.empty()) {
2416 auto note_rect = calcAnnotationPosition(mol, atom, note);
2417 if (note_rect.width_ < 0.0) {
2418 cerr << "Couldn't find good place for note " << note << " for atom "
2419 << atom->getIdx() << endl;
2420 } else {
2421 AnnotationType annot;
2422 annot.text_ = note;
2423 annot.rect_ = note_rect;
2424 annotations_[activeMolIdx_].push_back(annot);
2425 }
2426 }
2427 }
2428 }
2429 }
2430
2431 // ****************************************************************************
extractMolNotes(const ROMol & mol)2432 void MolDraw2D::extractMolNotes(const ROMol &mol) {
2433 PRECONDITION(activeMolIdx_ >= 0, "no mol id");
2434 PRECONDITION(static_cast<int>(annotations_.size()) > activeMolIdx_,
2435 "no space");
2436
2437 std::string note;
2438 // the molNote property takes priority
2439 if (!mol.getPropIfPresent(common_properties::molNote, note)) {
2440 unsigned int chiralFlag;
2441 if (drawOptions().includeChiralFlagLabel &&
2442 mol.getPropIfPresent(common_properties::_MolFileChiralFlag,
2443 chiralFlag) &&
2444 chiralFlag) {
2445 note = "ABS";
2446 }
2447 }
2448
2449 if (!note.empty()) {
2450 auto note_rect = calcAnnotationPosition(mol, note);
2451 if (note_rect.width_ < 0.0) {
2452 cerr << "Couldn't find good place for molecule note " << note << endl;
2453 } else {
2454 AnnotationType annot;
2455 annot.text_ = note;
2456 annot.rect_ = note_rect;
2457 annot.align_ = TextAlignType::START;
2458 annot.scaleText_ = false;
2459 annotations_[activeMolIdx_].push_back(annot);
2460 }
2461 }
2462 }
2463
2464 // ****************************************************************************
extractBondNotes(const ROMol & mol)2465 void MolDraw2D::extractBondNotes(const ROMol &mol) {
2466 PRECONDITION(activeMolIdx_ >= 0, "no mol id");
2467 PRECONDITION(static_cast<int>(annotations_.size()) > activeMolIdx_,
2468 "no space");
2469
2470 for (auto bond : mol.bonds()) {
2471 std::string note;
2472 if (bond->getPropIfPresent(common_properties::bondNote, note)) {
2473 if (!note.empty()) {
2474 auto note_rect = calcAnnotationPosition(mol, bond, note);
2475 if (note_rect.width_ < 0.0) {
2476 cerr << "Couldn't find good place for note " << note << " for bond "
2477 << bond->getIdx() << endl;
2478 } else {
2479 AnnotationType annot;
2480 annot.text_ = note;
2481 annot.rect_ = note_rect;
2482 annotations_[activeMolIdx_].push_back(annot);
2483 }
2484 }
2485 }
2486 }
2487 }
2488
2489 // ****************************************************************************
extractRadicals(const ROMol & mol)2490 void MolDraw2D::extractRadicals(const ROMol &mol) {
2491 PRECONDITION(activeMolIdx_ >= 0, "no mol id");
2492 PRECONDITION(static_cast<int>(radicals_.size()) > activeMolIdx_, "no space");
2493
2494 for (auto atom : mol.atoms()) {
2495 if (!atom->getNumRadicalElectrons()) {
2496 continue;
2497 }
2498 std::shared_ptr<StringRect> rad_rect(new StringRect);
2499 OrientType orient = calcRadicalRect(mol, atom, *rad_rect);
2500 radicals_[activeMolIdx_].push_back(make_pair(rad_rect, orient));
2501 }
2502 }
2503
2504 // ****************************************************************************
extractLinkNodes(const ROMol & mol)2505 void MolDraw2D::extractLinkNodes(const ROMol &mol) {
2506 PRECONDITION(activeMolIdx_ >= 0, "no mol id");
2507 PRECONDITION(static_cast<int>(post_shapes_.size()) > activeMolIdx_,
2508 "no space");
2509 PRECONDITION(static_cast<int>(annotations_.size()) > activeMolIdx_,
2510 "no space");
2511 if (!mol.hasProp(common_properties::molFileLinkNodes)) {
2512 return;
2513 }
2514
2515 bool strict = false;
2516 auto linkNodes = MolEnumerator::utils::getMolLinkNodes(mol, strict);
2517 for (const auto &node : linkNodes) {
2518 const double crossingFrac = 0.333;
2519 const double lengthFrac = 0.333;
2520 Point2D labelPt{-1000, -1000};
2521 Point2D labelPerp{0, 0};
2522 for (const auto &bAts : node.bondAtoms) {
2523 // unlike brackets, we know how these point
2524 Point2D startLoc = at_cds_[activeMolIdx_][bAts.first];
2525 Point2D endLoc = at_cds_[activeMolIdx_][bAts.second];
2526 auto vect = endLoc - startLoc;
2527 auto offset = vect * crossingFrac;
2528 auto crossingPt = startLoc + offset;
2529 Point2D perp{vect.y, -vect.x};
2530 perp *= lengthFrac;
2531 Point2D p1 = crossingPt + perp / 2.;
2532 Point2D p2 = crossingPt - perp / 2.;
2533
2534 std::vector<std::pair<Point2D, Point2D>> bondSegments; // not needed here
2535 MolDrawShape shp;
2536 shp.points =
2537 MolDraw2D_detail::getBracketPoints(p1, p2, startLoc, bondSegments);
2538 shp.shapeType = MolDrawShapeType::Polyline;
2539 post_shapes_[activeMolIdx_].emplace_back(std::move(shp));
2540
2541 if (p1.x > labelPt.x) {
2542 labelPt = p1;
2543 labelPerp = crossingPt - startLoc;
2544 }
2545 if (p2.x > labelPt.x) {
2546 labelPt = p2;
2547 labelPerp = crossingPt - startLoc;
2548 }
2549 }
2550
2551 // the label
2552 if (supportsAnnotations()) {
2553 std::string label =
2554 (boost::format("(%d-%d)") % node.minRep % node.maxRep).str();
2555 StringRect rect;
2556 Point2D perp = labelPerp;
2557 perp /= perp.length() * 5;
2558 rect.trans_ = labelPt + perp;
2559 AnnotationType annot;
2560 annot.text_ = label;
2561 annot.rect_ = rect;
2562 annot.align_ = TextAlignType::START;
2563 annotations_[activeMolIdx_].push_back(annot);
2564 }
2565 }
2566 }
2567
2568 // ****************************************************************************
extractBrackets(const ROMol & mol)2569 void MolDraw2D::extractBrackets(const ROMol &mol) {
2570 PRECONDITION(activeMolIdx_ >= 0, "no mol id");
2571 PRECONDITION(static_cast<int>(post_shapes_.size()) > activeMolIdx_,
2572 "no space");
2573 PRECONDITION(static_cast<int>(annotations_.size()) > activeMolIdx_,
2574 "no space");
2575 auto &sgs = getSubstanceGroups(mol);
2576 if (sgs.empty()) {
2577 return;
2578 }
2579 // details of this transformation are in extractAtomCoords
2580 double rot = -drawOptions().rotate * M_PI / 180.0;
2581 RDGeom::Transform2D trans;
2582 trans.SetTransform(Point2D(0.0, 0.0), rot);
2583 for (auto &sg : sgs) {
2584 if (sg.getBrackets().empty()) {
2585 continue;
2586 }
2587 // figure out the location of the reference point we'll use to figure out
2588 // which direction the bracket points
2589 // Thanks to John Mayfield for the thoughts on the best way to do this:
2590 // http://efficientbits.blogspot.com/2015/11/bringing-molfile-sgroups-to-cdk.html
2591 Point2D refPt{0., 0.};
2592 if (!sg.getAtoms().empty()) {
2593 // use the average position of the atoms in the sgroup
2594 for (auto aidx : sg.getAtoms()) {
2595 refPt += at_cds_[activeMolIdx_][aidx];
2596 }
2597 refPt /= sg.getAtoms().size();
2598 }
2599
2600 std::vector<std::pair<Point2D, Point2D>> sgBondSegments;
2601 for (auto bndIdx : sg.getBonds()) {
2602 const auto bnd = mol.getBondWithIdx(bndIdx);
2603 if (std::find(sg.getAtoms().begin(), sg.getAtoms().end(),
2604 bnd->getBeginAtomIdx()) != sg.getAtoms().end()) {
2605 sgBondSegments.push_back(
2606 std::make_pair(at_cds_[activeMolIdx_][bnd->getBeginAtomIdx()],
2607 at_cds_[activeMolIdx_][bnd->getEndAtomIdx()]));
2608
2609 } else if (std::find(sg.getAtoms().begin(), sg.getAtoms().end(),
2610 bnd->getEndAtomIdx()) != sg.getAtoms().end()) {
2611 sgBondSegments.push_back(
2612 std::make_pair(at_cds_[activeMolIdx_][bnd->getEndAtomIdx()],
2613 at_cds_[activeMolIdx_][bnd->getBeginAtomIdx()]));
2614 }
2615 }
2616 for (const auto &brk : sg.getBrackets()) {
2617 Point2D p1{brk[0]};
2618 Point2D p2{brk[1]};
2619 trans.TransformPoint(p1);
2620 trans.TransformPoint(p2);
2621 MolDrawShape shp;
2622 shp.points =
2623 MolDraw2D_detail::getBracketPoints(p1, p2, refPt, sgBondSegments);
2624 shp.shapeType = MolDrawShapeType::Polyline;
2625 post_shapes_[activeMolIdx_].emplace_back(std::move(shp));
2626 }
2627 if (supportsAnnotations()) {
2628 // FIX: we could imagine changing this to always show the annotations on
2629 // the right-most (or bottom-most) bracket
2630
2631 std::string connect;
2632 if (sg.getPropIfPresent("CONNECT", connect)) {
2633 // annotations go on the last bracket of an sgroup
2634 const auto &brkShp = post_shapes_[activeMolIdx_].back();
2635 StringRect rect;
2636 // CONNECT goes at the top
2637 auto topPt = brkShp.points[1];
2638 auto brkPt = brkShp.points[0];
2639 if (brkShp.points[2].y > topPt.y) {
2640 topPt = brkShp.points[2];
2641 brkPt = brkShp.points[3];
2642 }
2643 rect.trans_ = topPt + (topPt - brkPt);
2644 AnnotationType annot;
2645 annot.text_ = connect;
2646 annot.rect_ = rect;
2647 // if we're to the right of the bracket, we need to left justify,
2648 // otherwise things seem to work as is
2649 if (brkPt.x < topPt.x) {
2650 annot.align_ = TextAlignType::START;
2651 }
2652 annotations_[activeMolIdx_].push_back(annot);
2653 }
2654 std::string label;
2655 if (sg.getPropIfPresent("LABEL", label)) {
2656 // annotations go on the last bracket of an sgroup
2657 const auto &brkShp = post_shapes_[activeMolIdx_].back();
2658 StringRect rect;
2659 // LABEL goes at the bottom
2660 auto botPt = brkShp.points[2];
2661 auto brkPt = brkShp.points[3];
2662 if (brkShp.points[1].y < botPt.y) {
2663 botPt = brkShp.points[1];
2664 brkPt = brkShp.points[0];
2665 }
2666 rect.trans_ = botPt + (botPt - brkPt);
2667 AnnotationType annot;
2668 annot.text_ = label;
2669 annot.rect_ = rect;
2670 annotations_[activeMolIdx_].push_back(annot);
2671 }
2672 }
2673 }
2674 }
2675
2676 // ****************************************************************************
extractSGroupData(const ROMol & mol)2677 void MolDraw2D::extractSGroupData(const ROMol &mol) {
2678 PRECONDITION(activeMolIdx_ >= 0, "no mol id");
2679 PRECONDITION(static_cast<int>(annotations_.size()) > activeMolIdx_,
2680 "no space");
2681 if (!supportsAnnotations()) {
2682 return;
2683 }
2684 auto &sgs = getSubstanceGroups(mol);
2685 if (sgs.empty()) {
2686 return;
2687 }
2688
2689 // details of this transformation are in extractAtomCoords
2690 double rot = -drawOptions().rotate * M_PI / 180.0;
2691 RDGeom::Transform2D tform;
2692 tform.SetTransform(Point2D(0.0, 0.0), rot);
2693
2694 for (const auto &sg : sgs) {
2695 std::string typ;
2696 if (sg.getPropIfPresent("TYPE", typ) && typ == "DAT") {
2697 std::string text;
2698 // it seems like we should be rendering FIELDNAME, but
2699 // Marvin Sketch, Biovia Draw, and ChemDraw don't do it
2700 // if (sg.getPropIfPresent("FIELDNAME", text)) {
2701 // text += "=";
2702 // };
2703 if (sg.hasProp("DATAFIELDS")) {
2704 STR_VECT dfs = sg.getProp<STR_VECT>("DATAFIELDS");
2705 for (const auto &df : dfs) {
2706 text += df + "|";
2707 }
2708 text.pop_back();
2709 }
2710 if (text.empty()) {
2711 continue;
2712 }
2713 int atomIdx = -1;
2714 if (!sg.getAtoms().empty()) {
2715 atomIdx = sg.getAtoms()[0];
2716 };
2717 StringRect rect;
2718 std::string fieldDisp;
2719 if (sg.getPropIfPresent("FIELDDISP", fieldDisp)) {
2720 double xp = FileParserUtils::stripSpacesAndCast<double>(
2721 fieldDisp.substr(3, 10));
2722 double yp = FileParserUtils::stripSpacesAndCast<double>(
2723 fieldDisp.substr(13, 10));
2724 Point2D origLoc{xp, yp};
2725
2726 if (fieldDisp[25] == 'R') {
2727 origLoc += mol.getConformer().getAtomPos(atomIdx);
2728 } else {
2729 if (mol.hasProp("_centroidx")) {
2730 Point2D centroid;
2731 mol.getProp("_centroidx", centroid.x);
2732 mol.getProp("_centroidy", centroid.y);
2733 origLoc += centroid;
2734 }
2735 }
2736 tform.TransformPoint(origLoc);
2737 rect.trans_ = origLoc;
2738 } else if (atomIdx >= 0) {
2739 rect = calcAnnotationPosition(mol, mol.getAtomWithIdx(atomIdx), text);
2740 } else {
2741 BOOST_LOG(rdWarningLog)
2742 << "FIELDDISP info not found for DAT SGroup which isn't associated "
2743 "with an atom. SGroup will not be rendered."
2744 << std::endl;
2745 text = "";
2746 }
2747 if (!text.empty()) {
2748 AnnotationType annot;
2749 annot.text_ = text;
2750 annot.rect_ = rect;
2751 // looks like everybody renders these left justified
2752 annot.align_ = TextAlignType::START;
2753 annotations_[activeMolIdx_].push_back(annot);
2754 }
2755 }
2756 }
2757 }
2758
2759 // ****************************************************************************
extractVariableBonds(const ROMol & mol)2760 void MolDraw2D::extractVariableBonds(const ROMol &mol) {
2761 PRECONDITION(activeMolIdx_ >= 0, "no mol id");
2762 PRECONDITION(static_cast<int>(pre_shapes_.size()) > activeMolIdx_,
2763 "no space");
2764 PRECONDITION(static_cast<int>(annotations_.size()) > activeMolIdx_,
2765 "no space");
2766
2767 boost::dynamic_bitset<> atomsInvolved(mol.getNumAtoms());
2768 for (const auto bond : mol.bonds()) {
2769 std::string endpts;
2770 std::string attach;
2771 if (bond->getPropIfPresent(common_properties::_MolFileBondEndPts, endpts) &&
2772 bond->getPropIfPresent(common_properties::_MolFileBondAttach, attach)) {
2773 // FIX: maybe distinguish between "ANY" and "ALL" values of attach here?
2774 std::vector<unsigned int> oats =
2775 RDKit::SGroupParsing::ParseV3000Array<unsigned int>(endpts);
2776 atomsInvolved.reset();
2777 // decrement the indices and do error checking:
2778 for (auto &oat : oats) {
2779 if (oat == 0 || oat > mol.getNumAtoms()) {
2780 throw ValueErrorException("Bad variation point index");
2781 }
2782 --oat;
2783 atomsInvolved.set(oat);
2784 MolDrawShape shp;
2785 shp.shapeType = MolDrawShapeType::Ellipse;
2786 shp.lineWidth = 1;
2787 shp.lineColour = drawOptions().variableAttachmentColour;
2788 shp.fill = true;
2789 auto center = at_cds_[activeMolIdx_][oat];
2790 Point2D offset{drawOptions().variableAtomRadius,
2791 drawOptions().variableAtomRadius};
2792 shp.points = {center + offset, center - offset};
2793 pre_shapes_[activeMolIdx_].emplace_back(std::move(shp));
2794 }
2795
2796 for (const auto bond : mol.bonds()) {
2797 if (atomsInvolved[bond->getBeginAtomIdx()] &&
2798 atomsInvolved[bond->getEndAtomIdx()]) {
2799 MolDrawShape shp;
2800 shp.shapeType = MolDrawShapeType::Polyline;
2801 shp.lineWidth =
2802 lineWidth() * drawOptions().variableBondWidthMultiplier;
2803 shp.scaleLineWidth = true;
2804 shp.lineColour = drawOptions().variableAttachmentColour;
2805 shp.fill = false;
2806 shp.points = {at_cds_[activeMolIdx_][bond->getBeginAtomIdx()],
2807 at_cds_[activeMolIdx_][bond->getEndAtomIdx()]};
2808 pre_shapes_[activeMolIdx_].emplace_back(std::move(shp));
2809 }
2810 }
2811 // correct the symbol of the end atom (remove the *):
2812 if (!bond->getBeginAtom()->getAtomicNum()) {
2813 atom_syms_[activeMolIdx_][bond->getBeginAtomIdx()] =
2814 std::make_pair("", OrientType::C);
2815 }
2816 }
2817 }
2818 }
2819
2820 namespace {
2821 const DashPattern noDash;
2822 const DashPattern dots = assign::list_of(2)(6);
2823 const DashPattern dashes = assign::list_of(6)(6);
2824 const DashPattern shortDashes = assign::list_of(2)(2);
2825
2826 // ****************************************************************************
drawWedgedBond(MolDraw2D & d2d,const Bond & bond,bool inverted,const Point2D & cds1,const Point2D & cds2,bool draw_dashed,const DrawColour & col1,const DrawColour & col2)2827 void drawWedgedBond(MolDraw2D &d2d, const Bond &bond, bool inverted,
2828 const Point2D &cds1, const Point2D &cds2, bool draw_dashed,
2829 const DrawColour &col1, const DrawColour &col2) {
2830 if (!d2d.drawOptions().splitBonds) {
2831 if (inverted) {
2832 d2d.setActiveAtmIdx(bond.getEndAtomIdx(), bond.getBeginAtomIdx());
2833 } else {
2834 d2d.setActiveAtmIdx(bond.getBeginAtomIdx(), bond.getEndAtomIdx());
2835 }
2836 }
2837
2838 Point2D perp = calcPerpendicular(cds1, cds2);
2839 Point2D disp = perp * 0.15;
2840 // make sure the displacement isn't too large using the current scale factor
2841 // (part of github #985)
2842 // the constants are empirical to make sure that the wedge is visible, but
2843 // not absurdly large.
2844 if (d2d.scale() > 40) {
2845 disp *= .6;
2846 }
2847 Point2D end1 = cds2 + disp;
2848 Point2D end2 = cds2 - disp;
2849
2850 d2d.setColour(col1);
2851 if (draw_dashed) {
2852 d2d.setFillPolys(false);
2853
2854 unsigned int nDashes;
2855 // empirical cutoff to make sure we don't have too many dashes in the
2856 // wedge:
2857 auto factor = d2d.scale() * (cds1 - cds2).lengthSq();
2858 if (factor < 20) {
2859 nDashes = 3;
2860 } else if (factor < 30) {
2861 nDashes = 4;
2862 } else if (factor < 45) {
2863 nDashes = 5;
2864 } else {
2865 nDashes = 6;
2866 }
2867
2868 int orig_lw = d2d.lineWidth();
2869 int tgt_lw = 1; // use the minimum line width
2870 d2d.setLineWidth(tgt_lw);
2871
2872 if (d2d.drawOptions().splitBonds) {
2873 d2d.setActiveAtmIdx(inverted ? bond.getEndAtomIdx()
2874 : bond.getBeginAtomIdx());
2875 }
2876 Point2D e1 = end1 - cds1;
2877 Point2D e2 = end2 - cds1;
2878 for (unsigned int i = 1; i < nDashes + 1; ++i) {
2879 if ((nDashes / 2 + 1) == i) {
2880 d2d.setColour(col2);
2881 if (d2d.drawOptions().splitBonds) {
2882 d2d.setActiveAtmIdx(inverted ? bond.getBeginAtomIdx()
2883 : bond.getEndAtomIdx());
2884 }
2885 }
2886 Point2D e11 = cds1 + e1 * (rdcast<double>(i) / nDashes);
2887 Point2D e22 = cds1 + e2 * (rdcast<double>(i) / nDashes);
2888 if (d2d.drawOptions().comicMode) {
2889 auto pts = MolDraw2D_detail::handdrawnLine(e11, e22, d2d.scale());
2890 d2d.drawPolygon(pts);
2891 } else {
2892 d2d.drawLine(e11, e22);
2893 }
2894 }
2895 d2d.setLineWidth(orig_lw);
2896 } else {
2897 d2d.setFillPolys(true);
2898 if (col1 == col2 && !d2d.drawOptions().splitBonds) {
2899 d2d.drawTriangle(cds1, end1, end2);
2900 } else {
2901 if (d2d.drawOptions().splitBonds) {
2902 d2d.setActiveAtmIdx(inverted ? bond.getEndAtomIdx()
2903 : bond.getBeginAtomIdx());
2904 }
2905 Point2D e1 = end1 - cds1;
2906 Point2D e2 = end2 - cds1;
2907 Point2D mid1 = cds1 + e1 * 0.5;
2908 Point2D mid2 = cds1 + e2 * 0.5;
2909 d2d.drawTriangle(cds1, mid1, mid2);
2910 if (d2d.drawOptions().splitBonds) {
2911 d2d.setActiveAtmIdx(inverted ? bond.getBeginAtomIdx()
2912 : bond.getEndAtomIdx());
2913 }
2914 d2d.setColour(col2);
2915 d2d.drawTriangle(mid1, end2, end1);
2916 d2d.drawTriangle(mid1, mid2, end2);
2917 }
2918 }
2919 d2d.setActiveAtmIdx();
2920 }
2921
2922 // ****************************************************************************
drawDativeBond(MolDraw2D & d2d,const Bond & bond,const Point2D & cds1,const Point2D & cds2,const DrawColour & col1,const DrawColour & col2)2923 void drawDativeBond(MolDraw2D &d2d, const Bond &bond, const Point2D &cds1,
2924 const Point2D &cds2, const DrawColour &col1,
2925 const DrawColour &col2) {
2926 if (!d2d.drawOptions().splitBonds) {
2927 d2d.setActiveAtmIdx(bond.getBeginAtomIdx(), bond.getEndAtomIdx());
2928 } else {
2929 d2d.setActiveAtmIdx(bond.getBeginAtomIdx());
2930 }
2931
2932 Point2D mid = (cds1 + cds2) * 0.5;
2933 d2d.drawLine(cds1, mid, col1, col1);
2934
2935 if (d2d.drawOptions().splitBonds) {
2936 d2d.setActiveAtmIdx(bond.getEndAtomIdx());
2937 }
2938 d2d.setColour(col2);
2939 bool asPolygon = true;
2940 double frac = 0.2;
2941 double angle = M_PI / 6;
2942 // the polygon triangle at the end extends past cds2, so step back a bit
2943 // so as not to trample on anything else.
2944 Point2D delta = mid - cds2;
2945 Point2D end = cds2 + delta * frac;
2946 d2d.drawArrow(mid, end, asPolygon, frac, angle);
2947 d2d.setActiveAtmIdx();
2948 }
2949
drawBondLine(MolDraw2D & d2d,const Bond & bond,const Point2D & cds1,const Point2D & cds2,const DrawColour & col1,const DrawColour & col2,bool clearAIdx=true)2950 void drawBondLine(MolDraw2D &d2d, const Bond &bond, const Point2D &cds1,
2951 const Point2D &cds2, const DrawColour &col1,
2952 const DrawColour &col2, bool clearAIdx = true) {
2953 if (!d2d.drawOptions().splitBonds) {
2954 d2d.setActiveAtmIdx(bond.getBeginAtomIdx(), bond.getEndAtomIdx());
2955 d2d.drawLine(cds1, cds2, col1, col2);
2956 if (clearAIdx) {
2957 d2d.setActiveAtmIdx();
2958 }
2959 return;
2960 }
2961 Point2D mid = (cds1 + cds2) * 0.5;
2962 d2d.setActiveAtmIdx(bond.getBeginAtomIdx());
2963 d2d.drawLine(cds1, mid, col1, col1);
2964 d2d.setActiveAtmIdx(bond.getEndAtomIdx());
2965 d2d.drawLine(mid, cds2, col2, col2);
2966 if (clearAIdx) {
2967 d2d.setActiveAtmIdx();
2968 }
2969 }
2970
drawBondLine(MolDraw2D & d2d,const Bond & bond,const Point2D & cds1,const Point2D & cds2,bool clearAIdx=true)2971 void drawBondLine(MolDraw2D &d2d, const Bond &bond, const Point2D &cds1,
2972 const Point2D &cds2, bool clearAIdx = true) {
2973 if (!d2d.drawOptions().splitBonds) {
2974 d2d.drawLine(cds1, cds2);
2975 if (clearAIdx) {
2976 d2d.setActiveAtmIdx();
2977 }
2978 return;
2979 }
2980 const auto midp = (cds1 + cds2) / 2;
2981 d2d.setActiveAtmIdx(bond.getBeginAtomIdx());
2982 d2d.drawLine(cds1, midp);
2983 d2d.setActiveAtmIdx(bond.getEndAtomIdx());
2984 d2d.drawLine(midp, cds2);
2985 if (clearAIdx) {
2986 d2d.setActiveAtmIdx();
2987 }
2988 }
2989
drawBondWavyLine(MolDraw2D & d2d,const Bond & bond,const Point2D & cds1,const Point2D & cds2,const DrawColour & col1,const DrawColour & col2)2990 void drawBondWavyLine(MolDraw2D &d2d, const Bond &bond, const Point2D &cds1,
2991 const Point2D &cds2, const DrawColour &col1,
2992 const DrawColour &col2) {
2993 // as splitting wavy line might cause rendering problems
2994 // do not split and flag wavy bond with both atoms
2995 d2d.setActiveAtmIdx(bond.getBeginAtomIdx(), bond.getEndAtomIdx());
2996 d2d.drawWavyLine(cds1, cds2, col1, col2);
2997 d2d.setActiveAtmIdx();
2998 }
2999
drawNormalBond(MolDraw2D & d2d,const Bond & bond,bool highlight_bond,Point2D at1_cds,Point2D at2_cds,const std::vector<Point2D> & at_cds,DrawColour col1,DrawColour col2,double double_bond_offset)3000 void drawNormalBond(MolDraw2D &d2d, const Bond &bond, bool highlight_bond,
3001 Point2D at1_cds, Point2D at2_cds,
3002 const std::vector<Point2D> &at_cds, DrawColour col1,
3003 DrawColour col2, double double_bond_offset) {
3004 auto bt = bond.getBondType();
3005 auto &mol = bond.getOwningMol();
3006 // it's a double bond and one end is 1-connected, do two lines parallel
3007 // to the atom-atom line.
3008 if (bt == Bond::DOUBLE || bt == Bond::AROMATIC) {
3009 Point2D l1s, l1f, l2s, l2f;
3010 calcDoubleBondLines(mol, double_bond_offset, bond, at1_cds, at2_cds, at_cds,
3011 l1s, l1f, l2s, l2f);
3012 bool orig_slw = d2d.drawOptions().scaleBondWidth;
3013 if (highlight_bond) {
3014 d2d.drawOptions().scaleBondWidth =
3015 d2d.drawOptions().scaleHighlightBondWidth;
3016 }
3017 drawBondLine(d2d, bond, l1s, l1f, col1, col2);
3018 if (bt == Bond::AROMATIC) {
3019 d2d.setDash(dashes);
3020 }
3021 drawBondLine(d2d, bond, l2s, l2f, col1, col2);
3022 if (bt == Bond::AROMATIC) {
3023 d2d.setDash(noDash);
3024 }
3025 d2d.drawOptions().scaleBondWidth = orig_slw;
3026 } else if (Bond::SINGLE == bt && (Bond::BEGINWEDGE == bond.getBondDir() ||
3027 Bond::BEGINDASH == bond.getBondDir())) {
3028 // swap the direction if at1 has does not have stereochem set
3029 // or if at2 does have stereochem set and the bond starts there
3030 auto at1 = bond.getBeginAtom();
3031 auto at2 = bond.getEndAtom();
3032 auto inverted = false;
3033 if ((at1->getChiralTag() != Atom::CHI_TETRAHEDRAL_CW &&
3034 at1->getChiralTag() != Atom::CHI_TETRAHEDRAL_CCW) ||
3035 (at1->getIdx() != bond.getBeginAtomIdx() &&
3036 (at2->getChiralTag() == Atom::CHI_TETRAHEDRAL_CW ||
3037 at2->getChiralTag() == Atom::CHI_TETRAHEDRAL_CCW))) {
3038 // std::cerr << " swap" << std::endl;
3039 swap(at1_cds, at2_cds);
3040 swap(col1, col2);
3041 inverted = true;
3042 }
3043 if(d2d.drawOptions().singleColourWedgeBonds) {
3044 col1 = d2d.drawOptions().symbolColour;
3045 col2 = d2d.drawOptions().symbolColour;
3046 }
3047 // deliberately not scaling highlighted bond width
3048 if (Bond::BEGINWEDGE == bond.getBondDir()) {
3049 drawWedgedBond(d2d, bond, inverted, at1_cds, at2_cds, false, col1, col2);
3050 } else {
3051 drawWedgedBond(d2d, bond, inverted, at1_cds, at2_cds, true, col1, col2);
3052 }
3053 } else if (Bond::SINGLE == bt && Bond::UNKNOWN == bond.getBondDir()) {
3054 // unspecified stereo
3055 // deliberately not scaling highlighted bond width
3056 drawBondWavyLine(d2d, bond, at1_cds, at2_cds, col1, col2);
3057 } else if (Bond::DATIVE == bt || Bond::DATIVEL == bt || Bond::DATIVER == bt) {
3058 // deliberately not scaling highlighted bond width as I think
3059 // the arrowhead will look ugly.
3060 drawDativeBond(d2d, bond, at1_cds, at2_cds, col1, col2);
3061 } else if (Bond::ZERO == bt) {
3062 d2d.setDash(shortDashes);
3063 bool orig_slw = d2d.drawOptions().scaleBondWidth;
3064 if (highlight_bond) {
3065 d2d.drawOptions().scaleBondWidth =
3066 d2d.drawOptions().scaleHighlightBondWidth;
3067 }
3068 drawBondLine(d2d, bond, at1_cds, at2_cds, col1, col2);
3069 d2d.drawOptions().scaleBondWidth = orig_slw;
3070 d2d.setDash(noDash);
3071 } else if (Bond::HYDROGEN == bt) {
3072 d2d.setDash(dots);
3073 bool orig_slw = d2d.drawOptions().scaleBondWidth;
3074 if (highlight_bond) {
3075 d2d.drawOptions().scaleBondWidth =
3076 d2d.drawOptions().scaleHighlightBondWidth;
3077 }
3078 drawBondLine(d2d, bond, at1_cds, at2_cds, DrawColour(0.2, 0.2, 0.2),
3079 DrawColour(0.2, 0.2, 0.2));
3080 d2d.drawOptions().scaleBondWidth = orig_slw;
3081 d2d.setDash(noDash);
3082 } else {
3083 // in all other cases, we will definitely want to draw a line between
3084 // the two atoms
3085 bool orig_slw = d2d.drawOptions().scaleBondWidth;
3086 if (highlight_bond) {
3087 d2d.drawOptions().scaleBondWidth =
3088 d2d.drawOptions().scaleHighlightBondWidth;
3089 }
3090 drawBondLine(d2d, bond, at1_cds, at2_cds, col1, col2);
3091 if (Bond::TRIPLE == bt) {
3092 Point2D l1s, l1f, l2s, l2f;
3093 calcTripleBondLines(double_bond_offset, bond, at1_cds, at2_cds, l1s, l1f,
3094 l2s, l2f);
3095 drawBondLine(d2d, bond, l1s, l1f, col1, col2);
3096 drawBondLine(d2d, bond, l2s, l2f, col1, col2);
3097 }
3098 d2d.drawOptions().scaleBondWidth = orig_slw;
3099 }
3100 }
3101
drawQueryBond1(MolDraw2D & d2d,const Bond & bond,bool highlight_bond,const Point2D & at1_cds,const Point2D & at2_cds,const std::vector<Point2D> & at_cds,const DrawColour & col1,const DrawColour & col2,double double_bond_offset)3102 void drawQueryBond1(MolDraw2D &d2d, const Bond &bond, bool highlight_bond,
3103 const Point2D &at1_cds, const Point2D &at2_cds,
3104 const std::vector<Point2D> &at_cds, const DrawColour &col1,
3105 const DrawColour &col2, double double_bond_offset) {
3106 PRECONDITION(bond.hasQuery(), "no query");
3107 const auto qry = bond.getQuery();
3108 if (!d2d.drawOptions().splitBonds) {
3109 d2d.setActiveAtmIdx(bond.getBeginAtomIdx(), bond.getEndAtomIdx());
3110 }
3111 auto midp = (at2_cds + at1_cds) / 2.;
3112 auto dv = at2_cds - at1_cds;
3113 auto p1 = at1_cds + dv * (1. / 3.);
3114 auto p2 = at1_cds + dv * (2. / 3.);
3115 auto tdash = shortDashes;
3116 if (d2d.scale() < 10) {
3117 tdash[0] /= 4;
3118 tdash[1] /= 3;
3119 } else if (d2d.scale() < 20) {
3120 tdash[0] /= 2;
3121 tdash[1] /= 1.5;
3122 }
3123 if (qry->getDescription() == "SingleOrDoubleBond") {
3124 if (d2d.drawOptions().splitBonds) {
3125 d2d.setActiveAtmIdx(bond.getBeginAtomIdx());
3126 }
3127 {
3128 Point2D l1s, l1f, l2s, l2f;
3129 calcDoubleBondLines(bond.getOwningMol(), double_bond_offset, bond,
3130 at1_cds, p1, at_cds, l1s, l1f, l2s, l2f);
3131 d2d.setColour(col1);
3132 d2d.drawLine(l1s, l1f);
3133 d2d.drawLine(l2s, l2f);
3134 }
3135 drawBondLine(d2d, bond, p1, p2, col1, col2, false);
3136 {
3137 Point2D l1s, l1f, l2s, l2f;
3138 calcDoubleBondLines(bond.getOwningMol(), double_bond_offset, bond, p2,
3139 at2_cds, at_cds, l1s, l1f, l2s, l2f);
3140 d2d.setColour(col2);
3141 d2d.drawLine(l1s, l1f);
3142 d2d.drawLine(l2s, l2f);
3143 }
3144 } else if (qry->getDescription() == "SingleOrAromaticBond") {
3145 if (d2d.drawOptions().splitBonds) {
3146 d2d.setActiveAtmIdx(bond.getBeginAtomIdx());
3147 }
3148 {
3149 Point2D l1s, l1f, l2s, l2f;
3150 calcDoubleBondLines(bond.getOwningMol(), double_bond_offset, bond,
3151 at1_cds, p1, at_cds, l1s, l1f, l2s, l2f);
3152 d2d.setColour(col1);
3153 d2d.drawLine(l1s, l1f);
3154 d2d.setDash(tdash);
3155 d2d.drawLine(l2s, l2f);
3156 d2d.setDash(noDash);
3157 }
3158 drawBondLine(d2d, bond, p1, p2, col1, col2, false);
3159 {
3160 Point2D l1s, l1f, l2s, l2f;
3161 calcDoubleBondLines(bond.getOwningMol(), double_bond_offset, bond, p2,
3162 at2_cds, at_cds, l1s, l1f, l2s, l2f);
3163 d2d.setColour(col2);
3164 d2d.drawLine(l1s, l1f);
3165 d2d.setDash(tdash);
3166 d2d.drawLine(l2s, l2f);
3167 d2d.setDash(noDash);
3168 }
3169 } else if (qry->getDescription() == "DoubleOrAromaticBond") {
3170 if (d2d.drawOptions().splitBonds) {
3171 d2d.setActiveAtmIdx(bond.getBeginAtomIdx());
3172 }
3173 {
3174 Point2D l1s, l1f, l2s, l2f;
3175 calcDoubleBondLines(bond.getOwningMol(), double_bond_offset, bond,
3176 at1_cds, p1, at_cds, l1s, l1f, l2s, l2f);
3177 d2d.setColour(col1);
3178 d2d.drawLine(l1s, l1f);
3179 d2d.setDash(tdash);
3180 d2d.drawLine(l2s, l2f);
3181 d2d.setDash(noDash);
3182 }
3183 if (d2d.drawOptions().splitBonds) {
3184 {
3185 Point2D l1s, l1f, l2s, l2f;
3186 calcDoubleBondLines(bond.getOwningMol(), double_bond_offset, bond, p1,
3187 midp, at_cds, l1s, l1f, l2s, l2f);
3188 d2d.setColour(col1);
3189 d2d.drawLine(l1s, l1f, col1, col2);
3190 d2d.drawLine(l2s, l2f, col1, col2);
3191 d2d.setDash(noDash);
3192 }
3193 d2d.setActiveAtmIdx(bond.getEndAtomIdx());
3194 {
3195 Point2D l1s, l1f, l2s, l2f;
3196 calcDoubleBondLines(bond.getOwningMol(), double_bond_offset, bond, midp,
3197 p2, at_cds, l1s, l1f, l2s, l2f);
3198 d2d.setColour(col1);
3199 d2d.drawLine(l1s, l1f, col1, col2);
3200 d2d.drawLine(l2s, l2f, col1, col2);
3201 d2d.setDash(noDash);
3202 }
3203 } else {
3204 Point2D l1s, l1f, l2s, l2f;
3205 calcDoubleBondLines(bond.getOwningMol(), double_bond_offset, bond, p1, p2,
3206 at_cds, l1s, l1f, l2s, l2f);
3207 d2d.setColour(col1);
3208 d2d.drawLine(l1s, l1f, col1, col2);
3209 d2d.drawLine(l2s, l2f, col1, col2);
3210 d2d.setDash(noDash);
3211 }
3212 {
3213 Point2D l1s, l1f, l2s, l2f;
3214 calcDoubleBondLines(bond.getOwningMol(), double_bond_offset, bond, p2,
3215 at2_cds, at_cds, l1s, l1f, l2s, l2f);
3216 d2d.setColour(col2);
3217 d2d.drawLine(l1s, l1f);
3218 d2d.setDash(tdash);
3219 d2d.drawLine(l2s, l2f);
3220 d2d.setDash(noDash);
3221 }
3222 } else if (qry->getDescription() == "BondNull") {
3223 d2d.setDash(tdash);
3224 bool orig_slw = d2d.drawOptions().scaleBondWidth;
3225 if (highlight_bond) {
3226 d2d.drawOptions().scaleBondWidth =
3227 d2d.drawOptions().scaleHighlightBondWidth;
3228 }
3229 drawBondLine(d2d, bond, at1_cds, at2_cds, col1, col2, false);
3230 d2d.drawOptions().scaleBondWidth = orig_slw;
3231 d2d.setDash(noDash);
3232 } else {
3233 d2d.setDash(dots);
3234 bool orig_slw = d2d.drawOptions().scaleBondWidth;
3235 if (highlight_bond) {
3236 d2d.drawOptions().scaleBondWidth =
3237 d2d.drawOptions().scaleHighlightBondWidth;
3238 }
3239 drawBondLine(d2d, bond, at1_cds, at2_cds, col1, col2, false);
3240 d2d.drawOptions().scaleBondWidth = orig_slw;
3241 d2d.setDash(noDash);
3242 }
3243 d2d.setActiveAtmIdx();
3244 }
3245
drawQueryBond(MolDraw2D & d2d,const Bond & bond,bool highlight_bond,const Point2D & at1_cds,const Point2D & at2_cds,const std::vector<Point2D> & at_cds,const DrawColour & col1,const DrawColour & col2,double double_bond_offset)3246 void drawQueryBond(MolDraw2D &d2d, const Bond &bond, bool highlight_bond,
3247 const Point2D &at1_cds, const Point2D &at2_cds,
3248 const std::vector<Point2D> &at_cds, const DrawColour &col1,
3249 const DrawColour &col2, double double_bond_offset) {
3250 RDUNUSED_PARAM(col1);
3251 RDUNUSED_PARAM(col2);
3252
3253 PRECONDITION(bond.hasQuery(), "no query");
3254 const auto qry = bond.getQuery();
3255 if (!d2d.drawOptions().splitBonds) {
3256 d2d.setActiveAtmIdx(bond.getBeginAtomIdx(), bond.getEndAtomIdx());
3257 }
3258 auto midp = (at2_cds + at1_cds) / 2.;
3259 auto tdash = shortDashes;
3260 if (d2d.scale() < 10) {
3261 tdash[0] /= 4;
3262 tdash[1] /= 3;
3263 } else if (d2d.scale() < 20) {
3264 tdash[0] /= 2;
3265 tdash[1] /= 1.5;
3266 }
3267 DrawColour queryColour{0.5, 0.5, 0.5};
3268 d2d.setColour(queryColour);
3269
3270 bool drawGenericQuery = false;
3271 if (qry->getDescription() == "SingleOrDoubleBond") {
3272 if (d2d.drawOptions().splitBonds) {
3273 d2d.setActiveAtmIdx(bond.getBeginAtomIdx());
3274 }
3275 d2d.drawLine(at1_cds, midp);
3276 if (d2d.drawOptions().splitBonds) {
3277 d2d.setActiveAtmIdx(bond.getEndAtomIdx());
3278 }
3279 {
3280 Point2D l1s, l1f, l2s, l2f;
3281 calcDoubleBondLines(bond.getOwningMol(), double_bond_offset, bond, midp,
3282 at2_cds, at_cds, l1s, l1f, l2s, l2f);
3283 d2d.drawLine(l1s, l1f);
3284 d2d.drawLine(l2s, l2f);
3285 }
3286 } else if (qry->getDescription() == "SingleOrAromaticBond") {
3287 if (d2d.drawOptions().splitBonds) {
3288 d2d.setActiveAtmIdx(bond.getBeginAtomIdx());
3289 }
3290 d2d.drawLine(at1_cds, midp);
3291 if (d2d.drawOptions().splitBonds) {
3292 d2d.setActiveAtmIdx(bond.getEndAtomIdx());
3293 }
3294 {
3295 Point2D l1s, l1f, l2s, l2f;
3296 calcDoubleBondLines(bond.getOwningMol(), double_bond_offset, bond, midp,
3297 at2_cds, at_cds, l1s, l1f, l2s, l2f);
3298 d2d.drawLine(l1s, l1f);
3299 d2d.setDash(tdash);
3300 d2d.drawLine(l2s, l2f);
3301 d2d.setDash(noDash);
3302 }
3303 } else if (qry->getDescription() == "DoubleOrAromaticBond") {
3304 if (d2d.drawOptions().splitBonds) {
3305 d2d.setActiveAtmIdx(bond.getBeginAtomIdx());
3306 }
3307 {
3308 Point2D l1s, l1f, l2s, l2f;
3309 calcDoubleBondLines(bond.getOwningMol(), double_bond_offset, bond,
3310 at1_cds, midp, at_cds, l1s, l1f, l2s, l2f);
3311 d2d.drawLine(l1s, l1f);
3312 d2d.drawLine(l2s, l2f);
3313 }
3314 if (d2d.drawOptions().splitBonds) {
3315 d2d.setActiveAtmIdx(bond.getEndAtomIdx());
3316 }
3317 {
3318 Point2D l1s, l1f, l2s, l2f;
3319 calcDoubleBondLines(bond.getOwningMol(), double_bond_offset, bond, midp,
3320 at2_cds, at_cds, l1s, l1f, l2s, l2f);
3321 d2d.drawLine(l1s, l1f);
3322 d2d.setDash(tdash);
3323 d2d.drawLine(l2s, l2f);
3324 d2d.setDash(noDash);
3325 }
3326 } else if (qry->getDescription() == "BondNull") {
3327 d2d.setDash(tdash);
3328 drawBondLine(d2d, bond, at1_cds, at2_cds);
3329 d2d.setDash(noDash);
3330 } else if (qry->getDescription() == "BondAnd" &&
3331 qry->endChildren() - qry->beginChildren() == 2) {
3332 auto q1 = *(qry->beginChildren());
3333 auto q2 = *(qry->beginChildren() + 1);
3334
3335 if (q2->getDescription() == "BondOrder") {
3336 std::swap(q1, q2);
3337 }
3338 if (q1->getDescription() == "BondOrder" &&
3339 q2->getDescription() == "BondInRing") {
3340 drawNormalBond(d2d, bond, false, at1_cds, at2_cds, at_cds, queryColour,
3341 queryColour, double_bond_offset);
3342
3343 Point2D segment = at2_cds - at1_cds;
3344 d2d.setFillPolys(false);
3345 auto slw = d2d.drawOptions().scaleBondWidth;
3346 d2d.drawOptions().scaleBondWidth = false;
3347 auto lw = d2d.lineWidth();
3348 d2d.setLineWidth(1);
3349 if (!q2->getNegation()) {
3350 segment /= segment.length() * 6;
3351 Point2D r1 = Point2D(0.5 * segment.x - 0.866 * segment.y,
3352 0.866 * segment.x + 0.5 * segment.y);
3353 Point2D r2 =
3354 Point2D(0.5 * r1.x - 0.866 * r1.y, 0.866 * r1.x + 0.5 * r1.y);
3355 std::vector<Point2D> pts = {midp + segment, midp + r1, midp + r2,
3356 midp - segment, midp - r1, midp - r2,
3357 midp + segment};
3358 d2d.drawPolygon(pts);
3359
3360 } else {
3361 segment /= segment.length() * 10;
3362 auto l = segment.length();
3363 Point2D p1 = midp + segment + Point2D(l, l);
3364 Point2D p2 = midp + segment - Point2D(l, l);
3365 d2d.drawEllipse(p1, p2);
3366 p1 = midp - segment + Point2D(l, l);
3367 p2 = midp - segment - Point2D(l, l);
3368 d2d.drawEllipse(p1, p2);
3369 }
3370 d2d.drawOptions().scaleBondWidth = slw;
3371 d2d.setLineWidth(lw);
3372 } else {
3373 drawGenericQuery = true;
3374 }
3375 } else {
3376 drawGenericQuery = true;
3377 }
3378 if (drawGenericQuery) {
3379 d2d.setDash(dots);
3380 bool orig_slw = d2d.drawOptions().scaleBondWidth;
3381 if (highlight_bond) {
3382 d2d.drawOptions().scaleBondWidth =
3383 d2d.drawOptions().scaleHighlightBondWidth;
3384 }
3385 drawBondLine(d2d, bond, at1_cds, at2_cds);
3386 d2d.drawOptions().scaleBondWidth = orig_slw;
3387 d2d.setDash(noDash);
3388 }
3389 d2d.setActiveAtmIdx();
3390 }
3391
3392 } // namespace
3393
3394 // ****************************************************************************
drawBond(const ROMol & mol,const Bond * bond,int at1_idx,int at2_idx,const vector<int> * highlight_atoms,const map<int,DrawColour> * highlight_atom_map,const vector<int> * highlight_bonds,const map<int,DrawColour> * highlight_bond_map,const std::vector<std::pair<DrawColour,DrawColour>> * bond_colours)3395 void MolDraw2D::drawBond(
3396 const ROMol &mol, const Bond *bond, int at1_idx, int at2_idx,
3397 const vector<int> *highlight_atoms,
3398 const map<int, DrawColour> *highlight_atom_map,
3399 const vector<int> *highlight_bonds,
3400 const map<int, DrawColour> *highlight_bond_map,
3401 const std::vector<std::pair<DrawColour, DrawColour>> *bond_colours) {
3402 PRECONDITION(bond, "no bond");
3403 PRECONDITION(activeMolIdx_ >= 0, "bad mol idx");
3404 RDUNUSED_PARAM(highlight_atoms);
3405 RDUNUSED_PARAM(highlight_atom_map);
3406 RDUNUSED_PARAM(mol);
3407
3408 if (static_cast<unsigned int>(at1_idx) != bond->getBeginAtomIdx()) {
3409 std::swap(at1_idx, at2_idx);
3410 }
3411
3412 Point2D at1_cds = at_cds_[activeMolIdx_][at1_idx];
3413 Point2D at2_cds = at_cds_[activeMolIdx_][at2_idx];
3414
3415 double double_bond_offset = options_.multipleBondOffset;
3416 // mol files from, for example, Marvin use a bond length of 1 for just about
3417 // everything. When this is the case, the default multipleBondOffset is just
3418 // too much, so scale it back.
3419 if ((at1_cds - at2_cds).lengthSq() < 1.4) {
3420 double_bond_offset *= 0.6;
3421 }
3422
3423 adjustBondEndForLabel(atom_syms_[activeMolIdx_][at1_idx], at2_cds, at1_cds);
3424 adjustBondEndForLabel(atom_syms_[activeMolIdx_][at2_idx], at1_cds, at2_cds);
3425
3426 bool highlight_bond = false;
3427 if (highlight_bonds &&
3428 std::find(highlight_bonds->begin(), highlight_bonds->end(),
3429 bond->getIdx()) != highlight_bonds->end()) {
3430 highlight_bond = true;
3431 }
3432
3433 DrawColour col1, col2;
3434 int orig_lw = lineWidth();
3435 if (bond_colours) {
3436 col1 = (*bond_colours)[bond->getIdx()].first;
3437 col2 = (*bond_colours)[bond->getIdx()].second;
3438 } else {
3439 if (!highlight_bond) {
3440 col1 = getColour(at1_idx);
3441 col2 = getColour(at2_idx);
3442 } else {
3443 if (highlight_bond_map && highlight_bond_map->find(bond->getIdx()) !=
3444 highlight_bond_map->end()) {
3445 col1 = col2 = highlight_bond_map->find(bond->getIdx())->second;
3446 } else {
3447 col1 = col2 = drawOptions().highlightColour;
3448 }
3449 if (drawOptions().continuousHighlight) {
3450 setLineWidth(getHighlightBondWidth(bond->getIdx(), nullptr));
3451 } else {
3452 setLineWidth(getHighlightBondWidth(bond->getIdx(), nullptr) / 4);
3453 }
3454 }
3455 }
3456
3457 bool isComplex = false;
3458 if (bond->hasQuery()) {
3459 std::string descr = bond->getQuery()->getDescription();
3460 if (bond->getQuery()->getNegation() || descr != "BondOrder") {
3461 isComplex = true;
3462 drawQueryBond(*this, *bond, highlight_bond, at1_cds, at2_cds,
3463 at_cds_[activeMolIdx_], col1, col2, double_bond_offset);
3464 }
3465 }
3466
3467 if (!isComplex) {
3468 drawNormalBond(*this, *bond, highlight_bond, at1_cds, at2_cds,
3469 at_cds_[activeMolIdx_], col1, col2, double_bond_offset);
3470 }
3471 if (highlight_bond) {
3472 setLineWidth(orig_lw);
3473 }
3474 }
3475
3476 // ****************************************************************************
drawAtomLabel(int atom_num,const std::vector<int> * highlight_atoms,const std::map<int,DrawColour> * highlight_map)3477 void MolDraw2D::drawAtomLabel(int atom_num,
3478 const std::vector<int> *highlight_atoms,
3479 const std::map<int, DrawColour> *highlight_map) {
3480 drawAtomLabel(atom_num, getColour(atom_num, highlight_atoms, highlight_map));
3481 }
3482
3483 // ****************************************************************************
drawAtomLabel(int atom_num,const DrawColour & draw_colour)3484 void MolDraw2D::drawAtomLabel(int atom_num, const DrawColour &draw_colour) {
3485 text_drawer_->setColour(draw_colour);
3486 Point2D draw_cds = getDrawCoords(atom_num);
3487 text_drawer_->drawString(atom_syms_[activeMolIdx_][atom_num].first, draw_cds,
3488 atom_syms_[activeMolIdx_][atom_num].second);
3489 // this is useful for debugging the drawings.
3490 // int olw = lineWidth();
3491 // setLineWidth(1);
3492 // text_drawer_->drawStringRects(atom_syms_[activeMolIdx_][atom_num].first,
3493 // atom_syms_[activeMolIdx_][atom_num].second,
3494 // draw_cds, *this);
3495 // setLineWidth(olw);
3496 }
3497
3498 // ****************************************************************************
drawAnnotation(const AnnotationType & annot)3499 void MolDraw2D::drawAnnotation(const AnnotationType &annot) {
3500 double full_font_scale = text_drawer_->fontScale();
3501 // turn off minFontSize for the annotation, as we do want it to be smaller
3502 // than the letters, even if that makes it tiny. The annotation positions
3503 // have been calculated on the assumption that this is the case, and if
3504 // minFontSize is applied, they may well clash with the atom symbols.
3505 double omfs = text_drawer_->minFontSize();
3506 if (annot.scaleText_) {
3507 text_drawer_->setMinFontSize(-1);
3508 text_drawer_->setFontScale(drawOptions().annotationFontScale *
3509 full_font_scale);
3510 }
3511 Point2D draw_cds = getDrawCoords(annot.rect_.trans_);
3512 text_drawer_->drawString(annot.text_, draw_cds, annot.align_);
3513
3514 if (annot.scaleText_) {
3515 text_drawer_->setMinFontSize(omfs);
3516 text_drawer_->setFontScale(full_font_scale);
3517 }
3518 }
3519 // ****************************************************************************
calcRadicalRect(const ROMol & mol,const Atom * atom,StringRect & rad_rect)3520 OrientType MolDraw2D::calcRadicalRect(const ROMol &mol, const Atom *atom,
3521 StringRect &rad_rect) {
3522 int num_rade = atom->getNumRadicalElectrons();
3523 double spot_rad = 0.2 * drawOptions().multipleBondOffset;
3524 Point2D const &at_cds = at_cds_[activeMolIdx_][atom->getIdx()];
3525 string const &at_sym = atom_syms_[activeMolIdx_][atom->getIdx()].first;
3526 OrientType orient = atom_syms_[activeMolIdx_][atom->getIdx()].second;
3527 double rad_size = (3 * num_rade - 1) * spot_rad;
3528 rad_size = (4 * num_rade - 2) * spot_rad;
3529 double x_min, y_min, x_max, y_max;
3530 Point2D at_draw_cds = getDrawCoords(at_cds);
3531 if (!at_sym.empty()) {
3532 text_drawer_->getStringExtremes(at_sym, orient, x_min, y_min, x_max, y_max);
3533 x_min += at_draw_cds.x;
3534 x_max += at_draw_cds.x;
3535 y_min += at_draw_cds.y;
3536 y_max += at_draw_cds.y;
3537 } else {
3538 x_min = at_draw_cds.x - 3 * spot_rad * text_drawer_->fontScale();
3539 x_max = at_draw_cds.x + 3 * spot_rad * text_drawer_->fontScale();
3540 y_min = at_draw_cds.y - 3 * spot_rad * text_drawer_->fontScale();
3541 y_max = at_draw_cds.y + 3 * spot_rad * text_drawer_->fontScale();
3542 }
3543
3544 auto rect_to_atom_coords = [&](StringRect &rect) {
3545 rect.width_ /= text_drawer_->fontScale();
3546 rect.height_ /= text_drawer_->fontScale();
3547 rect.trans_ = getAtomCoords(make_pair(rect.trans_.x, rect.trans_.y));
3548 };
3549
3550 auto try_all = [&](OrientType ornt) -> bool {
3551 vector<std::shared_ptr<StringRect>> rad_rects(
3552 1, std::shared_ptr<StringRect>(new StringRect(rad_rect)));
3553 if (!text_drawer_->doesRectIntersect(at_sym, ornt, at_cds, rad_rect) &&
3554 !doesAtomNoteClash(rad_rect, rad_rects, mol, atom->getIdx())) {
3555 rect_to_atom_coords(rad_rect);
3556 return true;
3557 } else {
3558 return false;
3559 }
3560 };
3561
3562 auto try_north = [&]() -> bool {
3563 rad_rect.width_ = rad_size * text_drawer_->fontScale();
3564 rad_rect.height_ = spot_rad * 3.0 * text_drawer_->fontScale();
3565 rad_rect.trans_.x = at_draw_cds.x;
3566 rad_rect.trans_.y = y_max + 0.5 * rad_rect.height_;
3567 return try_all(OrientType::N);
3568 };
3569 auto try_south = [&]() -> bool {
3570 rad_rect.width_ = rad_size * text_drawer_->fontScale();
3571 rad_rect.height_ = spot_rad * 3.0 * text_drawer_->fontScale();
3572 rad_rect.trans_.x = at_draw_cds.x;
3573 rad_rect.trans_.y = y_min - 0.5 * rad_rect.height_;
3574 return try_all(OrientType::S);
3575 };
3576 auto try_east = [&]() -> bool {
3577 rad_rect.trans_.x = x_max + 3.0 * spot_rad * text_drawer_->fontScale();
3578 rad_rect.trans_.y = at_draw_cds.y;
3579 rad_rect.width_ = spot_rad * 1.5 * text_drawer_->fontScale();
3580 rad_rect.height_ = rad_size * text_drawer_->fontScale();
3581 return try_all(OrientType::E);
3582 };
3583 auto try_west = [&]() -> bool {
3584 rad_rect.trans_.x = x_min - 3.0 * spot_rad * text_drawer_->fontScale();
3585 rad_rect.trans_.y = at_draw_cds.y;
3586 rad_rect.width_ = spot_rad * 1.5 * text_drawer_->fontScale();
3587 rad_rect.height_ = rad_size * text_drawer_->fontScale();
3588 return try_all(OrientType::W);
3589 };
3590
3591 auto try_rads = [&](OrientType ornt) -> bool {
3592 switch (ornt) {
3593 case OrientType::N:
3594 case OrientType::C:
3595 return try_north();
3596 case OrientType::E:
3597 return try_east();
3598 case OrientType::S:
3599 return try_south();
3600 case OrientType::W:
3601 return try_west();
3602 }
3603 return false;
3604 };
3605 if (try_rads(orient)) {
3606 return orient;
3607 }
3608 OrientType all_ors[4] = {OrientType::N, OrientType::E, OrientType::S,
3609 OrientType::W};
3610 for (int io = 0; io < 4; ++io) {
3611 if (orient != all_ors[io]) {
3612 if (try_rads(all_ors[io])) {
3613 return all_ors[io];
3614 }
3615 }
3616 }
3617 // stick them N irrespective of a clash whilst muttering "sod it"
3618 // under our breath.
3619 try_north();
3620 return OrientType::N;
3621 }
3622
3623 namespace {} // namespace
3624
3625 // ****************************************************************************
drawRadicals(const ROMol & mol)3626 void MolDraw2D::drawRadicals(const ROMol &mol) {
3627 // take account of differing font scale and main scale if we've hit
3628 // max or min font size.
3629 double f_scale = text_drawer_->fontScale() / scale();
3630 double spot_rad = 0.2 * drawOptions().multipleBondOffset * f_scale;
3631 setColour(DrawColour(0.0, 0.0, 0.0));
3632 // Point2D should be in atom coords
3633 auto draw_spot = [&](const Point2D &cds) {
3634 bool ofp = fillPolys();
3635 setFillPolys(true);
3636 int olw = lineWidth();
3637 setLineWidth(0);
3638 drawArc(cds, spot_rad, 0, 360);
3639 setLineWidth(olw);
3640 setFillPolys(ofp);
3641 };
3642 // cds in draw coords
3643
3644 auto draw_spots = [&](const Point2D &cds, int num_spots, double width,
3645 int dir = 0) {
3646 Point2D ncds = cds;
3647 switch (num_spots) {
3648 case 3:
3649 draw_spot(ncds);
3650 if (dir) {
3651 ncds.y = cds.y - 0.5 * width + spot_rad;
3652 } else {
3653 ncds.x = cds.x - 0.5 * width + spot_rad;
3654 }
3655 draw_spot(ncds);
3656 if (dir) {
3657 ncds.y = cds.y + 0.5 * width - spot_rad;
3658 } else {
3659 ncds.x = cds.x + 0.5 * width - spot_rad;
3660 }
3661 draw_spot(ncds);
3662 /* fallthrough */
3663 case 1:
3664 draw_spot(cds);
3665 break;
3666 case 4:
3667 if (dir) {
3668 ncds.y = cds.y + 6.0 * spot_rad;
3669 } else {
3670 ncds.x = cds.x + 6.0 * spot_rad;
3671 }
3672 draw_spot(ncds);
3673 if (dir) {
3674 ncds.y = cds.y - 6.0 * spot_rad;
3675 } else {
3676 ncds.x = cds.x - 6.0 * spot_rad;
3677 }
3678 draw_spot(ncds);
3679 /* fallthrough */
3680 case 2:
3681 if (dir) {
3682 ncds.y = cds.y + 2.0 * spot_rad;
3683 } else {
3684 ncds.x = cds.x + 2.0 * spot_rad;
3685 }
3686 draw_spot(ncds);
3687 if (dir) {
3688 ncds.y = cds.y - 2.0 * spot_rad;
3689 } else {
3690 ncds.x = cds.x - 2.0 * spot_rad;
3691 }
3692 draw_spot(ncds);
3693 break;
3694 }
3695 };
3696
3697 size_t rad_num = 0;
3698 for (auto atom : mol.atoms()) {
3699 int num_rade = atom->getNumRadicalElectrons();
3700 if (!num_rade) {
3701 continue;
3702 }
3703 auto rad_rect = radicals_[activeMolIdx_][rad_num].first;
3704 OrientType draw_or = radicals_[activeMolIdx_][rad_num].second;
3705 if (draw_or == OrientType::N || draw_or == OrientType::S ||
3706 draw_or == OrientType::C) {
3707 draw_spots(rad_rect->trans_, num_rade, rad_rect->width_, 0);
3708 } else {
3709 draw_spots(rad_rect->trans_, num_rade, rad_rect->height_, 1);
3710 }
3711 ++rad_num;
3712 }
3713 }
3714
3715 // ****************************************************************************
getNoteStartAngle(const ROMol & mol,const Atom * atom) const3716 double MolDraw2D::getNoteStartAngle(const ROMol &mol, const Atom *atom) const {
3717 if (atom->getDegree() == 0) {
3718 return M_PI / 2.0;
3719 }
3720 Point2D at_cds = at_cds_[activeMolIdx_][atom->getIdx()];
3721 vector<Point2D> bond_vecs;
3722 for (const auto &nbr : make_iterator_range(mol.getAtomNeighbors(atom))) {
3723 Point2D bond_vec = at_cds.directionVector(at_cds_[activeMolIdx_][nbr]);
3724 bond_vec.normalize();
3725 bond_vecs.emplace_back(bond_vec);
3726 }
3727
3728 Point2D ret_vec;
3729 if (bond_vecs.size() == 1) {
3730 if (atom_syms_[activeMolIdx_][atom->getIdx()].first.empty()) {
3731 // go with perpendicular to bond. This is mostly to avoid getting
3732 // a zero at the end of a bond to carbon, which looks like a black
3733 // oxygen atom in the default font in SVG and PNG.
3734 ret_vec.x = bond_vecs[0].y;
3735 ret_vec.y = -bond_vecs[0].x;
3736 } else {
3737 // go opposite end
3738 ret_vec = -bond_vecs[0];
3739 }
3740 } else if (bond_vecs.size() == 2) {
3741 ret_vec = bond_vecs[0] + bond_vecs[1];
3742 if (ret_vec.lengthSq() > 1.0e-6) {
3743 if (!atom->getNumImplicitHs() || atom->getAtomicNum() == 6) {
3744 // prefer outside the angle, unless there are Hs that will be in
3745 // the way, probably.
3746 ret_vec *= -1.0;
3747 }
3748 } else {
3749 // it must be a -# or == or some such. Take perpendicular to
3750 // one of them
3751 ret_vec.x = -bond_vecs.front().y;
3752 ret_vec.y = bond_vecs.front().x;
3753 ret_vec.normalize();
3754 }
3755 } else {
3756 // just take 2 that are probably adjacent
3757 double discrim = 4.0 * M_PI / bond_vecs.size();
3758 for (size_t i = 0; i < bond_vecs.size() - 1; ++i) {
3759 for (size_t j = i + 1; j < bond_vecs.size(); ++j) {
3760 double ang = acos(bond_vecs[i].dotProduct(bond_vecs[j]));
3761 if (ang < discrim) {
3762 ret_vec = bond_vecs[i] + bond_vecs[j];
3763 ret_vec.normalize();
3764 discrim = -1.0;
3765 break;
3766 }
3767 }
3768 }
3769 if (discrim > 0.0) {
3770 ret_vec = bond_vecs[0] + bond_vecs[1];
3771 ret_vec *= -1.0;
3772 }
3773 }
3774
3775 // start angle is the angle between ret_vec and the x axis
3776 return atan2(ret_vec.y, ret_vec.x);
3777 }
3778
3779 // ****************************************************************************
doesAtomNoteClash(StringRect & note_rect,const vector<std::shared_ptr<StringRect>> & rects,const ROMol & mol,unsigned int atom_idx)3780 bool MolDraw2D::doesAtomNoteClash(
3781 StringRect ¬e_rect, const vector<std::shared_ptr<StringRect>> &rects,
3782 const ROMol &mol, unsigned int atom_idx) {
3783 auto atom = mol.getAtomWithIdx(atom_idx);
3784
3785 note_rect.clash_score_ = 0;
3786 if (doesNoteClashNbourBonds(note_rect, rects, mol, atom)) {
3787 return true;
3788 }
3789 note_rect.clash_score_ = 1;
3790 if (doesNoteClashAtomLabels(note_rect, rects, mol, atom_idx)) {
3791 return true;
3792 }
3793 note_rect.clash_score_ = 2;
3794 if (doesNoteClashOtherNotes(note_rect, rects)) {
3795 return true;
3796 }
3797 note_rect.clash_score_ = 3;
3798 return false;
3799 }
3800
3801 // ****************************************************************************
doesBondNoteClash(StringRect & note_rect,const vector<std::shared_ptr<StringRect>> & rects,const ROMol & mol,const Bond * bond)3802 bool MolDraw2D::doesBondNoteClash(
3803 StringRect ¬e_rect, const vector<std::shared_ptr<StringRect>> &rects,
3804 const ROMol &mol, const Bond *bond) {
3805 note_rect.clash_score_ = 0;
3806 string note = bond->getProp<string>(common_properties::bondNote);
3807 if (doesNoteClashNbourBonds(note_rect, rects, mol, bond->getBeginAtom())) {
3808 return true;
3809 }
3810 note_rect.clash_score_ = 1;
3811 unsigned int atom_idx = bond->getBeginAtomIdx();
3812 if (doesNoteClashAtomLabels(note_rect, rects, mol, atom_idx)) {
3813 return true;
3814 }
3815 note_rect.clash_score_ = 2;
3816 if (doesNoteClashOtherNotes(note_rect, rects)) {
3817 return true;
3818 }
3819 note_rect.clash_score_ = 3;
3820 return false;
3821 }
3822
3823 // ****************************************************************************
doesNoteClashNbourBonds(const StringRect & note_rect,const vector<std::shared_ptr<StringRect>> & rects,const ROMol & mol,const Atom * atom) const3824 bool MolDraw2D::doesNoteClashNbourBonds(
3825 const StringRect ¬e_rect,
3826 const vector<std::shared_ptr<StringRect>> &rects, const ROMol &mol,
3827 const Atom *atom) const {
3828 double double_bond_offset = -1.0;
3829 Point2D const &at2_dcds =
3830 getDrawCoords(at_cds_[activeMolIdx_][atom->getIdx()]);
3831
3832 double line_width = lineWidth() * scale() * 0.02;
3833 for (const auto &nbr : make_iterator_range(mol.getAtomNeighbors(atom))) {
3834 Point2D const &at1_dcds = getDrawCoords(at_cds_[activeMolIdx_][nbr]);
3835 if (text_drawer_->doesLineIntersect(rects, note_rect.trans_, at1_dcds,
3836 at2_dcds, line_width)) {
3837 return true;
3838 }
3839 // now see about clashing with other lines if not single
3840 auto bond = mol.getBondBetweenAtoms(atom->getIdx(), nbr);
3841 Bond::BondType bt = bond->getBondType();
3842 if (bt == Bond::SINGLE) {
3843 continue;
3844 }
3845
3846 if (double_bond_offset < 0.0) {
3847 double_bond_offset = options_.multipleBondOffset;
3848 // mol files from, for example, Marvin use a bond length of 1 for just
3849 // about everything. When this is the case, the default multipleBondOffset
3850 // is just too much, so scale it back.
3851 if ((at1_dcds - at2_dcds).lengthSq() < 1.4 * scale()) {
3852 double_bond_offset *= 0.6;
3853 }
3854 }
3855 if (bt == Bond::DOUBLE || bt == Bond::AROMATIC || bt == Bond::TRIPLE) {
3856 Point2D l1s, l1f, l2s, l2f;
3857 if (bt == Bond::DOUBLE || bt == Bond::AROMATIC) {
3858 // use the atom coords for this ot make sure the perp goes the
3859 // correct way (y coordinate issue).
3860 calcDoubleBondLines(mol, double_bond_offset, *bond,
3861 at_cds_[activeMolIdx_][nbr],
3862 at_cds_[activeMolIdx_][atom->getIdx()],
3863 at_cds_[activeMolIdx_], l1s, l1f, l2s, l2f);
3864 } else {
3865 calcTripleBondLines(
3866 double_bond_offset, *bond, at_cds_[activeMolIdx_][nbr],
3867 at_cds_[activeMolIdx_][atom->getIdx()], l1s, l1f, l2s, l2f);
3868 }
3869 l1s = getDrawCoords(l1s);
3870 l1f = getDrawCoords(l1f);
3871 l2s = getDrawCoords(l2s);
3872 l2f = getDrawCoords(l2f);
3873
3874 if (text_drawer_->doesLineIntersect(rects, note_rect.trans_, l1s, l1f,
3875 line_width) ||
3876 text_drawer_->doesLineIntersect(rects, note_rect.trans_, l2s, l2f,
3877 line_width)) {
3878 return true;
3879 }
3880 }
3881 }
3882
3883 return false;
3884 }
3885
3886 // ****************************************************************************
doesNoteClashAtomLabels(const StringRect & note_rect,const vector<std::shared_ptr<StringRect>> & rects,const ROMol & mol,unsigned int atom_idx) const3887 bool MolDraw2D::doesNoteClashAtomLabels(
3888 const StringRect ¬e_rect,
3889 const vector<std::shared_ptr<StringRect>> &rects, const ROMol &mol,
3890 unsigned int atom_idx) const {
3891 // try the atom_idx first as it's the most likely clash
3892 Point2D draw_cds = getDrawCoords(atom_idx);
3893 if (text_drawer_->doesStringIntersect(
3894 rects, note_rect.trans_, atom_syms_[activeMolIdx_][atom_idx].first,
3895 atom_syms_[activeMolIdx_][atom_idx].second, draw_cds)) {
3896 return true;
3897 }
3898 // if it's cluttered, it might clash with other labels.
3899 for (auto atom : mol.atoms()) {
3900 if (atom_idx == atom->getIdx()) {
3901 continue;
3902 }
3903 const auto &atsym = atom_syms_[activeMolIdx_][atom->getIdx()];
3904 if (atsym.first.empty()) {
3905 continue;
3906 }
3907 draw_cds = getDrawCoords(atom->getIdx());
3908 if (text_drawer_->doesStringIntersect(rects, note_rect.trans_, atsym.first,
3909 atsym.second, draw_cds)) {
3910 return true;
3911 }
3912 }
3913
3914 return false;
3915 }
3916
3917 // ****************************************************************************
doesNoteClashOtherNotes(const StringRect & note_rect,const vector<std::shared_ptr<StringRect>> & rects) const3918 bool MolDraw2D::doesNoteClashOtherNotes(
3919 const StringRect ¬e_rect,
3920 const vector<std::shared_ptr<StringRect>> &rects) const {
3921 for (auto const &annot : annotations_[activeMolIdx_]) {
3922 if (text_drawer_->doesRectIntersect(rects, note_rect.trans_, annot.rect_)) {
3923 return true;
3924 }
3925 }
3926 return false;
3927 }
3928
3929 // ****************************************************************************
getDrawLineWidth() const3930 double MolDraw2D::getDrawLineWidth() const {
3931 double width = lineWidth();
3932 // This works fairly well for SVG and Cairo. 0.02 is picked by eye
3933 if (drawOptions().scaleBondWidth) {
3934 width *= scale() * 0.02;
3935 if (width < 0.0) {
3936 width = 0.0;
3937 }
3938 }
3939 return width;
3940 }
3941
3942 // ****************************************************************************
3943 // take the coords for atnum, with neighbour nbr_cds, and move cds out to
3944 // accommodate the label associated with it.
adjustBondEndForLabel(const std::pair<std::string,OrientType> & lbl,const Point2D & nbr_cds,Point2D & cds) const3945 void MolDraw2D::adjustBondEndForLabel(
3946 const std::pair<std::string, OrientType> &lbl, const Point2D &nbr_cds,
3947 Point2D &cds) const {
3948 if (lbl.first.empty()) {
3949 return;
3950 }
3951
3952 Point2D draw_cds = getDrawCoords(cds);
3953 Point2D nbr_draw_cds = getDrawCoords(nbr_cds);
3954
3955 text_drawer_->adjustLineForString(lbl.first, lbl.second, nbr_draw_cds,
3956 draw_cds);
3957
3958 cds = getAtomCoords(make_pair(draw_cds.x, draw_cds.y));
3959
3960 if (drawOptions().additionalAtomLabelPadding > 0.0) {
3961 // directionVector is normalised.
3962 Point2D bond =
3963 cds.directionVector(nbr_cds) * drawOptions().additionalAtomLabelPadding;
3964 cds += bond;
3965 }
3966 }
3967
3968 // ****************************************************************************
getAtomSymbolAndOrientation(const Atom & atom) const3969 pair<string, OrientType> MolDraw2D::getAtomSymbolAndOrientation(
3970 const Atom &atom) const {
3971 OrientType orient = getAtomOrientation(atom);
3972 string symbol = getAtomSymbol(atom, orient);
3973
3974 return std::make_pair(symbol, orient);
3975 }
3976
getAtomListText(const Atom & atom)3977 std::string getAtomListText(const Atom &atom) {
3978 PRECONDITION(atom.hasQuery(), "no query");
3979 PRECONDITION(atom.getQuery()->getDescription() == "AtomOr", "bad query type");
3980
3981 std::string res = "";
3982 if (atom.getQuery()->getNegation()) {
3983 res += "!";
3984 }
3985 res += "[";
3986 std::vector<int> vals;
3987 getAtomListQueryVals(atom.getQuery(), vals);
3988 for (unsigned int i = 0; i < vals.size(); ++i) {
3989 if (i != 0) {
3990 res += ",";
3991 }
3992 res += PeriodicTable::getTable()->getElementSymbol(vals[i]);
3993 }
3994
3995 return res + "]";
3996 }
3997
3998 // ****************************************************************************
getAtomSymbol(const RDKit::Atom & atom,OrientType orientation) const3999 string MolDraw2D::getAtomSymbol(const RDKit::Atom &atom,
4000 OrientType orientation) const {
4001 if (drawOptions().noAtomLabels) {
4002 return "";
4003 }
4004 // adds XML-like annotation for super- and sub-script, in the same manner
4005 // as MolDrawing.py. My first thought was for a LaTeX-like system,
4006 // obviously...
4007 string symbol;
4008 bool literal_symbol = true;
4009 unsigned int iso = atom.getIsotope();
4010 if (drawOptions().atomLabels.find(atom.getIdx()) !=
4011 drawOptions().atomLabels.end()) {
4012 // specified labels are trump: no matter what else happens we will show
4013 // them.
4014 symbol = drawOptions().atomLabels.find(atom.getIdx())->second;
4015 } else if (atom.hasProp(common_properties::_displayLabel) ||
4016 atom.hasProp(common_properties::_displayLabelW)) {
4017 // logic here: if either _displayLabel or _displayLabelW is set, we will
4018 // definitely use one of those. if only one is set, we'll use that one if
4019 // both are set and the orientation is W then we'll use _displayLabelW,
4020 // otherwise _displayLabel
4021
4022 std::string lbl;
4023 std::string lblw;
4024 atom.getPropIfPresent(common_properties::_displayLabel, lbl);
4025 atom.getPropIfPresent(common_properties::_displayLabelW, lblw);
4026 if (lbl.empty()) {
4027 lbl = lblw;
4028 }
4029 if (orientation == OrientType::W && !lblw.empty()) {
4030 symbol = lblw;
4031 } else {
4032 symbol = lbl;
4033 }
4034 } else if (atom.hasProp(common_properties::atomLabel)) {
4035 symbol = atom.getProp<std::string>(common_properties::atomLabel);
4036 } else if (drawOptions().dummiesAreAttachments && atom.getAtomicNum() == 0 &&
4037 atom.getDegree() == 1) {
4038 symbol = "";
4039 literal_symbol = false;
4040 } else if (isAtomListQuery(&atom)) {
4041 symbol = getAtomListText(atom);
4042 } else if (isComplexQuery(&atom)) {
4043 symbol = "?";
4044 } else if (drawOptions().atomLabelDeuteriumTritium &&
4045 atom.getAtomicNum() == 1 && (iso == 2 || iso == 3)) {
4046 symbol = ((iso == 2) ? "D" : "T");
4047 iso = 0;
4048 } else {
4049 literal_symbol = false;
4050 std::vector<std::string> preText, postText;
4051
4052 // first thing after the symbol is the atom map
4053 if (atom.hasProp("molAtomMapNumber")) {
4054 string map_num = "";
4055 atom.getProp("molAtomMapNumber", map_num);
4056 postText.push_back(std::string(":") + map_num);
4057 }
4058
4059 if (0 != atom.getFormalCharge()) {
4060 // charge always comes post the symbol
4061 int ichg = atom.getFormalCharge();
4062 string sgn = ichg > 0 ? string("+") : string("-");
4063 ichg = abs(ichg);
4064 if (ichg > 1) {
4065 sgn = std::to_string(ichg) + sgn;
4066 }
4067 // put the charge as a superscript
4068 postText.push_back(string("<sup>") + sgn + string("</sup>"));
4069 }
4070
4071 int num_h = (atom.getAtomicNum() == 6 && atom.getDegree() > 0)
4072 ? 0
4073 : atom.getTotalNumHs(); // FIX: still not quite right
4074
4075 if (drawOptions().explicitMethyl && atom.getAtomicNum() == 6 &&
4076 atom.getDegree() == 1) {
4077 symbol += atom.getSymbol();
4078 num_h = atom.getTotalNumHs();
4079 }
4080
4081 if (num_h > 0 && !atom.hasQuery()) {
4082 // the H text comes after the atomic symbol
4083 std::string h = "H";
4084 if (num_h > 1) {
4085 // put the number as a subscript
4086 h += string("<sub>") + std::to_string(num_h) + string("</sub>");
4087 }
4088 postText.push_back(h);
4089 }
4090
4091 if (0 != iso &&
4092 ((drawOptions().isotopeLabels && atom.getAtomicNum() != 0) ||
4093 (drawOptions().dummyIsotopeLabels && atom.getAtomicNum() == 0))) {
4094 // isotope always comes before the symbol
4095 preText.push_back(std::string("<sup>") + std::to_string(iso) +
4096 std::string("</sup>"));
4097 }
4098
4099 symbol = "";
4100 for (const std::string &se : preText) {
4101 symbol += se;
4102 }
4103
4104 // allenes need a C, but extend to any atom with degree 2 and both
4105 // bonds in a line.
4106 if (isLinearAtom(atom, at_cds_[activeMolIdx_]) ||
4107 (atom.getAtomicNum() != 6 || atom.getDegree() == 0 || preText.size() ||
4108 postText.size())) {
4109 symbol += atom.getSymbol();
4110 }
4111 for (const std::string &se : postText) {
4112 symbol += se;
4113 }
4114 }
4115
4116 if (literal_symbol && !symbol.empty()) {
4117 symbol = "<lit>" + symbol + "</lit>";
4118 }
4119 // cout << "Atom symbol " << atom.getIdx() << " : " << symbol << endl;
4120 return symbol;
4121 } // namespace RDKit
4122
4123 // ****************************************************************************
getAtomOrientation(const RDKit::Atom & atom) const4124 OrientType MolDraw2D::getAtomOrientation(const RDKit::Atom &atom) const {
4125 // cout << "Atomic " << atom.getAtomicNum() << " degree : "
4126 // << atom.getDegree() << " : " << atom.getTotalNumHs() << endl;
4127 // anything with a slope of more than 70 degrees is vertical. This way,
4128 // the NH in an indole is vertical as RDKit lays it out normally (72ish
4129 // degrees) but the 2 amino groups of c1ccccc1C1CCC(N)(N)CC1 are E and W
4130 // when they are drawn at the bottom of the molecule.
4131 static const double VERT_SLOPE = tan(70.0 * M_PI / 180.0);
4132
4133 auto &mol = atom.getOwningMol();
4134 const Point2D &at1_cds = at_cds_[activeMolIdx_][atom.getIdx()];
4135 Point2D nbr_sum(0.0, 0.0);
4136 // cout << "Nbours for atom : " << at1->getIdx() << endl;
4137 for (const auto &nbri : make_iterator_range(mol.getAtomBonds(&atom))) {
4138 const Bond *bond = mol[nbri];
4139 const Point2D &at2_cds =
4140 at_cds_[activeMolIdx_][bond->getOtherAtomIdx(atom.getIdx())];
4141 nbr_sum += at2_cds - at1_cds;
4142 }
4143
4144 OrientType orient = OrientType::C;
4145 if (atom.getDegree()) {
4146 double islope = 1000.0;
4147 if (fabs(nbr_sum.x) > 1.0e-4) {
4148 islope = nbr_sum.y / nbr_sum.x;
4149 }
4150 if (fabs(islope) <= VERT_SLOPE) {
4151 if (nbr_sum.x > 0.0) {
4152 orient = OrientType::W;
4153 } else {
4154 orient = OrientType::E;
4155 }
4156 } else {
4157 if (nbr_sum.y > 0.0) {
4158 orient = OrientType::N;
4159 } else {
4160 orient = OrientType::S;
4161 }
4162 }
4163 // atoms of single degree should always be either W or E, never N or S. If
4164 // either of the latter, make it E if the slope is close to vertical,
4165 // otherwise have it either as required.
4166 if (orient == OrientType::N || orient == OrientType::S) {
4167 if (atom.getDegree() == 1) {
4168 if (fabs(islope) > VERT_SLOPE) {
4169 orient = OrientType::E;
4170 } else {
4171 if (nbr_sum.x > 0.0) {
4172 orient = OrientType::W;
4173 } else {
4174 orient = OrientType::E;
4175 }
4176 }
4177 } else if (atom.getDegree() == 3) {
4178 // Atoms of degree 3 can sometimes have a bond pointing down with S
4179 // orientation or up with N orientation, which puts the H on the bond.
4180 auto &mol = atom.getOwningMol();
4181 const Point2D &at1_cds = at_cds_[activeMolIdx_][atom.getIdx()];
4182 for (const auto &nbri : make_iterator_range(mol.getAtomBonds(&atom))) {
4183 const Bond *bond = mol[nbri];
4184 const Point2D &at2_cds =
4185 at_cds_[activeMolIdx_][bond->getOtherAtomIdx(atom.getIdx())];
4186 Point2D bond_vec = at2_cds - at1_cds;
4187 double ang = atan(bond_vec.y / bond_vec.x) * 180.0 / M_PI;
4188 if (ang > 80.0 && ang < 100.0 && orient == OrientType::S) {
4189 orient = OrientType::N;
4190 break;
4191 } else if (ang < -80.0 && ang > -100.0 && orient == OrientType::N) {
4192 orient = OrientType::S;
4193 break;
4194 }
4195 }
4196 }
4197 }
4198 } else {
4199 // last check: degree zero atoms from the last three periods should have
4200 // the Hs first
4201 static int HsListedFirstSrc[] = {8, 9, 16, 17, 34, 35, 52, 53, 84, 85};
4202 std::vector<int> HsListedFirst(
4203 HsListedFirstSrc,
4204 HsListedFirstSrc + sizeof(HsListedFirstSrc) / sizeof(int));
4205 if (std::find(HsListedFirst.begin(), HsListedFirst.end(),
4206 atom.getAtomicNum()) != HsListedFirst.end()) {
4207 orient = OrientType::W;
4208 } else {
4209 orient = OrientType::E;
4210 }
4211 }
4212
4213 return orient;
4214 }
4215
4216 // ****************************************************************************
adjustScaleForAtomLabels(const std::vector<int> * highlight_atoms,const map<int,double> * highlight_radii)4217 void MolDraw2D::adjustScaleForAtomLabels(
4218 const std::vector<int> *highlight_atoms,
4219 const map<int, double> *highlight_radii) {
4220 double x_max(x_min_ + x_range_), y_max(y_min_ + y_range_);
4221
4222 for (size_t i = 0; i < atom_syms_[activeMolIdx_].size(); ++i) {
4223 if (!atom_syms_[activeMolIdx_][i].first.empty()) {
4224 double this_x_min, this_y_min, this_x_max, this_y_max;
4225 getStringExtremes(atom_syms_[activeMolIdx_][i].first,
4226 atom_syms_[activeMolIdx_][i].second,
4227 at_cds_[activeMolIdx_][i], this_x_min, this_y_min,
4228 this_x_max, this_y_max);
4229 x_max = std::max(x_max, this_x_max);
4230 x_min_ = std::min(x_min_, this_x_min);
4231 y_max = std::max(y_max, this_y_max);
4232 y_min_ = std::min(y_min_, this_y_min);
4233 }
4234 if (highlight_atoms &&
4235 highlight_atoms->end() !=
4236 find(highlight_atoms->begin(), highlight_atoms->end(), i)) {
4237 Point2D centre;
4238 double xradius, yradius;
4239 // this involves a 2nd call to text_drawer_->getStringRect, but never mind
4240 calcLabelEllipse(i, highlight_radii, centre, xradius, yradius);
4241 double this_x_min = centre.x - xradius;
4242 double this_x_max = centre.x + xradius;
4243 double this_y_min = centre.y - yradius;
4244 double this_y_max = centre.y + yradius;
4245 x_max = std::max(x_max, this_x_max);
4246 x_min_ = std::min(x_min_, this_x_min);
4247 y_max = std::max(y_max, this_y_max);
4248 y_min_ = std::min(y_min_, this_y_min);
4249 }
4250 }
4251
4252 x_range_ = max(x_max - x_min_, x_range_);
4253 y_range_ = max(y_max - y_min_, y_range_);
4254 }
4255
4256 // ****************************************************************************
adjustScaleForRadicals(const ROMol & mol)4257 void MolDraw2D::adjustScaleForRadicals(const ROMol &mol) {
4258 if (scale() != text_drawer_->fontScale()) {
4259 // we've hit max or min font size, so re-compute radical rectangles as
4260 // they'll be too far from the character.
4261 radicals_[activeMolIdx_].clear();
4262 extractRadicals(mol);
4263 }
4264 double x_max(x_min_ + x_range_), y_max(y_min_ + y_range_);
4265
4266 for (auto rad_pair : radicals_[activeMolIdx_]) {
4267 auto rad_rect = rad_pair.first;
4268 x_max = max(x_max, rad_rect->trans_.x + rad_rect->width_ / 2.0);
4269 y_max = max(y_max, rad_rect->trans_.y + rad_rect->height_ / 2.0);
4270 x_min_ = min(x_min_, rad_rect->trans_.x - rad_rect->width_ / 2.0);
4271 y_min_ = min(y_min_, rad_rect->trans_.y - rad_rect->height_ / 2.0);
4272 }
4273
4274 x_range_ = max(x_max - x_min_, x_range_);
4275 y_range_ = max(y_max - y_min_, y_range_);
4276 }
4277
4278 // ****************************************************************************
adjustScaleForAnnotation(const vector<AnnotationType> & notes)4279 void MolDraw2D::adjustScaleForAnnotation(const vector<AnnotationType> ¬es) {
4280 double x_max(x_min_ + x_range_), y_max(y_min_ + y_range_);
4281
4282 for (auto const &pr : notes) {
4283 const auto ¬e_rect = pr.rect_;
4284 double this_x_max = note_rect.trans_.x;
4285 double this_x_min = note_rect.trans_.x;
4286 double this_y_max = note_rect.trans_.y;
4287 double this_y_min = note_rect.trans_.y;
4288 if (pr.align_ == TextAlignType::START) {
4289 this_x_max += note_rect.width_;
4290 } else if (pr.align_ == TextAlignType::END) {
4291 this_x_min -= note_rect.width_;
4292 } else {
4293 this_x_max += note_rect.width_ / 2.0;
4294 this_x_min -= note_rect.width_ / 2.0;
4295 }
4296 this_y_max += note_rect.height_ / 2.0;
4297 this_y_min -= note_rect.height_ / 2.0;
4298
4299 x_max = std::max(x_max, this_x_max);
4300 x_min_ = std::min(x_min_, this_x_min);
4301 y_max = std::max(y_max, this_y_max);
4302 y_min_ = std::min(y_min_, this_y_min);
4303 }
4304 x_range_ = max(x_max - x_min_, x_range_);
4305 y_range_ = max(y_max - y_min_, y_range_);
4306 }
4307
4308 // ****************************************************************************
drawTriangle(const Point2D & cds1,const Point2D & cds2,const Point2D & cds3)4309 void MolDraw2D::drawTriangle(const Point2D &cds1, const Point2D &cds2,
4310 const Point2D &cds3) {
4311 std::vector<Point2D> pts;
4312 if (!drawOptions().comicMode) {
4313 pts = {cds1, cds2, cds3};
4314 } else {
4315 auto lpts = MolDraw2D_detail::handdrawnLine(cds1, cds2, scale_);
4316 std::move(lpts.begin(), lpts.end(), std::back_inserter(pts));
4317 lpts = MolDraw2D_detail::handdrawnLine(cds2, cds3, scale_);
4318 std::move(lpts.begin(), lpts.end(), std::back_inserter(pts));
4319 lpts = MolDraw2D_detail::handdrawnLine(cds3, cds1, scale_);
4320 std::move(lpts.begin(), lpts.end(), std::back_inserter(pts));
4321 }
4322 drawPolygon(pts);
4323 };
4324
4325 // ****************************************************************************
drawArrow(const Point2D & arrowBegin,const Point2D & arrowEnd,bool asPolygon,double frac,double angle)4326 void MolDraw2D::drawArrow(const Point2D &arrowBegin, const Point2D &arrowEnd,
4327 bool asPolygon, double frac, double angle) {
4328 Point2D delta = arrowBegin - arrowEnd;
4329 double cos_angle = std::cos(angle), sin_angle = std::sin(angle);
4330
4331 Point2D p1 = arrowEnd;
4332 p1.x += frac * (delta.x * cos_angle + delta.y * sin_angle);
4333 p1.y += frac * (delta.y * cos_angle - delta.x * sin_angle);
4334
4335 Point2D p2 = arrowEnd;
4336 p2.x += frac * (delta.x * cos_angle - delta.y * sin_angle);
4337 p2.y += frac * (delta.y * cos_angle + delta.x * sin_angle);
4338
4339 drawLine(arrowBegin, arrowEnd);
4340 if (!asPolygon) {
4341 drawLine(arrowEnd, p1);
4342 drawLine(arrowEnd, p2);
4343 } else {
4344 std::vector<Point2D> pts = {p1, arrowEnd, p2};
4345 bool fps = fillPolys();
4346 setFillPolys(true);
4347 drawPolygon(pts);
4348 setFillPolys(fps);
4349 }
4350 }
4351
4352 // ****************************************************************************
tabulaRasa()4353 void MolDraw2D::tabulaRasa() {
4354 scale_ = 1.0;
4355 text_drawer_->setFontScale(1.0);
4356 x_trans_ = y_trans_ = 0.0;
4357 x_offset_ = y_offset_ = 0;
4358 d_metadata.clear();
4359 d_numMetadataEntries = 0;
4360 setActiveAtmIdx();
4361 }
4362
4363 // ****************************************************************************
drawEllipse(const Point2D & cds1,const Point2D & cds2)4364 void MolDraw2D::drawEllipse(const Point2D &cds1, const Point2D &cds2) {
4365 std::vector<Point2D> pts;
4366 MolDraw2D_detail::arcPoints(cds1, cds2, pts, 0, 360);
4367 drawPolygon(pts);
4368 }
4369
4370 // ****************************************************************************
drawArc(const Point2D & centre,double radius,double ang1,double ang2)4371 void MolDraw2D::drawArc(const Point2D ¢re, double radius, double ang1,
4372 double ang2) {
4373 drawArc(centre, radius, radius, ang1, ang2);
4374 }
4375
4376 // ****************************************************************************
drawArc(const Point2D & centre,double xradius,double yradius,double ang1,double ang2)4377 void MolDraw2D::drawArc(const Point2D ¢re, double xradius, double yradius,
4378 double ang1, double ang2) {
4379 std::vector<Point2D> pts;
4380 // 5 degree increments should be plenty, as the circles are probably
4381 // going to be small.
4382 int num_steps = 1 + int((ang2 - ang1) / 5.0);
4383 double ang_incr = double((ang2 - ang1) / num_steps) * M_PI / 180.0;
4384 double start_ang_rads = ang2 * M_PI / 180.0;
4385 for (int i = 0; i <= num_steps; ++i) {
4386 double ang = start_ang_rads + double(i) * ang_incr;
4387 double x = centre.x + xradius * cos(ang);
4388 double y = centre.y + yradius * sin(ang);
4389 pts.emplace_back(Point2D(x, y));
4390 }
4391
4392 if (fillPolys()) {
4393 // otherwise it draws an arc back to the pts.front() rather than filling
4394 // in the sector.
4395 pts.emplace_back(centre);
4396 }
4397 drawPolygon(pts);
4398 }
4399
4400 // ****************************************************************************
drawRect(const Point2D & cds1,const Point2D & cds2)4401 void MolDraw2D::drawRect(const Point2D &cds1, const Point2D &cds2) {
4402 std::vector<Point2D> pts(4);
4403 pts[0] = cds1;
4404 pts[1] = Point2D(cds1.x, cds2.y);
4405 pts[2] = cds2;
4406 pts[3] = Point2D(cds2.x, cds1.y);
4407 // if fillPolys() is false, it doesn't close the polygon because of
4408 // its use for drawing filled or open ellipse segments.
4409 if (!fillPolys()) {
4410 pts.emplace_back(cds1);
4411 }
4412 drawPolygon(pts);
4413 }
4414
drawWavyLine(const Point2D & cds1,const Point2D & cds2,const DrawColour & col1,const DrawColour & col2,unsigned int nSegments,double vertOffset)4415 void MolDraw2D::drawWavyLine(const Point2D &cds1, const Point2D &cds2,
4416 const DrawColour &col1, const DrawColour &col2,
4417 unsigned int nSegments, double vertOffset) {
4418 RDUNUSED_PARAM(nSegments);
4419 RDUNUSED_PARAM(vertOffset);
4420 drawLine(cds1, cds2, col1, col2);
4421 }
4422
4423 // ****************************************************************************
4424 // we draw the line at cds2, perpendicular to the line cds1-cds2
drawAttachmentLine(const Point2D & cds1,const Point2D & cds2,const DrawColour & col,double len,unsigned int nSegments)4425 void MolDraw2D::drawAttachmentLine(const Point2D &cds1, const Point2D &cds2,
4426 const DrawColour &col, double len,
4427 unsigned int nSegments) {
4428 Point2D perp = calcPerpendicular(cds1, cds2);
4429 Point2D p1 = Point2D(cds2.x - perp.x * len / 2, cds2.y - perp.y * len / 2);
4430 Point2D p2 = Point2D(cds2.x + perp.x * len / 2, cds2.y + perp.y * len / 2);
4431 drawWavyLine(p1, p2, col, col, nSegments);
4432 }
4433
4434 // ****************************************************************************
doLinesIntersect(const Point2D & l1s,const Point2D & l1f,const Point2D & l2s,const Point2D & l2f,Point2D * ip)4435 bool doLinesIntersect(const Point2D &l1s, const Point2D &l1f,
4436 const Point2D &l2s, const Point2D &l2f, Point2D *ip) {
4437 // using spell from answer 2 of
4438 // https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
4439 double s1_x = l1f.x - l1s.x;
4440 double s1_y = l1f.y - l1s.y;
4441 double s2_x = l2f.x - l2s.x;
4442 double s2_y = l2f.y - l2s.y;
4443
4444 double d = (-s2_x * s1_y + s1_x * s2_y);
4445 if (d == 0.0) {
4446 // parallel lines.
4447 return false;
4448 }
4449 double s, t;
4450 s = (-s1_y * (l1s.x - l2s.x) + s1_x * (l1s.y - l2s.y)) / d;
4451 t = (s2_x * (l1s.y - l2s.y) - s2_y * (l1s.x - l2s.x)) / d;
4452
4453 if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
4454 if (ip) {
4455 ip->x = l1s.x + t * s1_x;
4456 ip->y = l1s.y + t * s1_y;
4457 }
4458 return true;
4459 }
4460
4461 return false;
4462 }
4463
4464 // ****************************************************************************
doesLineIntersectLabel(const Point2D & ls,const Point2D & lf,const StringRect & lab_rect,double padding)4465 bool doesLineIntersectLabel(const Point2D &ls, const Point2D &lf,
4466 const StringRect &lab_rect, double padding) {
4467 Point2D tl, tr, br, bl;
4468 lab_rect.calcCorners(tl, tr, br, bl, padding);
4469
4470 // first check if line is completely inside label. Unlikely, but who
4471 // knows?
4472 if (ls.x >= tl.x && ls.x <= br.x && lf.x >= tl.x && lf.x <= br.x &&
4473 ls.y <= tl.y && ls.y >= br.y && lf.y <= tl.y && lf.y >= br.y) {
4474 return true;
4475 }
4476 if (doLinesIntersect(ls, lf, tl, tr) || doLinesIntersect(ls, lf, tr, br) ||
4477 doLinesIntersect(ls, lf, br, bl) || doLinesIntersect(ls, lf, bl, tl)) {
4478 return true;
4479 }
4480 return false;
4481 }
4482
4483 } // namespace RDKit
4484