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, "\"", """);
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