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> &region : 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 &centre, 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 &note) {
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 &note) {
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 &note) {
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 &note) {
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 &note_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 &note_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 &note_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 &note_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 &note_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> &notes) {
4280   double x_max(x_min_ + x_range_), y_max(y_min_ + y_range_);
4281 
4282   for (auto const &pr : notes) {
4283     const auto &note_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 &centre, 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 &centre, 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