From 893affdff0c843d0e0cd3f445e50cbf5fa09b15e Mon Sep 17 00:00:00 2001 From: Paolo Tosco Date: Tue, 18 Oct 2022 05:00:35 +0200 Subject: [PATCH] Expose highlighAtomColors, highlighBondColors and highlightAtomRadii to CFFI and JS (#5657) * expose highlighAtomColors, highlighBondColors and highlightAtomRadii to CFFI and JS * removed comment Co-authored-by: Tosco, Paolo --- Code/MinimalLib/cffi_test.c | 29 +++++ Code/MinimalLib/common.h | 188 +++++++++++++++++++++++---------- Code/MinimalLib/jswrapper.cpp | 27 +++-- Code/MinimalLib/tests/tests.js | 32 ++++++ 4 files changed, 213 insertions(+), 63 deletions(-) diff --git a/Code/MinimalLib/cffi_test.c b/Code/MinimalLib/cffi_test.c index ce6ab498b..75853a6f5 100644 --- a/Code/MinimalLib/cffi_test.c +++ b/Code/MinimalLib/cffi_test.c @@ -206,6 +206,35 @@ void test_svg() { assert(strstr(svg, "")); free(svg); + svg = get_svg( + pkl, pkl_size, + "{\"highlightAtomColors\":{" + "\"0\": [1.0, 0.0, 0.0]," + "\"1\": [1.0, 0.0, 0.0]," + "\"2\": [1.0, 0.0, 0.0]," + "\"3\": [0.0, 1.0, 0.0]," + "\"4\": [1.0, 0.0, 0.0]," + "\"5\": [1.0, 0.0, 0.0]," + "\"6\": [1.0, 0.0, 0.0]" + "}, \"highlightBondColors\":{" + "\"2\": [0.0, 0.7, 0.9]" + "}, \"highlightAtomRadii\":{" + "\"0\": 0.1," + "\"1\": 0.1," + "\"2\": 0.1," + "\"3\": 0.8," + "\"4\": 0.1," + "\"5\": 0.1," + "\"6\": 0.1" + "}, \"atoms\": [0, 1, 2, 3, 4, 5, 6], \"bonds\": [2], \"width\":127}"); + assert(!strstr(svg, "fill:#FF7F7F")); + assert(strstr(svg, "fill:#FF0000")); + assert(strstr(svg, "fill:#00FF00")); + assert(strstr(svg, "fill:#00B2E5")); + assert(strstr(svg, "width='127px'")); + assert(strstr(svg, "")); + free(svg); + free(pkl); pkl = NULL; printf(" done\n"); diff --git a/Code/MinimalLib/common.h b/Code/MinimalLib/common.h index 0571d4892..f8655f5dd 100644 --- a/Code/MinimalLib/common.h +++ b/Code/MinimalLib/common.h @@ -256,40 +256,87 @@ ChemicalReaction *rxn_from_input(const std::string &input, return rxn_from_input(input, json); } -std::string process_details(const std::string &details, int &width, int &height, - int &offsetx, int &offsety, std::string &legend, - std::vector &atomIds, +std::string parse_int_array(const rj::Document &doc, std::vector &intVec, + const std::string &keyName, + const std::string &valueName) { + const auto it = doc.FindMember(keyName.c_str()); + if (it != doc.MemberEnd()) { + if (!it->value.IsArray()) { + return "JSON contains '" + keyName + "' field, but it is not an array"; + } + for (const auto &val : it->value.GetArray()) { + if (!val.IsInt()) { + return valueName + " should be integers"; + } + intVec.push_back(val.GetInt()); + } + } + return ""; +} + +std::string parse_rgba_array(const rj::Value &val, DrawColour &color, + const std::string &keyName) { + if (!val.IsArray() || val.Size() < 3 || val.Size() > 4) { + return "JSON contains '" + keyName + + "' field, but the " + "colors are not R,G,B[,A] arrays"; + } + std::vector rgba(4, 1.0); + unsigned int i = 0; + for (const auto &component : val.GetArray()) { + if (!component.IsNumber()) { + return "JSON contains '" + keyName + + "' field, but the " + "R,G,B[,A] arrays contain non-float values"; + } + CHECK_INVARIANT(i < 4, ""); + rgba[i++] = component.GetDouble(); + } + color.r = rgba[0]; + color.g = rgba[1]; + color.b = rgba[2]; + color.a = rgba[3]; + return ""; +} + +std::string parse_highlight_colors(const rj::Document &doc, + std::map &colorMap, + const std::string &keyName) { + const auto it = doc.FindMember(keyName.c_str()); + if (it != doc.MemberEnd()) { + if (!it->value.IsObject()) { + return "JSON contains '" + keyName + "' field, but it is not an object"; + } + for (const auto &entry : it->value.GetObject()) { + DrawColour color; + auto problems = parse_rgba_array(entry.value, color, keyName); + if (!problems.empty()) { + return problems; + } + int idx = std::atoi(entry.name.GetString()); + colorMap[idx] = std::move(color); + } + } + return ""; +} + +std::string process_details(rj::Document &doc, const std::string &details, + int &width, int &height, int &offsetx, int &offsety, + std::string &legend, std::vector &atomIds, std::vector &bondIds, bool &kekulize) { - rj::Document doc; doc.Parse(details.c_str()); if (!doc.IsObject()) { return "Invalid JSON"; } - - const auto atomsIt = doc.FindMember("atoms"); - if (atomsIt != doc.MemberEnd()) { - if (!atomsIt->value.IsArray()) { - return "JSON contains 'atoms' field, but it is not an array"; - } - for (const auto &molval : atomsIt->value.GetArray()) { - if (!molval.IsInt()) { - return ("Atom IDs should be integers"); - } - atomIds.push_back(molval.GetInt()); - } + std::string problems; + problems = parse_int_array(doc, atomIds, "atoms", "Atom IDs"); + if (!problems.empty()) { + return problems; } - const auto bondsIt = doc.FindMember("bonds"); - if (bondsIt != doc.MemberEnd()) { - if (!bondsIt->value.IsArray()) { - return "JSON contains 'bonds' field, but it is not an array"; - } - for (const auto &molval : bondsIt->value.GetArray()) { - if (!molval.IsInt()) { - return ("Bond IDs should be integers"); - } - bondIds.push_back(molval.GetInt()); - } + problems = parse_int_array(doc, bondIds, "bonds", "Bond IDs"); + if (!problems.empty()) { + return problems; } const auto widthIt = doc.FindMember("width"); @@ -345,21 +392,59 @@ std::string process_details(const std::string &details, int &width, int &height, return ""; } +std::string process_mol_details(const std::string &details, int &width, + int &height, int &offsetx, int &offsety, + std::string &legend, std::vector &atomIds, + std::vector &bondIds, + std::map &atomMap, + std::map &bondMap, + std::map &radiiMap, + bool &kekulize) { + rj::Document doc; + auto problems = process_details(doc, details, width, height, offsetx, offsety, + legend, atomIds, bondIds, kekulize); + if (!problems.empty()) { + return problems; + } + + problems = parse_highlight_colors(doc, atomMap, "highlightAtomColors"); + if (!problems.empty()) { + return problems; + } + + problems = parse_highlight_colors(doc, bondMap, "highlightBondColors"); + if (!problems.empty()) { + return problems; + } + + const auto radiiMapit = doc.FindMember("highlightAtomRadii"); + if (radiiMapit != doc.MemberEnd()) { + if (!radiiMapit->value.IsObject()) { + return "JSON contains 'highlightAtomRadii' field, but it is not an object"; + } + for (const auto &entry : radiiMapit->value.GetObject()) { + if (!entry.value.IsNumber()) { + return "JSON contains 'highlightAtomRadii' field, but the radii" + "are not floats"; + } + int idx = std::atoi(entry.name.GetString()); + radiiMap[idx] = entry.value.GetDouble(); + } + } + return ""; +} + std::string process_rxn_details( const std::string &details, int &width, int &height, int &offsetx, int &offsety, std::string &legend, std::vector &atomIds, std::vector &bondIds, bool &kekulize, bool &highlightByReactant, std::vector &highlightColorsReactants) { - auto problems = process_details(details, width, height, offsetx, offsety, + rj::Document doc; + auto problems = process_details(doc, details, width, height, offsetx, offsety, legend, atomIds, bondIds, kekulize); if (!problems.empty()) { return problems; } - rj::Document doc; - doc.Parse(details.c_str()); - if (!doc.IsObject()) { - return "Invalid JSON"; - } auto highlightByReactantIt = doc.FindMember("highlightByReactant"); if (highlightByReactantIt != doc.MemberEnd()) { if (!highlightByReactantIt->value.IsBool()) { @@ -375,22 +460,13 @@ std::string process_rxn_details( return "JSON contains 'highlightColorsReactants' field, but it is not an " "array"; } - for (const auto &color : highlightColorsReactantsIt->value.GetArray()) { - if (!color.IsArray() || color.Size() < 3 || color.Size() > 4) { - return "JSON contains 'highlightColorsReactants' array, but the " - "elements are not R,G,B[,A] arrays"; + for (const auto &rgbaArray : highlightColorsReactantsIt->value.GetArray()) { + DrawColour color; + problems = parse_rgba_array(rgbaArray, color, "highlightColorsReactants"); + if (!problems.empty()) { + return problems; } - std::vector rgba(4, 1.0); - unsigned int i = 0; - for (const auto &component : color.GetArray()) { - if (!component.IsDouble()) { - return "JSON contains 'highlightColorsReactants' array, but the " - "R,G,B[,A] arrays contain non-float values"; - } - CHECK_INVARIANT(i < 4, ""); - rgba[i++] = component.GetDouble(); - } - highlightColorsReactants.emplace_back(rgba[0], rgba[1], rgba[2], rgba[3]); + highlightColorsReactants.push_back(std::move(color)); } } return ""; @@ -426,18 +502,22 @@ std::string mol_to_svg(const ROMol &m, int w, int h, const std::string &details = "") { std::vector atomIds; std::vector bondIds; + std::map atomMap; + std::map bondMap; + std::map radiiMap; std::string legend = ""; + std::string problems; int offsetx = 0; int offsety = 0; bool kekulize = true; if (!details.empty()) { - auto problems = process_details(details, w, h, offsetx, offsety, legend, - atomIds, bondIds, kekulize); + problems = + process_mol_details(details, w, h, offsetx, offsety, legend, atomIds, + bondIds, atomMap, bondMap, radiiMap, kekulize); if (!problems.empty()) { return problems; } } - MolDraw2DSVG drawer(w, h); if (!details.empty()) { MolDraw2DUtils::updateDrawerParamsFromJSON(drawer, details); @@ -445,8 +525,10 @@ std::string mol_to_svg(const ROMol &m, int w, int h, drawer.setOffset(offsetx, offsety); MolDraw2DUtils::prepareAndDrawMolecule(drawer, m, legend, &atomIds, &bondIds, - nullptr, nullptr, nullptr, -1, - kekulize); + atomMap.empty() ? nullptr : &atomMap, + bondMap.empty() ? nullptr : &bondMap, + radiiMap.empty() ? nullptr : &radiiMap, + -1, kekulize); drawer.finishDrawing(); return drawer.getDrawingText(); diff --git a/Code/MinimalLib/jswrapper.cpp b/Code/MinimalLib/jswrapper.cpp index 567d5c61d..8053a1075 100644 --- a/Code/MinimalLib/jswrapper.cpp +++ b/Code/MinimalLib/jswrapper.cpp @@ -20,11 +20,12 @@ using namespace RDKit; namespace RDKit { namespace MinimalLib { -extern std::string process_details(const std::string &details, int &width, - int &height, int &offsetx, int &offsety, - std::string &legend, - std::vector &atomIds, - std::vector &bondIds, bool &kekulize); +extern std::string process_mol_details( + const std::string &details, int &width, int &height, int &offsetx, + int &offsety, std::string &legend, std::vector &atomIds, + std::vector &bondIds, std::map &atomMap, + std::map &bondMap, std::map &radiiMap, + bool &kekulize); extern std::string process_rxn_details( const std::string &details, int &width, int &height, int &offsetx, int &offsety, std::string &legend, std::vector &atomIds, @@ -69,13 +70,17 @@ std::string draw_to_canvas_with_highlights(JSMol &self, emscripten::val canvas, int h = canvas["height"].as(); std::vector atomIds; std::vector bondIds; + std::map atomMap; + std::map bondMap; + std::map radiiMap; std::string legend = ""; int offsetx = 0; int offsety = 0; bool kekulize = true; if (!details.empty()) { - auto problems = MinimalLib::process_details( - details, w, h, offsetx, offsety, legend, atomIds, bondIds, kekulize); + auto problems = MinimalLib::process_mol_details( + details, w, h, offsetx, offsety, legend, atomIds, bondIds, atomMap, + bondMap, radiiMap, kekulize); if (!problems.empty()) { return problems; } @@ -87,9 +92,11 @@ std::string draw_to_canvas_with_highlights(JSMol &self, emscripten::val canvas, } d2d->setOffset(offsetx, offsety); - MolDraw2DUtils::prepareAndDrawMolecule(*d2d, *self.d_mol, legend, &atomIds, - &bondIds, nullptr, nullptr, nullptr, - -1, kekulize); + MolDraw2DUtils::prepareAndDrawMolecule( + *d2d, *self.d_mol, legend, &atomIds, &bondIds, + atomMap.empty() ? nullptr : &atomMap, + bondMap.empty() ? nullptr : &bondMap, + radiiMap.empty() ? nullptr : &radiiMap, -1, kekulize); return ""; } diff --git a/Code/MinimalLib/tests/tests.js b/Code/MinimalLib/tests/tests.js index 64a71ec64..acfd360e1 100644 --- a/Code/MinimalLib/tests/tests.js +++ b/Code/MinimalLib/tests/tests.js @@ -992,6 +992,37 @@ M END assert.equal(props.get(1), "test2"); } +function test_highlights() { + var mol = RDKitModule.get_mol('c1cc(O)ccc1'); + var svg = mol.get_svg_with_highlights(JSON.stringify({ + highlightAtomColors: { + 0: [1.0, 0.0, 0.0], + 1: [1.0, 0.0, 0.0], + 2: [1.0, 0.0, 0.0], + 3: [0.0, 1.0, 0.0], + 4: [1.0, 0.0, 0.0], + 5: [1.0, 0.0, 0.0], + 6: [1.0, 0.0, 0.0], + }, highlightBondColors: { + 2: [0.0, 0.7, 0.9], + }, highlightAtomRadii: { + 0: 0.1, + 1: 0.1, + 2: 0.1, + 3: 0.8, + 4: 0.1, + 5: 0.1, + 6: 0.1, + }, atoms: [0, 1, 2, 3, 4, 5, 6], bonds: [2], width: 127 + })); + assert(!svg.includes('fill:#FF7F7F')); + assert(svg.includes('fill:#FF0000')); + assert(svg.includes('fill:#00FF00')); + assert(svg.includes('fill:#00B2E5')); + assert(svg.includes("width='127px'")); + assert(svg.includes('')); +} + initRDKitModule().then(function(instance) { var done = {}; const waitAllTestsFinished = () => { @@ -1035,6 +1066,7 @@ initRDKitModule().then(function(instance) { test_rxn_drawing(); test_legacy_stereochem(); test_prop(); + test_highlights(); waitAllTestsFinished().then(() => console.log("Tests finished successfully") );