// // Copyright (C) 2020-2022 David Cosgrove and other RDKit contributors // // @@ 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). // #include #include namespace RDKit { namespace MolDraw2D_detail { // **************************************************************************** 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) {} // **************************************************************************** DrawText::~DrawText() {} // **************************************************************************** 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_; } // **************************************************************************** bool DrawText::setFontScale(double new_scale, bool ignoreLimits) { font_scale_ = new_scale; if (ignoreLimits) { return true; } font_scale_ = new_scale; double nfs = fontSize(); if (max_font_size_ > 0.0 && nfs * (baseFontSize() / DEFAULT_FONT_SCALE) > max_font_size_) { font_scale_ = max_font_size_ / baseFontSize(); return false; } if (min_font_size_ > 0.0 && nfs * (baseFontSize() / DEFAULT_FONT_SCALE) < min_font_size_) { font_scale_ = min_font_size_ / baseFontSize(); return false; } return true; } // **************************************************************************** void DrawText::drawString(const std::string &str, const Point2D &cds, TextAlignType talign) { std::vector> rects; std::vector draw_modes; std::vector 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> rects; std::vector draw_modes; std::vector 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> rects; std::vector draw_modes; std::vector 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 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 (MolDraw2D_detail::doLinesIntersect(end2, end1, tl, tr, ip.get())) { end2 = *ip; } if (MolDraw2D_detail::doLinesIntersect(end2, end1, tr, br, ip.get())) { end2 = *ip; } if (MolDraw2D_detail::doLinesIntersect(end2, end1, br, bl, ip.get())) { end2 = *ip; } if (MolDraw2D_detail::doLinesIntersect(end2, end1, bl, tl, ip.get())) { end2 = *ip; } } } // **************************************************************************** void DrawText::drawStringRects(const std::string &label, OrientType orient, TextAlignType talign, const Point2D &cds, MolDraw2D &mol_draw) const { std::vector> rects; std::vector draw_modes; std::vector draw_chars; getStringRects(label, orient, rects, draw_modes, draw_chars, false, talign); 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); } } // **************************************************************************** bool DrawText::doesRectIntersect(const std::string &label, OrientType orient, const Point2D &cds, const StringRect &rect) const { if (label.empty()) { return false; } std::vector> rects; std::vector draw_modes; std::vector draw_chars; getStringRects(label, orient, rects, draw_modes, draw_chars); return doesRectIntersect(rects, cds, rect); } // **************************************************************************** bool DrawText::doesRectIntersect( const std::vector> &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> rects; std::vector draw_modes; std::vector draw_chars; getStringRects(label, orient, rects, draw_modes, draw_chars); return doesLineIntersect(rects, cds, end1, end2, padding); } // **************************************************************************** bool DrawText::doesLineIntersect( const std::vector> &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 (MolDraw2D_detail::doLinesIntersect(end2, end1, tl, tr, nullptr)) { return true; } if (MolDraw2D_detail::doLinesIntersect(end2, end1, tr, br, nullptr)) { return true; } if (MolDraw2D_detail::doLinesIntersect(end2, end1, br, bl, nullptr)) { return true; } if (MolDraw2D_detail::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> rects1; std::vector draw_modes1; std::vector draw_chars1; getStringRects(label1, orient1, rects1, draw_modes1, draw_chars1); return doesStringIntersect(rects1, cds1, label2, orient2, cds2); } // **************************************************************************** bool DrawText::doesStringIntersect( const std::vector> &rects, const Point2D &cds1, const std::string &label2, OrientType orient2, const Point2D &cds2) const { if (label2.empty()) { return false; } std::vector> rects2; std::vector draw_modes2; std::vector 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::max(); x_max = y_max = std::numeric_limits::lowest(); std::vector> rects; std::vector draw_modes; std::vector to_draw; getStringRects(label, orient, rects, draw_modes, to_draw, dontSplit); 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); } } // **************************************************************************** void DrawText::alignString( TextAlignType talign, const std::vector &draw_modes, std::vector> &rects) const { // std::string comes in with rects aligned with first char centred on (0,0). // 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; 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.x = rects[0]->trans_.x - rects[align_char]->trans_.x; } else { // centre on the middle of the Normal text. The super- or subscripts // should be at the ends. double x_min = std::numeric_limits::max(); double x_max = std::numeric_limits::lowest(); double y_min = std::numeric_limits::max(); double y_max = std::numeric_limits::lowest(); int align_char = -1; for (size_t i = 0; i < rects.size(); ++i) { if (draw_modes[i] == TextDrawType::TextDrawNormal) { align_char = align_char == -1 ? i : align_char; 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_char = align_char == -1 ? 0 : align_char; align_trans.x = -(x_max - x_min - rects[align_char]->width_) / 2.0; align_trans.y = 0.0; } for (auto r : rects) { r->trans_ += align_trans; } } // **************************************************************************** void DrawText::adjustStringRectsForSuperSubScript( const std::vector &draw_modes, std::vector> &rects) const { int 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; 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; break; case TextDrawType::TextDrawNormal: last_char = i; break; } } // contiguous sub- and super-scripts go at the same x. for (auto i = 1u; i < rects.size(); ++i) { if ((draw_modes[i] == TextDrawType::TextDrawSubscript && draw_modes[i - 1] == TextDrawType::TextDrawSuperscript) || (draw_modes[i - 1] == TextDrawType::TextDrawSubscript && draw_modes[i] == TextDrawType::TextDrawSuperscript)) { double moveBack = rects[i]->trans_.x - rects[i - 1]->trans_.x; for (auto j = i; j < rects.size(); ++j) { rects[j]->trans_.x -= moveBack; } } } } // **************************************************************************** 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("") == bit1) { draw_mode = TextDrawType::TextDrawSubscript; i += 4; return true; } else if (std::string("") == bit1) { draw_mode = TextDrawType::TextDrawSuperscript; i += 4; return true; } else if (std::string("") == bit2) { draw_mode = TextDrawType::TextDrawNormal; i += 5; return true; } else if (std::string("") == bit2) { draw_mode = TextDrawType::TextDrawNormal; i += 5; return true; } return false; } // **************************************************************************** void DrawText::getStringRects(const std::string &text, OrientType orient, std::vector> &rects, std::vector &draw_modes, std::vector &draw_chars, bool dontSplit, TextAlignType textAlign) const { PRECONDITION(!text.empty(), "empty string"); std::vector text_bits; if (!dontSplit) { text_bits = atomLabelToPieces(text, orient); } else { text_bits.push_back(text); } TextAlignType ta = orient == OrientType::C ? textAlign : TextAlignType::MIDDLE; 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 (const 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 (const auto &tb : text_bits) { std::vector> t_rects; std::vector t_draw_modes; std::vector t_draw_chars; getStringRects(tb, t_rects, t_draw_modes, t_draw_chars); alignString(ta, t_draw_modes, t_rects); double max_height = std::numeric_limits::lowest(); 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> &rects, const std::vector &draw_modes, const std::vector &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_; setFontScale(full_scale * selectScaleFactor(draw_chars[i], draw_modes[i]), true); drawChar(draw_chars[i], draw_cds); setFontScale(full_scale, true); } } // **************************************************************************** std::vector atomLabelToPieces(const std::string &label, OrientType orient) { // std::cout << "splitting " << label << " : " << orient << std::endl; std::vector label_pieces; if (label.empty()) { return label_pieces; } // if we have the mark-up XX the symbol is to be used // without modification if (label.substr(0, 5) == "") { std::string lit_sym = label.substr(5); size_t idx = lit_sym.find(""); 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) == "+" || label_pieces[i] == "-") { 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 and pieces. std::vector 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 MolDraw2D_detail } // namespace RDKit