1 //
2 //  Copyright (C) 2015-2019 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 // derived from Dave Cosgrove's MolDraw2D
11 //
12 
13 #include <GraphMol/MolDraw2D/MolDraw2DSVG.h>
14 #include <GraphMol/MolDraw2D/DrawText.h>
15 #include <GraphMol/SmilesParse/SmilesWrite.h>
16 #include <Geometry/point.h>
17 #ifdef RDK_BUILD_FREETYPE_SUPPORT
18 #include <GraphMol/MolDraw2D/DrawTextFTSVG.h>
19 #else
20 #include <GraphMol/MolDraw2D/DrawTextSVG.h>
21 #endif
22 
23 #include <boost/format.hpp>
24 #include <boost/algorithm/string.hpp>
25 #include <sstream>
26 
27 namespace RDKit {
28 namespace {
29 template <class t_obj>
outputTagClasses(const t_obj * obj,std::ostream & d_os,const std::string & d_activeClass)30 void outputTagClasses(const t_obj *obj, std::ostream &d_os,
31                       const std::string &d_activeClass) {
32   if (!d_activeClass.empty()) {
33     d_os << " " << d_activeClass;
34   }
35   if (obj->hasProp("_tagClass")) {
36     std::string value;
37     obj->getProp("_tagClass", value);
38     std::replace(value.begin(), value.end(), '\"', '_');
39     std::replace(value.begin(), value.end(), '\'', '_');
40     std::replace(value.begin(), value.end(), '.', '_');
41     d_os << " " << value;
42   }
43 }
44 
45 template <class t_obj>
outputMetaData(const t_obj * obj,std::ostream & d_os,const std::string & d_activeClass)46 void outputMetaData(const t_obj *obj, std::ostream &d_os,
47                     const std::string &d_activeClass) {
48   RDUNUSED_PARAM(d_activeClass);
49   std::string value;
50   for (const auto &prop : obj->getPropList()) {
51     if (prop.length() < 11 || prop.rfind("_metaData-", 0) != 0) {
52       continue;
53     }
54     obj->getProp(prop, value);
55     boost::replace_all(value, "\"", "&quot;");
56     d_os << " " << prop.substr(10) << "=\"" << value << "\"";
57   }
58 }
59 }  // namespace
60 
DrawColourToSVG(const DrawColour & col)61 std::string DrawColourToSVG(const DrawColour &col) {
62   const char *convert = "0123456789ABCDEF";
63   std::string res(7, ' ');
64   res[0] = '#';
65   unsigned int v;
66   unsigned int i = 1;
67   v = int(255 * col.r);
68   if (v > 255) {
69     throw ValueErrorException(
70         "elements of the color should be between 0 and 1");
71   }
72   res[i++] = convert[v / 16];
73   res[i++] = convert[v % 16];
74   v = int(255 * col.g);
75   if (v > 255) {
76     throw ValueErrorException(
77         "elements of the color should be between 0 and 1");
78   }
79   res[i++] = convert[v / 16];
80   res[i++] = convert[v % 16];
81   v = int(255 * col.b);
82   if (v > 255) {
83     throw ValueErrorException(
84         "elements of the color should be between 0 and 1");
85   }
86   res[i++] = convert[v / 16];
87   res[i++] = convert[v % 16];
88   return res;
89 }
90 
91 // ****************************************************************************
initDrawing()92 void MolDraw2DSVG::initDrawing() {
93   d_os << "<?xml version='1.0' encoding='iso-8859-1'?>\n";
94   d_os << "<svg version='1.1' baseProfile='full'\n      \
95         xmlns='http://www.w3.org/2000/svg'\n              \
96         xmlns:rdkit='http://www.rdkit.org/xml'\n              \
97         xmlns:xlink='http://www.w3.org/1999/xlink'\n          \
98         xml:space='preserve'\n";
99   d_os << boost::format{"width='%1%px' height='%2%px' viewBox='0 0 %1% %2%'>\n"}
100       % width() % height();
101   d_os << "<!-- END OF HEADER -->\n";
102 
103   // d_os<<"<g transform='translate("<<width()*.05<<","<<height()*.05<<")
104   // scale(.85,.85)'>";
105 }
106 
107 // ****************************************************************************
initTextDrawer(bool noFreetype)108 void MolDraw2DSVG::initTextDrawer(bool noFreetype) {
109   double max_fnt_sz = drawOptions().maxFontSize;
110   double min_fnt_sz = drawOptions().minFontSize;
111 
112   if (noFreetype) {
113     text_drawer_.reset(
114         new DrawTextSVG(max_fnt_sz, min_fnt_sz, d_os, d_activeClass));
115   } else {
116 #ifdef RDK_BUILD_FREETYPE_SUPPORT
117     try {
118       text_drawer_.reset(new DrawTextFTSVG(
119           max_fnt_sz, min_fnt_sz, drawOptions().fontFile, d_os, d_activeClass));
120     } catch (std::runtime_error &e) {
121       BOOST_LOG(rdWarningLog)
122           << e.what() << std::endl
123           << "Falling back to native SVG text handling." << std::endl;
124       text_drawer_.reset(
125           new DrawTextSVG(max_fnt_sz, min_fnt_sz, d_os, d_activeClass));
126     }
127 #else
128     text_drawer_.reset(
129         new DrawTextSVG(max_fnt_sz, min_fnt_sz, d_os, d_activeClass));
130 #endif
131   }
132 }
133 
134 // ****************************************************************************
finishDrawing()135 void MolDraw2DSVG::finishDrawing() {
136   // d_os << "</g>";
137   d_os << "</svg>\n";
138 }
139 
140 // ****************************************************************************
setColour(const DrawColour & col)141 void MolDraw2DSVG::setColour(const DrawColour &col) {
142   MolDraw2D::setColour(col);
143 }
144 
145 // ****************************************************************************
drawWavyLine(const Point2D & cds1,const Point2D & cds2,const DrawColour & col1,const DrawColour & col2,unsigned int nSegments,double vertOffset)146 void MolDraw2DSVG::drawWavyLine(const Point2D &cds1, const Point2D &cds2,
147                                 const DrawColour &col1, const DrawColour &col2,
148                                 unsigned int nSegments, double vertOffset) {
149   PRECONDITION(nSegments > 1, "too few segments");
150   RDUNUSED_PARAM(col2);
151 
152   if (nSegments % 2) {
153     ++nSegments;  // we're going to assume an even number of segments
154   }
155   setColour(col1);
156 
157   Point2D delta = (cds2 - cds1);
158   Point2D perp(delta.y, -delta.x);
159   perp.normalize();
160   perp *= vertOffset;
161   delta /= nSegments;
162 
163   Point2D c1 = getDrawCoords(cds1);
164 
165   std::string col = DrawColourToSVG(colour());
166   double width = getDrawLineWidth();
167   d_os << "<path ";
168   outputClasses();
169   d_os << "d='M" << c1.x << "," << c1.y;
170   for (unsigned int i = 0; i < nSegments; ++i) {
171     Point2D startpt = cds1 + delta * i;
172     Point2D segpt = getDrawCoords(startpt + delta);
173     Point2D cpt1 =
174         getDrawCoords(startpt + delta / 3. + perp * (i % 2 ? -1 : 1));
175     Point2D cpt2 =
176         getDrawCoords(startpt + delta * 2. / 3. + perp * (i % 2 ? -1 : 1));
177     d_os << " C" << cpt1.x << "," << cpt1.y << " " << cpt2.x << "," << cpt2.y
178          << " " << segpt.x << "," << segpt.y;
179   }
180   d_os << "' ";
181 
182   d_os << "style='fill:none;stroke:" << col
183        << ";stroke-width:" << boost::format("%.1f") % width
184        << "px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
185        << "'";
186   d_os << " />\n";
187 }
188 
189 // ****************************************************************************
drawBond(const ROMol & mol,const Bond * bond,int at1_idx,int at2_idx,const std::vector<int> * highlight_atoms,const std::map<int,DrawColour> * highlight_atom_map,const std::vector<int> * highlight_bonds,const std::map<int,DrawColour> * highlight_bond_map,const std::vector<std::pair<DrawColour,DrawColour>> * bond_colours)190 void MolDraw2DSVG::drawBond(
191     const ROMol &mol, const Bond *bond, int at1_idx, int at2_idx,
192     const std::vector<int> *highlight_atoms,
193     const std::map<int, DrawColour> *highlight_atom_map,
194     const std::vector<int> *highlight_bonds,
195     const std::map<int, DrawColour> *highlight_bond_map,
196     const std::vector<std::pair<DrawColour, DrawColour>> *bond_colours) {
197   PRECONDITION(bond, "bad bond");
198   std::string o_class = d_activeClass;
199   if (!d_activeClass.empty()) {
200     d_activeClass += " ";
201   }
202   d_activeClass += boost::str(boost::format("bond-%d") % bond->getIdx());
203   MolDraw2D::drawBond(mol, bond, at1_idx, at2_idx, highlight_atoms,
204                       highlight_atom_map, highlight_bonds, highlight_bond_map,
205                       bond_colours);
206   d_activeClass = o_class;
207 };
208 
209 // ****************************************************************************
drawAtomLabel(int atom_num,const DrawColour & draw_colour)210 void MolDraw2DSVG::drawAtomLabel(int atom_num, const DrawColour &draw_colour) {
211   std::string o_class = d_activeClass;
212   if (!d_activeClass.empty()) {
213     d_activeClass += " ";
214   }
215   d_activeClass += boost::str(boost::format("atom-%d") % atom_num);
216   MolDraw2D::drawAtomLabel(atom_num, draw_colour);
217   d_activeClass = o_class;
218 }
219 
220 // ****************************************************************************
drawAnnotation(const AnnotationType & annot)221 void MolDraw2DSVG::drawAnnotation(const AnnotationType &annot) {
222   std::string o_class = d_activeClass;
223   if (!d_activeClass.empty()) {
224     d_activeClass += " ";
225   }
226   d_activeClass += "note";
227   MolDraw2D::drawAnnotation(annot);
228   d_activeClass = o_class;
229 }
230 
231 // ****************************************************************************
drawLine(const Point2D & cds1,const Point2D & cds2)232 void MolDraw2DSVG::drawLine(const Point2D &cds1, const Point2D &cds2) {
233   Point2D c1 = getDrawCoords(cds1);
234   Point2D c2 = getDrawCoords(cds2);
235   std::string col = DrawColourToSVG(colour());
236   double width = getDrawLineWidth();
237   std::string dashString = "";
238   const DashPattern &dashes = dash();
239   if (dashes.size()) {
240     std::stringstream dss;
241     dss << ";stroke-dasharray:";
242     std::copy(dashes.begin(), dashes.end() - 1,
243               std::ostream_iterator<double>(dss, ","));
244     dss << dashes.back();
245     dashString = dss.str();
246   }
247   d_os << "<path ";
248   outputClasses();
249   d_os << "d='M " << c1.x << "," << c1.y << " L " << c2.x << "," << c2.y
250        << "' ";
251   d_os << "style='fill:none;fill-rule:evenodd;stroke:" << col
252        << ";stroke-width:" << boost::format("%.1f") % width
253        << "px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
254        << dashString << "'";
255   d_os << " />\n";
256 }
257 
258 // ****************************************************************************
drawPolygon(const std::vector<Point2D> & cds)259 void MolDraw2DSVG::drawPolygon(const std::vector<Point2D> &cds) {
260   PRECONDITION(cds.size() >= 3, "must have at least three points");
261 
262   std::string col = DrawColourToSVG(colour());
263   double width = getDrawLineWidth();
264   std::string dashString = "";
265   d_os << "<path ";
266   outputClasses();
267   d_os << "d='M";
268   Point2D c0 = getDrawCoords(cds[0]);
269   d_os << " " << c0.x << "," << c0.y;
270   for (unsigned int i = 1; i < cds.size(); ++i) {
271     Point2D ci = getDrawCoords(cds[i]);
272     d_os << " L " << ci.x << "," << ci.y;
273   }
274   if (fillPolys()) {
275     // the Z closes the path which we don't want for unfilled polygons
276     d_os << " Z' style='fill:" << col
277          << ";fill-rule:evenodd;fill-opacity:" << colour().a << ";";
278   } else {
279     d_os << "' style='fill:none;";
280   }
281 
282   d_os << "stroke:" << col << ";stroke-width:" << boost::format("%.1f") % width
283        << "px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:"
284        << colour().a << ";" << dashString << "'";
285   d_os << " />\n";
286 }
287 
288 // ****************************************************************************
drawEllipse(const Point2D & cds1,const Point2D & cds2)289 void MolDraw2DSVG::drawEllipse(const Point2D &cds1, const Point2D &cds2) {
290   Point2D c1 = getDrawCoords(cds1);
291   Point2D c2 = getDrawCoords(cds2);
292   double w = c2.x - c1.x;
293   double h = c2.y - c1.y;
294   double cx = c1.x + w / 2;
295   double cy = c1.y + h / 2;
296   w = w > 0 ? w : -1 * w;
297   h = h > 0 ? h : -1 * h;
298 
299   std::string col = DrawColourToSVG(colour());
300   double width = getDrawLineWidth();
301   std::string dashString = "";
302   d_os << "<ellipse"
303        << " cx='" << cx << "'"
304        << " cy='" << cy << "'"
305        << " rx='" << w / 2 << "'"
306        << " ry='" << h / 2 << "' ";
307   outputClasses();
308   d_os << " style='";
309   if (fillPolys()) {
310     d_os << "fill:" << col << ";fill-rule:evenodd;";
311   } else {
312     d_os << "fill:none;";
313   }
314 
315   d_os << "stroke:" << col << ";stroke-width:" << boost::format("%.1f") % width
316        << "px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
317        << dashString << "'";
318   d_os << " />\n";
319 }
320 
321 // ****************************************************************************
clearDrawing()322 void MolDraw2DSVG::clearDrawing() {
323   std::string col = DrawColourToSVG(drawOptions().backgroundColour);
324   d_os << "<rect";
325   d_os << " style='opacity:1.0;fill:" << col << ";stroke:none'";
326   d_os << " width='" << width() << "' height='" << height() << "'";
327   d_os << " x='" << offset().x << "' y='" << offset().y << "'";
328   d_os << "> </rect>\n";
329 }
330 
331 // ****************************************************************************
332 static const char *RDKIT_SVG_VERSION = "0.9";
addMoleculeMetadata(const ROMol & mol,int confId) const333 void MolDraw2DSVG::addMoleculeMetadata(const ROMol &mol, int confId) const {
334   PRECONDITION(d_os, "no output stream");
335   d_os << "<metadata>" << std::endl;
336   d_os << "<rdkit:mol"
337        << " xmlns:rdkit = \"http://www.rdkit.org/xml\""
338        << " version=\"" << RDKIT_SVG_VERSION << "\""
339        << ">" << std::endl;
340   for (const auto atom : mol.atoms()) {
341     d_os << "<rdkit:atom idx=\"" << atom->getIdx() + 1 << "\"";
342     bool doKekule = false, allHsExplicit = true, isomericSmiles = true;
343     d_os << " atom-smiles=\""
344          << SmilesWrite::GetAtomSmiles(atom, doKekule, nullptr, allHsExplicit,
345                                        isomericSmiles)
346          << "\"";
347     auto tag = boost::str(boost::format("_atomdrawpos_%d") % confId);
348 
349     const Conformer &conf = mol.getConformer(confId);
350     RDGeom::Point3D pos = conf.getAtomPos(atom->getIdx());
351 
352     Point2D dpos(pos.x, pos.y);
353     if (atom->hasProp(tag)) {
354       dpos = atom->getProp<Point2D>(tag);
355     } else {
356       dpos = getDrawCoords(dpos);
357     }
358     d_os << " drawing-x=\"" << dpos.x << "\""
359          << " drawing-y=\"" << dpos.y << "\"";
360     d_os << " x=\"" << pos.x << "\""
361          << " y=\"" << pos.y << "\""
362          << " z=\"" << pos.z << "\"";
363 
364     outputMetaData(atom, d_os, d_activeClass);
365 
366     d_os << " />" << std::endl;
367   }
368   for (const auto bond : mol.bonds()) {
369     d_os << "<rdkit:bond idx=\"" << bond->getIdx() + 1 << "\"";
370     d_os << " begin-atom-idx=\"" << bond->getBeginAtomIdx() + 1 << "\"";
371     d_os << " end-atom-idx=\"" << bond->getEndAtomIdx() + 1 << "\"";
372     bool doKekule = false, allBondsExplicit = true;
373     d_os << " bond-smiles=\""
374          << SmilesWrite::GetBondSmiles(bond, -1, doKekule, allBondsExplicit)
375          << "\"";
376 
377     outputMetaData(bond, d_os, d_activeClass);
378 
379     d_os << " />" << std::endl;
380   }
381   d_os << "</rdkit:mol></metadata>" << std::endl;
382 }
383 
addMoleculeMetadata(const std::vector<ROMol * > & mols,const std::vector<int> confIds) const384 void MolDraw2DSVG::addMoleculeMetadata(const std::vector<ROMol *> &mols,
385                                        const std::vector<int> confIds) const {
386   for (unsigned int i = 0; i < mols.size(); ++i) {
387     int confId = -1;
388     if (confIds.size() == mols.size()) {
389       confId = confIds[i];
390     }
391     addMoleculeMetadata(*(mols[i]), confId);
392   }
393 };
394 
tagAtoms(const ROMol & mol,double radius,const std::map<std::string,std::string> & events)395 void MolDraw2DSVG::tagAtoms(const ROMol &mol, double radius,
396                             const std::map<std::string, std::string> &events) {
397   PRECONDITION(d_os, "no output stream");
398   // first bonds so that they are under the atoms
399   for (const auto &bond : mol.bonds()) {
400     const auto this_idx = bond->getIdx();
401     const auto a_idx1 = bond->getBeginAtomIdx();
402     const auto a_idx2 = bond->getEndAtomIdx();
403     const auto a1pos = getDrawCoords(atomCoords()[bond->getBeginAtomIdx()]);
404     const auto a2pos = getDrawCoords(atomCoords()[bond->getEndAtomIdx()]);
405     const auto width = 2 + lineWidth();
406     if (drawOptions().splitBonds) {
407       const auto midp = (a1pos + a2pos) / 2;
408       // from begin to mid
409       d_os << "<path "
410            << " d='M " << a1pos.x << "," << a1pos.y << " L " << midp.x << ","
411            << midp.y << "'";
412       d_os << " class='bond-selector bond-" << this_idx << " atom-" << a_idx1;
413       outputTagClasses(bond, d_os, d_activeClass);
414       d_os << "'";
415       d_os << " style='fill:#fff;stroke:#fff;stroke-width:"
416            << boost::format("%.1f") % width
417            << "px;fill-opacity:0;"
418               "stroke-opacity:0' ";
419       d_os << "/>\n";
420       // mid to end
421       d_os << "<path "
422            << " d='M " << midp.x << "," << midp.y << " L " << a2pos.x << ","
423            << a2pos.y << "'";
424       d_os << " class='bond-selector bond-" << this_idx << " atom-" << a_idx2;
425       outputTagClasses(bond, d_os, d_activeClass);
426       d_os << "'";
427       d_os << " style='fill:#fff;stroke:#fff;stroke-width:"
428            << boost::format("%.1f") % width
429            << "px;fill-opacity:0;"
430               "stroke-opacity:0' ";
431       d_os << "/>\n";
432     } else {
433       d_os << "<path "
434            << " d='M " << a1pos.x << "," << a1pos.y << " L " << a2pos.x << ","
435            << a2pos.y << "'";
436       d_os << " class='bond-selector bond-" << this_idx << " atom-" << a_idx1
437            << " atom-" << a_idx2;
438       outputTagClasses(bond, d_os, d_activeClass);
439       d_os << "'";
440       d_os << " style='fill:#fff;stroke:#fff;stroke-width:"
441            << boost::format("%.1f") % width
442            << "px;fill-opacity:0;"
443               "stroke-opacity:0' ";
444       d_os << "/>\n";
445     }
446   }
447   for (const auto &at : mol.atoms()) {
448     auto this_idx = at->getIdx();
449     auto pos = getDrawCoords(atomCoords()[this_idx]);
450     d_os << "<circle "
451          << " cx='" << pos.x << "'"
452          << " cy='" << pos.y << "'"
453          << " r='" << (scale() * radius) << "'";
454     d_os << " class='atom-selector atom-" << this_idx;
455     outputTagClasses(at, d_os, d_activeClass);
456     d_os << "'";
457     d_os << " style='fill:#fff;stroke:#fff;stroke-width:1px;fill-opacity:0;"
458             "stroke-opacity:0' ";
459     for (const auto &event : events) {
460       d_os << " " << event.first << "='" << event.second << "(" << this_idx
461            << ");"
462            << "'";
463     }
464     d_os << "/>\n";
465   }
466 }
467 
outputClasses()468 void MolDraw2DSVG::outputClasses() {
469   if (d_activeClass.empty() && !hasActiveAtmIdx()) return;
470 
471   d_os << "class='";
472   if (!d_activeClass.empty()) {
473     d_os << d_activeClass;
474   }
475   if (!hasActiveAtmIdx()) {
476     d_os << "' ";
477     return;
478   }
479   d_os << (!d_activeClass.empty() ? " " : "");
480   const auto aidx1 = getActiveAtmIdx1();
481   if (aidx1 >= 0) {
482     d_os << "atom-" << aidx1;
483   }
484   const auto aidx2 = getActiveAtmIdx2();
485   if (aidx2 >= 0 && aidx2 != aidx1) {
486     d_os << " atom-" << aidx2;
487   }
488   d_os << "' ";
489 }
490 
491 }  // namespace RDKit
492