Files
rdkit/Code/GraphMol/MolDraw2D/DrawText.cpp
Greg Landrum fbc76aea18 MolDraw2D: collected tweaks and bug fixes (#3930)
* Fixes #2976
Stops re-generating reaction coordinates when that's not required

* add better test

* try to deal with bad font sizes from mol files

* one more expected result update

* try to be smarter about font changes

* suppress a compiler warning

* remove debug output

* update some expected test results
2021-03-17 04:47:44 +01:00

722 lines
24 KiB
C++

//
// @@ All Rights Reserved @@
// This file is part of the RDKit.
// The contents are covered by the terms of the BSD license
// which is included in the file license.txt, found at the root
// of the RDKit source tree.
//
// Original author: David Cosgrove (CozChemIx) on 29/04/2020.
//
#include <GraphMol/MolDraw2D/DrawText.h>
namespace RDKit {
// ****************************************************************************
DrawText::DrawText(double max_fnt_sz, double min_fnt_sz)
: colour_(DrawColour(0.0, 0.0, 0.0)),
font_scale_(1.0),
max_font_size_(max_fnt_sz),
min_font_size_(min_fnt_sz) {}
// ****************************************************************************
DrawColour const &DrawText::colour() const { return colour_; }
// ****************************************************************************
void DrawText::setColour(const DrawColour &col) { colour_ = col; }
// ****************************************************************************
double DrawText::fontSize() const { return fontScale() * baseFontSize(); }
// ****************************************************************************
void DrawText::setFontSize(double new_size) {
if (new_size < minFontSize()) {
BOOST_LOG(rdWarningLog)
<< "The new font size " << new_size << " is below the current minimum ("
<< minFontSize() << ")." << std::endl;
} else if (new_size > maxFontSize()) {
BOOST_LOG(rdWarningLog)
<< "The new font size " << new_size << " is above the current maximum ("
<< maxFontSize() << ")." << std::endl;
}
double new_scale = new_size / baseFontSize();
setFontScale(new_scale);
}
// ****************************************************************************
double DrawText::baseFontSize() const { return base_font_size_; }
// ****************************************************************************
void DrawText::setBaseFontSize(double val) { base_font_size_ = val; }
// ****************************************************************************
double DrawText::maxFontSize() const { return max_font_size_; }
// ****************************************************************************
void DrawText::setMaxFontSize(double new_max) { max_font_size_ = new_max; }
// ****************************************************************************
double DrawText::minFontSize() const { return min_font_size_; }
// ****************************************************************************
void DrawText::setMinFontSize(double new_min) { min_font_size_ = new_min; }
// ****************************************************************************
double DrawText::fontScale() const { return font_scale_; }
// ****************************************************************************
void DrawText::setFontScale(double new_scale) {
font_scale_ = new_scale;
double nfs = fontSize();
if (max_font_size_ != -1 &&
nfs * (baseFontSize() / FONT_SIZE) > max_font_size_) {
font_scale_ = max_font_size_ / baseFontSize();
}
if (min_font_size_ != -1 &&
nfs * (baseFontSize() / FONT_SIZE) < min_font_size_) {
font_scale_ = min_font_size_ / baseFontSize();
}
}
// ****************************************************************************
void DrawText::drawString(const std::string &str, const Point2D &cds,
TextAlignType talign) {
std::vector<std::shared_ptr<StringRect>> rects;
std::vector<TextDrawType> draw_modes;
std::vector<char> draw_chars;
getStringRects(str, rects, draw_modes, draw_chars);
alignString(talign, draw_modes, rects);
drawChars(cds, rects, draw_modes, draw_chars);
}
// ****************************************************************************
void DrawText::drawString(const std::string &label, const Point2D &cds,
OrientType orient) {
std::vector<std::shared_ptr<StringRect>> rects;
std::vector<TextDrawType> draw_modes;
std::vector<char> draw_chars;
getStringRects(label, orient, rects, draw_modes, draw_chars);
drawChars(cds, rects, draw_modes, draw_chars);
}
// ****************************************************************************
void DrawText::adjustLineForString(const std::string &label, OrientType orient,
const Point2D &end1, Point2D &end2) const {
std::vector<std::shared_ptr<StringRect>> rects;
std::vector<TextDrawType> draw_modes;
std::vector<char> draw_chars;
Point2D lab_pos = end2;
getStringRects(label, orient, rects, draw_modes, draw_chars);
double bond_len = (end1 - end2).length();
for (size_t i = 0; i < rects.size(); ++i) {
const auto &r = rects[i];
r->trans_ += lab_pos;
Point2D tl, tr, bl, br;
r->calcCorners(tl, tr, br, bl, 0.025 * bond_len);
std::unique_ptr<Point2D> ip(new Point2D);
// if it's a wide label, such as C:7, the bond can intersect
// more than 1 side of the rectangle, so check them all.
if (doLinesIntersect(end2, end1, tl, tr, ip.get())) {
end2 = *ip;
}
if (doLinesIntersect(end2, end1, tr, br, ip.get())) {
end2 = *ip;
}
if (doLinesIntersect(end2, end1, br, bl, ip.get())) {
end2 = *ip;
}
if (doLinesIntersect(end2, end1, bl, tl, ip.get())) {
end2 = *ip;
}
}
}
// ****************************************************************************
void DrawText::drawStringRects(const std::string &label, OrientType orient,
const Point2D &cds, MolDraw2D &mol_draw) const {
std::vector<std::shared_ptr<StringRect>> rects;
std::vector<TextDrawType> draw_modes;
std::vector<char> draw_chars;
size_t i = 0;
getStringRects(label, orient, rects, draw_modes, draw_chars);
for (auto r : rects) {
r->trans_.x += cds.x;
r->trans_.y += cds.y;
Point2D tl, tr, br, bl;
r->calcCorners(tl, tr, br, bl, 0.0);
tl = mol_draw.getAtomCoords(std::make_pair(tl.x, tl.y));
tr = mol_draw.getAtomCoords(std::make_pair(tr.x, tr.y));
br = mol_draw.getAtomCoords(std::make_pair(br.x, br.y));
bl = mol_draw.getAtomCoords(std::make_pair(bl.x, bl.y));
mol_draw.setColour(DrawColour(1.0, 0.0, 0.0));
mol_draw.drawLine(tl, tr);
mol_draw.setColour(DrawColour(0.0, 1.0, 0.0));
mol_draw.drawLine(tr, br);
mol_draw.setColour(DrawColour(0.0, 0.0, 1.0));
mol_draw.drawLine(br, bl);
mol_draw.setColour(DrawColour(0.0, 0.95, 0.95));
mol_draw.drawLine(bl, tl);
++i;
}
}
// ****************************************************************************
bool DrawText::doesRectIntersect(const std::string &label, OrientType orient,
const Point2D &cds,
const StringRect &rect) const {
if (label.empty()) {
return false;
}
std::vector<std::shared_ptr<StringRect>> rects;
std::vector<TextDrawType> draw_modes;
std::vector<char> draw_chars;
getStringRects(label, orient, rects, draw_modes, draw_chars);
return doesRectIntersect(rects, cds, rect);
}
// ****************************************************************************
bool DrawText::doesRectIntersect(
const std::vector<std::shared_ptr<StringRect>> &rects, const Point2D &cds,
const StringRect &rect) const {
for (auto r : rects) {
StringRect nr(*r);
nr.trans_ += cds;
if (nr.doesItIntersect(rect)) {
return true;
}
}
return false;
}
// ****************************************************************************
bool DrawText::doesLineIntersect(const std::string &label, OrientType orient,
const Point2D &cds, const Point2D &end1,
const Point2D &end2, double padding) const {
std::vector<std::shared_ptr<StringRect>> rects;
std::vector<TextDrawType> draw_modes;
std::vector<char> draw_chars;
getStringRects(label, orient, rects, draw_modes, draw_chars);
return doesLineIntersect(rects, cds, end1, end2, padding);
}
// ****************************************************************************
bool DrawText::doesLineIntersect(
const std::vector<std::shared_ptr<StringRect>> &rects, const Point2D &cds,
const Point2D &end1, const Point2D &end2, double padding) const {
for (auto r : rects) {
StringRect nr(*r);
nr.trans_ += cds;
Point2D tl, tr, bl, br;
nr.calcCorners(tl, tr, br, bl, padding);
if (doLinesIntersect(end2, end1, tl, tr, nullptr)) {
return true;
}
if (doLinesIntersect(end2, end1, tr, br, nullptr)) {
return true;
}
if (doLinesIntersect(end2, end1, br, bl, nullptr)) {
return true;
}
if (doLinesIntersect(end2, end1, bl, tl, nullptr)) {
return true;
}
}
return false;
}
// ****************************************************************************
bool DrawText::doesStringIntersect(const std::string &label1,
OrientType orient1, const Point2D &cds1,
const std::string &label2,
OrientType orient2,
const Point2D &cds2) const {
if (label1.empty() || label2.empty()) {
return false;
}
std::vector<std::shared_ptr<StringRect>> rects1;
std::vector<TextDrawType> draw_modes1;
std::vector<char> draw_chars1;
getStringRects(label1, orient1, rects1, draw_modes1, draw_chars1);
return doesStringIntersect(rects1, cds1, label2, orient2, cds2);
}
// ****************************************************************************
bool DrawText::doesStringIntersect(
const std::vector<std::shared_ptr<StringRect>> &rects, const Point2D &cds1,
const std::string &label2, OrientType orient2, const Point2D &cds2) const {
if (label2.empty()) {
return false;
}
std::vector<std::shared_ptr<StringRect>> rects2;
std::vector<TextDrawType> draw_modes2;
std::vector<char> draw_chars2;
getStringRects(label2, orient2, rects2, draw_modes2, draw_chars2);
for (auto r1 : rects) {
StringRect nr1(*r1);
nr1.trans_ += cds1;
for (auto r2 : rects2) {
StringRect nr2(*r2);
nr2.trans_ += cds2;
if (nr1.doesItIntersect(nr2)) {
return true;
}
}
}
return false;
}
// ****************************************************************************
void DrawText::getStringExtremes(const std::string &label, OrientType orient,
double &x_min, double &y_min, double &x_max,
double &y_max, bool dontSplit) const {
if (label.empty()) {
x_min = x_max = 0.0;
y_min = y_max = 0.0;
return;
}
x_min = y_min = std::numeric_limits<double>::max();
x_max = y_max = -std::numeric_limits<double>::max();
std::vector<std::shared_ptr<StringRect>> rects;
std::vector<TextDrawType> draw_modes;
std::vector<char> to_draw;
getStringRects(label, orient, rects, draw_modes, to_draw, dontSplit);
int i = 0;
for (auto r : rects) {
Point2D tl, tr, br, bl;
r->calcCorners(tl, tr, br, bl, 0.0);
// sometimes the rect is in a coordinate frame where +ve y is down,
// sometimes it's up. For these purposes, we don't care so long as
// the y_max is larger than the y_min. We probably don't need to do
// all the tests for x_min and x_max;
x_min = std::min(bl.x, x_min);
x_min = std::min(tr.x, x_min);
y_min = std::min(bl.y, y_min);
y_min = std::min(tr.y, y_min);
x_max = std::max(bl.x, x_max);
x_max = std::max(tr.x, x_max);
y_max = std::max(bl.y, y_max);
y_max = std::max(tr.y, y_max);
++i;
}
}
// ****************************************************************************
void DrawText::alignString(
TextAlignType talign, const std::vector<TextDrawType> &draw_modes,
std::vector<std::shared_ptr<StringRect>> &rects) const {
// std::string comes in with rects aligned with first char with its
// left hand and bottom edges at 0 on y and x respectively.
// Adjust relative to that so that the relative alignment point is at
// (0,0).
if (talign == TextAlignType::MIDDLE) {
size_t num_norm = count(draw_modes.begin(), draw_modes.end(),
TextDrawType::TextDrawNormal);
if (num_norm == 1) {
talign = TextAlignType::START;
}
}
Point2D align_trans, align_offset;
if (talign == TextAlignType::START || talign == TextAlignType::END) {
size_t align_char = 0;
for (size_t i = 0; i < rects.size(); ++i) {
if (draw_modes[i] == TextDrawType::TextDrawNormal) {
align_char = i;
if (talign == TextAlignType::START) {
break;
}
}
}
align_trans = rects[align_char]->trans_;
align_offset = rects[align_char]->offset_;
} else {
// centre on the middle of the Normal text. The super- or subscripts
// should be at the ends.
double x_min = std::numeric_limits<double>::max();
double x_max = -x_min;
double y_min = std::numeric_limits<double>::max();
double y_max = -y_min;
align_offset.x = align_offset.y = 0.0;
int num_norm = 0;
for (size_t i = 0; i < rects.size(); ++i) {
if (draw_modes[i] == TextDrawType::TextDrawNormal) {
Point2D tl, tr, br, bl;
rects[i]->calcCorners(tl, tr, br, bl, 0.0);
// sometimes the rect is in a coordinate frame where +ve y is down,
// sometimes it's up. For these purposes, we don't care so long as
// the y_max is larger than the y_min. We probably don't need to do
// all the tests for x_min and x_max;
x_min = std::min(bl.x, x_min);
x_min = std::min(tr.x, x_min);
y_min = std::min(bl.y, y_min);
y_min = std::min(tr.y, y_min);
x_max = std::max(bl.x, x_max);
x_max = std::max(tr.x, x_max);
y_max = std::max(bl.y, y_max);
y_max = std::max(tr.y, y_max);
align_offset += rects[i]->offset_;
++num_norm;
}
}
align_trans.x = (x_max - x_min) / 2.0;
align_trans.y = 0.0;
align_offset /= num_norm;
}
for (auto r : rects) {
r->trans_ -= align_trans;
r->offset_ = align_offset;
}
}
// ****************************************************************************
void DrawText::adjustStringRectsForSuperSubScript(
const std::vector<TextDrawType> &draw_modes,
std::vector<std::shared_ptr<StringRect>> &rects) const {
double last_char = -1;
for (size_t i = 0; i < draw_modes.size(); ++i) {
switch (draw_modes[i]) {
case TextDrawType::TextDrawSuperscript:
// superscripts may come before or after a letter. If before,
// spin through to first non-superscript for last_height
if (last_char < 0) {
for (size_t j = i + 1; j < draw_modes.size(); ++j) {
if (draw_modes[j] == TextDrawType::TextDrawNormal) {
last_char = j;
break;
}
}
}
// adjust up by last height / 2.
rects[i]->rect_corr_ = rects[last_char]->height_;
rects[i]->trans_.y -= rects[i]->rect_corr_ / 2.0;
// if the last char was a subscript, remove the advance
if (i && draw_modes[i - 1] == TextDrawType::TextDrawSubscript) {
double move_by = rects[i]->trans_.x - rects[i - 1]->trans_.x;
if (move_by > 0.0) {
for (size_t j = 0; j < i; ++j) {
rects[j]->trans_.x += move_by;
}
} else {
for (size_t j = i; j < draw_modes.size(); ++j) {
rects[j]->trans_.x += move_by;
}
}
rects[i]->trans_.x = rects[i - 1]->trans_.x;
}
break;
case TextDrawType::TextDrawSubscript:
// adjust y down by last height / 2
rects[i]->rect_corr_ = -rects[last_char]->height_;
rects[i]->trans_.y -= rects[i]->rect_corr_ / 2.0;
// if the last char was a superscript, remove the advance
if (i && draw_modes[i - 1] == TextDrawType::TextDrawSuperscript) {
double move_by = rects[i]->trans_.x - rects[i - 1]->trans_.x;
if (move_by > 0.0) {
for (size_t j = 0; j < i; ++j) {
rects[j]->trans_.x += move_by;
}
} else {
for (size_t j = i; j < draw_modes.size(); ++j) {
rects[j]->trans_.x += move_by;
}
}
rects[i]->trans_.x = rects[i - 1]->trans_.x;
}
break;
case TextDrawType::TextDrawNormal:
last_char = i;
break;
}
}
}
// ****************************************************************************
double DrawText::selectScaleFactor(char c, TextDrawType draw_type) const {
switch (draw_type) {
case TextDrawType::TextDrawNormal:
return 1.0;
case TextDrawType::TextDrawSubscript:
return SUBS_SCALE;
case TextDrawType::TextDrawSuperscript:
if (c == '-' || c == '+') {
return SUBS_SCALE;
} else {
return SUPER_SCALE;
}
}
return 1.0;
}
// ****************************************************************************
void DrawText::getStringSize(const std::string &label, double &label_width,
double &label_height) const {
double x_min, y_min, x_max, y_max;
getStringExtremes(label, OrientType::E, x_min, y_min, x_max, y_max);
label_width = x_max - x_min;
label_height = y_max - y_min;
}
// ****************************************************************************
bool setStringDrawMode(const std::string &instring, TextDrawType &draw_mode,
size_t &i) {
std::string bit1 = instring.substr(i, 5);
std::string bit2 = instring.substr(i, 6);
// could be markup for super- or sub-script
if (std::string("<sub>") == bit1) {
draw_mode = TextDrawType::TextDrawSubscript;
i += 4;
return true;
} else if (std::string("<sup>") == bit1) {
draw_mode = TextDrawType::TextDrawSuperscript;
i += 4;
return true;
} else if (std::string("</sub>") == bit2) {
draw_mode = TextDrawType::TextDrawNormal;
i += 5;
return true;
} else if (std::string("</sup>") == bit2) {
draw_mode = TextDrawType::TextDrawNormal;
i += 5;
return true;
}
return false;
}
// ****************************************************************************
void DrawText::getStringRects(const std::string &text, OrientType orient,
std::vector<std::shared_ptr<StringRect>> &rects,
std::vector<TextDrawType> &draw_modes,
std::vector<char> &draw_chars,
bool dontSplit) const {
std::vector<std::string> text_bits;
if (!dontSplit) {
text_bits = atomLabelToPieces(text, orient);
} else {
text_bits.push_back(text);
}
if (orient == OrientType::W) {
// stick the pieces together again backwards and draw as one so there
// aren't ugly splits in the string.
std::string new_lab;
for (auto i = text_bits.rbegin(); i != text_bits.rend(); ++i) {
new_lab += *i;
}
getStringRects(new_lab, rects, draw_modes, draw_chars);
alignString(TextAlignType::END, draw_modes, rects);
} else if (orient == OrientType::E) {
// likewise, but forwards
std::string new_lab;
for (auto lab : text_bits) {
new_lab += lab;
}
getStringRects(new_lab, rects, draw_modes, draw_chars);
alignString(TextAlignType::START, draw_modes, rects);
} else {
double running_y = 0;
for (size_t i = 0; i < text_bits.size(); ++i) {
std::vector<std::shared_ptr<StringRect>> t_rects;
std::vector<TextDrawType> t_draw_modes;
std::vector<char> t_draw_chars;
getStringRects(text_bits[i], t_rects, t_draw_modes, t_draw_chars);
alignString(TextAlignType::MIDDLE, t_draw_modes, t_rects);
double max_height = -std::numeric_limits<double>::max();
for (auto r : t_rects) {
max_height = std::max(r->height_, max_height);
r->y_shift_ = running_y;
}
rects.insert(rects.end(), t_rects.begin(), t_rects.end());
draw_modes.insert(draw_modes.end(), t_draw_modes.begin(),
t_draw_modes.end());
draw_chars.insert(draw_chars.end(), t_draw_chars.begin(),
t_draw_chars.end());
if (orient == OrientType::N) {
running_y -= 1.1 * max_height;
} else if (orient == OrientType::S) {
running_y += 1.1 * max_height;
}
}
}
}
// ****************************************************************************
void DrawText::drawChars(const Point2D &a_cds,
const std::vector<std::shared_ptr<StringRect>> &rects,
const std::vector<TextDrawType> &draw_modes,
const std::vector<char> &draw_chars) {
double full_scale = fontScale();
for (size_t i = 0; i < rects.size(); ++i) {
Point2D draw_cds;
draw_cds.x = a_cds.x + rects[i]->trans_.x - rects[i]->offset_.x;
draw_cds.y = a_cds.y - rects[i]->trans_.y +
rects[i]->offset_.y; // opposite sign convention
draw_cds.y -= rects[i]->rect_corr_ + rects[i]->y_shift_;
double mfs = minFontSize();
setMinFontSize(-1);
setFontScale(full_scale * selectScaleFactor(draw_chars[i], draw_modes[i]));
drawChar(draw_chars[i], draw_cds);
setMinFontSize(mfs);
setFontScale(full_scale);
}
}
// ****************************************************************************
std::vector<std::string> atomLabelToPieces(const std::string &label,
OrientType orient) {
// cout << "splitting " << label << " : " << orient << endl;
std::vector<std::string> label_pieces;
if (label.empty()) {
return label_pieces;
}
// if we have the mark-up <lit>XX</lit> the symbol is to be used
// without modification
if (label.substr(0, 5) == "<lit>") {
std::string lit_sym = label.substr(5);
size_t idx = lit_sym.find("</lit>");
if (idx != std::string::npos) {
lit_sym = lit_sym.substr(0, idx);
}
label_pieces.emplace_back(lit_sym);
return label_pieces;
}
std::string next_piece;
size_t i = 0;
while (true) {
if (i == label.length()) {
if (!next_piece.empty()) {
label_pieces.emplace_back(next_piece);
break;
}
}
if (label.substr(i, 2) == "<s" || label[i] == ':' || isupper(label[i])) {
// save the old piece, start a new one
if (!next_piece.empty()) {
label_pieces.emplace_back(next_piece);
next_piece.clear();
}
}
next_piece += label[i++];
}
if (label_pieces.size() < 2) {
return label_pieces;
}
// if the orientation is S or E, any charge flag needs to be at the end.
if (orient == OrientType::E || orient == OrientType::S) {
for (size_t i = 0; i < label_pieces.size(); ++i) {
if (label_pieces[i] == "<sup>+</sup>" ||
label_pieces[i] == "<sup>-</sup>") {
label_pieces.push_back(label_pieces[i]);
label_pieces[i].clear();
break;
}
}
}
// Now group some together. This relies on the order that
// getAtomLabel built them in the first place. Each atom symbol
// needs to be flanked by any <sub> and <super> pieces.
std::vector<std::string> final_pieces;
std::string curr_piece;
bool had_symbol = false;
for (const auto &p : label_pieces) {
if (p.empty()) {
continue;
}
if (!isupper(p[0])) {
curr_piece += p;
} else {
if (had_symbol) {
final_pieces.push_back(curr_piece);
curr_piece = p;
had_symbol = true;
} else {
curr_piece += p;
had_symbol = true;
}
}
}
if (!curr_piece.empty()) {
final_pieces.push_back(curr_piece);
}
// cout << "Final pieces : " << endl;
// for(auto l: final_pieces) {
// cout << l << endl;
// }
// cout << endl;
return final_pieces;
}
std::ostream &operator<<(std::ostream &oss, const TextAlignType &tat) {
switch (tat) {
case TextAlignType::START:
oss << "START";
break;
case TextAlignType::MIDDLE:
oss << "MIDDLE";
break;
case TextAlignType::END:
oss << "END";
break;
}
return oss;
}
std::ostream &operator<<(std::ostream &oss, const TextDrawType &tdt) {
switch (tdt) {
case TextDrawType::TextDrawNormal:
oss << "TextDrawNormal";
break;
case TextDrawType::TextDrawSuperscript:
oss << "TextDrawSuperscript";
break;
case TextDrawType::TextDrawSubscript:
oss << "TextDrawSubscript";
break;
}
return oss;
}
std::ostream &operator<<(std::ostream &oss, const OrientType &o) {
switch (o) {
case OrientType::C:
oss << "C";
break;
case OrientType::N:
oss << "N";
break;
case OrientType::S:
oss << "S";
break;
case OrientType::E:
oss << "E";
break;
case OrientType::W:
oss << "W";
break;
}
return oss;
}
} // namespace RDKit