1 /* Copyright (C) 2015 The Chemistry Development Kit (CDK) project 2 * 3 * Contact: cdk-devel@lists.sourceforge.net 4 * 5 * This program is free software; you can redistribute it and/or 6 * modify it under the terms of the GNU Lesser General Public License 7 * as published by the Free Software Foundation; either version 2.1 8 * of the License, or (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU Lesser General Public License for more details. 14 * 15 * You should have received a copy of the GNU Lesser General Public License 16 * along with this program; if not, write to the Free Software 17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 */ 19 package org.openscience.cdk.depict; 20 21 import com.google.common.collect.FluentIterable; 22 import org.openscience.cdk.CDKConstants; 23 import org.openscience.cdk.exception.CDKException; 24 import org.openscience.cdk.geometry.GeometryUtil; 25 import org.openscience.cdk.interfaces.IAtom; 26 import org.openscience.cdk.interfaces.IAtomContainer; 27 import org.openscience.cdk.interfaces.IAtomContainerSet; 28 import org.openscience.cdk.interfaces.IBond; 29 import org.openscience.cdk.interfaces.IChemObject; 30 import org.openscience.cdk.interfaces.IReaction; 31 import org.openscience.cdk.layout.StructureDiagramGenerator; 32 import org.openscience.cdk.renderer.RendererModel; 33 import org.openscience.cdk.renderer.SymbolVisibility; 34 import org.openscience.cdk.renderer.color.CDK2DAtomColors; 35 import org.openscience.cdk.renderer.color.IAtomColorer; 36 import org.openscience.cdk.renderer.elements.Bounds; 37 import org.openscience.cdk.renderer.elements.ElementGroup; 38 import org.openscience.cdk.renderer.elements.IRenderingElement; 39 import org.openscience.cdk.renderer.elements.MarkedElement; 40 import org.openscience.cdk.renderer.generators.BasicSceneGenerator; 41 import org.openscience.cdk.renderer.generators.BasicSceneGenerator.BackgroundColor; 42 import org.openscience.cdk.renderer.generators.IGenerator; 43 import org.openscience.cdk.renderer.generators.IGeneratorParameter; 44 import org.openscience.cdk.renderer.generators.standard.SelectionVisibility; 45 import org.openscience.cdk.renderer.generators.standard.StandardGenerator; 46 import org.openscience.cdk.renderer.generators.standard.StandardGenerator.DelocalisedDonutsBondDisplay; 47 import org.openscience.cdk.renderer.generators.standard.StandardGenerator.ForceDelocalisedBondDisplay; 48 import org.openscience.cdk.tools.LoggingToolFactory; 49 50 import javax.vecmath.Point2d; 51 import java.awt.Color; 52 import java.awt.Dimension; 53 import java.awt.Font; 54 import java.util.ArrayList; 55 import java.util.Arrays; 56 import java.util.Collection; 57 import java.util.Collections; 58 import java.util.HashMap; 59 import java.util.List; 60 import java.util.Map; 61 62 /** 63 * A high-level API for depicting molecules and reactions. 64 * 65 * <br> 66 * <b>General Usage</b> 67 * Create a generator and reuse it for multiple depictions. Configure how 68 * the depiction will look using {@code with...()} methods. 69 * <pre>{@code 70 * DepictionGenerator dg = new DepictionGenerator().withSize(512, 512) 71 * .withAtomColors(); 72 * for (IAtomContainer mol : mols) { 73 * dg.depict(mol).writeTo("~/mol.png"); 74 * } 75 * }</pre> 76 * 77 * <br> 78 * <b>One Line Quick Use</b> 79 * For simplified use we can create a generator and use it once for a single depiction. 80 * <pre>{@code 81 * new DepictionGenerator().depict(mol) 82 * .writeTo("~/mol.png"); 83 * }</pre> 84 * The intermediate {@link Depiction} object can write to many different formats 85 * through a variety of API calls. 86 * <pre>{@code 87 * Depiction depiction = new DepictionGenerator().depict(mol); 88 * 89 * // quick use, format determined by name by path 90 * depiction.writeTo("~/mol.png"); 91 * depiction.writeTo("~/mol.svg"); 92 * depiction.writeTo("~/mol.pdf"); 93 * depiction.writeTo("~/mol.jpg"); 94 * 95 * // manually specify the format 96 * depiction.writeTo(Depiction.SVG_FMT, "~/mol"); 97 * 98 * // convert to a Java buffered image 99 * BufferedImage img = depiction.toImg(); 100 * 101 * // get the SVG XML string 102 * String svg = depiction.toSvgStr(); 103 * }</pre> 104 * 105 * @author John may 106 */ 107 @SuppressWarnings("PMD.ShortVariable") 108 public final class DepictionGenerator { 109 110 /** 111 * Visually distinct colors for highlighting. 112 * http://stackoverflow.com/a/4382138 113 * Kenneth L. Kelly and Deanne B. Judd. 114 * "Color: Universal Language and Dictionary of Names", 115 * National Bureau of Standards, 116 * Spec. Publ. 440, Dec. 1976, 189 pages. 117 */ 118 private static final Color[] KELLY_MAX_CONTRAST = new Color[]{ 119 new Color(0x00538A), // Strong Blue (sub-optimal for defective color vision) 120 new Color(0x93AA00), // Vivid Yellowish Green (sub-optimal for defective color vision) 121 new Color(0xC10020), // Vivid Red 122 new Color(0xFFB300), // Vivid Yellow 123 new Color(0x007D34), // Vivid Green (sub-optimal for defective color vision) 124 new Color(0xFF6800), // Vivid Orange 125 new Color(0xCEA262), // Grayish Yellow 126 new Color(0x817066), // Medium Gray 127 new Color(0xA6BDD7), // Very Light Blue 128 new Color(0x803E75), // Strong Purple 129 130 new Color(0xF6768E), // Strong Purplish Pink (sub-optimal for defective color vision) 131 132 new Color(0xFF7A5C), // Strong Yellowish Pink (sub-optimal for defective color vision) 133 new Color(0x53377A), // Strong Violet (sub-optimal for defective color vision) 134 new Color(0xFF8E00), // Vivid Orange Yellow (sub-optimal for defective color vision) 135 new Color(0xB32851), // Strong Purplish Red (sub-optimal for defective color vision) 136 new Color(0xF4C800), // Vivid Greenish Yellow (sub-optimal for defective color vision) 137 new Color(0x7F180D), // Strong Reddish Brown (sub-optimal for defective color vision) 138 139 new Color(0x593315), // Deep Yellowish Brown (sub-optimal for defective color vision) 140 new Color(0xF13A13), // Vivid Reddish Orange (sub-optimal for defective color vision) 141 new Color(0x232C16), // Dark Olive Green (sub-optimal for defective color vision) 142 }; 143 144 /** 145 * Magic value for indicating automatic parameters. These can 146 * be overridden by a caller. 147 */ 148 public static double AUTOMATIC = -1; 149 150 /** 151 * Default margin for vector graphics formats. 152 */ 153 public static double DEFAULT_MM_MARGIN = 0.56; 154 155 /** 156 * Default margin for raster graphics formats. 157 */ 158 public static double DEFAULT_PX_MARGIN = 4; 159 160 /** 161 * The dimensions (width x height) of the depiction. 162 */ 163 private Dimensions dimensions = Dimensions.AUTOMATIC; 164 165 /** 166 * Storage of rendering parameters. 167 */ 168 private final Map<Class<? extends IGeneratorParameter>, IGeneratorParameter<?>> params = new HashMap<>(); 169 170 /** 171 * Font used for depictions. 172 */ 173 private final Font font; 174 175 /** 176 * Diagram generators. 177 */ 178 private final List<IGenerator<IAtomContainer>> gens = new ArrayList<>(); 179 180 /** 181 * Flag to indicate atom numbers should be displayed. 182 */ 183 private boolean annotateAtomNum = false; 184 185 /** 186 * Flag to indicate atom values should be displayed. 187 */ 188 private boolean annotateAtomVal = false; 189 190 /** 191 * Flag to indicate atom maps should be displayed. 192 */ 193 private boolean annotateAtomMap = false; 194 195 /** 196 * Flag to indicate atom maps should be highlighted with colored. 197 */ 198 private boolean highlightAtomMap = false; 199 200 /** 201 * Colors to use in atom-map highlighting. 202 */ 203 private Color[] atomMapColors = null; 204 205 /** 206 * Reactions are aligned such that mapped atoms have the same coordinates on the left/right. 207 */ 208 private boolean alignMappedReactions = true; 209 210 /** 211 * Object that should be highlighted 212 */ 213 private Map<IChemObject, Color> highlight = new HashMap<>(); 214 215 /** 216 * Create a depiction generator using the standard sans-serif 217 * system font. 218 */ DepictionGenerator()219 public DepictionGenerator() { 220 this(new Font(getDefaultOsFont(), Font.PLAIN, 13)); 221 setParam(BasicSceneGenerator.BondLength.class, 26.1d); 222 setParam(StandardGenerator.HashSpacing.class, 26 / 8d); 223 setParam(StandardGenerator.WaveSpacing.class, 26 / 8d); 224 } 225 226 /** 227 * Create a depiction generator that will render atom 228 * labels using the specified AWT font. 229 * 230 * @param font the font to use to display 231 */ DepictionGenerator(Font font)232 public DepictionGenerator(Font font) { 233 gens.add(new BasicSceneGenerator()); 234 gens.add(new StandardGenerator(this.font = font)); 235 236 237 for (IGenerator<IAtomContainer> gen : gens) { 238 for (IGeneratorParameter<?> param : gen.getParameters()) { 239 params.put(param.getClass(), param); 240 } 241 } 242 for (IGeneratorParameter<?> param : new RendererModel().getRenderingParameters()) { 243 params.put(param.getClass(), param); 244 } 245 246 // default margin and separation is automatic 247 // since it depends on raster (px) vs vector (mm) 248 setParam(BasicSceneGenerator.Margin.class, AUTOMATIC); 249 setParam(RendererModel.Padding.class, AUTOMATIC); 250 } 251 252 /** 253 * Internal copy constructor. 254 * 255 * @param org original depiction 256 */ DepictionGenerator(DepictionGenerator org)257 private DepictionGenerator(DepictionGenerator org) { 258 this.annotateAtomMap = org.annotateAtomMap; 259 this.annotateAtomVal = org.annotateAtomVal; 260 this.annotateAtomNum = org.annotateAtomNum; 261 this.highlightAtomMap = org.highlightAtomMap; 262 this.atomMapColors = org.atomMapColors; 263 this.dimensions = org.dimensions; 264 this.font = org.font; 265 this.highlight.putAll(org.highlight); 266 this.gens.addAll(org.gens); 267 this.params.putAll(org.params); 268 this.alignMappedReactions = org.alignMappedReactions; 269 } 270 getParameterValue(Class<T> key)271 private <U, T extends IGeneratorParameter<U>> U getParameterValue(Class<T> key) { 272 @SuppressWarnings("unchecked") 273 final T param = (T) params.get(key); 274 if (param == null) 275 throw new IllegalArgumentException("No parameter registered: " + key + " " + params.keySet()); 276 return (U) param.getValue(); 277 } 278 setParam(Class<T> key, U val)279 private <T extends IGeneratorParameter<S>, S, U extends S> void setParam(Class<T> key, U val) { 280 T param = null; 281 try { 282 param = key.newInstance(); 283 param.setValue(val); 284 params.put(key, param); 285 } catch (InstantiationException | IllegalAccessException e) { 286 LoggingToolFactory.createLoggingTool(getClass()).error("Could not copy rendering parameter: " + key); 287 } 288 } 289 getModel()290 private RendererModel getModel() { 291 RendererModel model = new RendererModel(); 292 for (IGenerator<IAtomContainer> gen : gens) 293 model.registerParameters(gen); 294 for (IGeneratorParameter<?> param : params.values()) 295 model.set(param.getClass(), param.getValue()); 296 return model; 297 } 298 299 /** 300 * Depict a single molecule. 301 * 302 * @param mol molecule 303 * @return depiction instance 304 * @throws CDKException a depiction could not be generated 305 */ depict(final IAtomContainer mol)306 public Depiction depict(final IAtomContainer mol) throws CDKException { 307 return depict(Collections.singleton(mol), 1, 1); 308 } 309 310 /** 311 * Depict a set of molecules, they will be depicted in a grid. The grid 312 * size (nrow x ncol) is determined automatically based on the number 313 * molecules. 314 * 315 * @param mols molecules 316 * @return depiction 317 * @throws CDKException a depiction could not be generated 318 * @see #depict(Iterable, int, int) 319 */ depict(Iterable<IAtomContainer> mols)320 public Depiction depict(Iterable<IAtomContainer> mols) throws CDKException { 321 int nMols = FluentIterable.from(mols).size(); 322 Dimension grid = Dimensions.determineGrid(nMols); 323 return depict(mols, grid.height, grid.width); 324 } 325 326 /** 327 * Depict a set of molecules, they will be depicted in a grid with the 328 * specified number of rows and columns. Rows are filled first and then 329 * columns. 330 * 331 * @param mols molecules 332 * @param nrow number of rows 333 * @param ncol number of columns 334 * @return depiction 335 * @throws CDKException a depiction could not be generated 336 */ depict(Iterable<IAtomContainer> mols, int nrow, int ncol)337 public Depiction depict(Iterable<IAtomContainer> mols, int nrow, int ncol) throws CDKException { 338 339 List<LayoutBackup> layoutBackups = new ArrayList<>(); 340 int molId = 0; 341 for (IAtomContainer mol : mols) { 342 if (mol == null) 343 throw new NullPointerException("Null molecule provided!"); 344 setIfMissing(mol, MarkedElement.ID_KEY, "mol" + ++molId); 345 layoutBackups.add(new LayoutBackup(mol)); 346 } 347 348 // ensure we have coordinates, generate them if not 349 // we also rescale the molecules such that all bond 350 // lengths are the same. 351 prepareCoords(mols); 352 353 // highlight parts 354 for (Map.Entry<IChemObject, Color> e : highlight.entrySet()) 355 e.getKey().setProperty(StandardGenerator.HIGHLIGHT_COLOR, e.getValue()); 356 357 // setup the model scale 358 List<IAtomContainer> molList = FluentIterable.from(mols).toList(); 359 DepictionGenerator copy = this.withParam(BasicSceneGenerator.Scale.class, 360 caclModelScale(molList)); 361 362 // generate bound rendering elements 363 final RendererModel model = copy.getModel(); 364 final List<Bounds> molElems = copy.generate(molList, model, 1); 365 366 // reset molecule coordinates 367 for (LayoutBackup backup : layoutBackups) 368 backup.reset(); 369 370 // generate titles (if enabled) 371 final List<Bounds> titles = new ArrayList<>(); 372 if (copy.getParameterValue(BasicSceneGenerator.ShowMoleculeTitle.class)) { 373 for (IAtomContainer mol : mols) 374 titles.add(copy.generateTitle(mol, model.get(BasicSceneGenerator.Scale.class))); 375 } 376 377 // remove current highlight buffer 378 for (IChemObject obj : this.highlight.keySet()) 379 obj.removeProperty(StandardGenerator.HIGHLIGHT_COLOR); 380 this.highlight.clear(); 381 382 return new MolGridDepiction(model, molElems, titles, dimensions, nrow, ncol); 383 } 384 385 /** 386 * Prepare a collection of molecules for rendering. If coordinates are not 387 * present they are generated, if coordinates exists they are scaled to 388 * be consistent (length=1.5). 389 * 390 * @param mols molecules 391 * @return coordinates 392 * @throws CDKException 393 */ prepareCoords(Iterable<IAtomContainer> mols)394 private void prepareCoords(Iterable<IAtomContainer> mols) throws CDKException { 395 for (IAtomContainer mol : mols) { 396 if (!ensure2dLayout(mol) && mol.getBondCount() > 0) { 397 final double factor = GeometryUtil.getScaleFactor(mol, 1.5); 398 GeometryUtil.scaleMolecule(mol, factor); 399 } 400 } 401 } 402 setIfMissing(IChemObject chemObject, String key, String val)403 private static void setIfMissing(IChemObject chemObject, String key, String val) { 404 if (chemObject.getProperty(key) == null) 405 chemObject.setProperty(key, val); 406 } 407 408 /** 409 * Depict a reaction. 410 * 411 * @param rxn reaction instance 412 * @return depiction 413 * @throws CDKException a depiction could not be generated 414 */ depict(IReaction rxn)415 public Depiction depict(IReaction rxn) throws CDKException { 416 417 ensure2dLayout(rxn); // can reorder components if align is enabled! 418 419 final Color fgcol = getParameterValue(StandardGenerator.AtomColor.class).getAtomColor(rxn.getBuilder() 420 .newInstance(IAtom.class, "C")); 421 422 final List<IAtomContainer> reactants = toList(rxn.getReactants()); 423 final List<IAtomContainer> products = toList(rxn.getProducts()); 424 final List<IAtomContainer> agents = toList(rxn.getAgents()); 425 List<LayoutBackup> layoutBackups = new ArrayList<>(); 426 427 // set ids for tagging elements 428 int molId = 0; 429 for (IAtomContainer mol : reactants) { 430 setIfMissing(mol, MarkedElement.ID_KEY, "mol" + ++molId); 431 setIfMissing(mol, MarkedElement.CLASS_KEY, "reactant"); 432 layoutBackups.add(new LayoutBackup(mol)); 433 } 434 for (IAtomContainer mol : products) { 435 setIfMissing(mol, MarkedElement.ID_KEY, "mol" + ++molId); 436 setIfMissing(mol, MarkedElement.CLASS_KEY, "product"); 437 layoutBackups.add(new LayoutBackup(mol)); 438 } 439 for (IAtomContainer mol : agents) { 440 setIfMissing(mol, MarkedElement.ID_KEY, "mol" + ++molId); 441 setIfMissing(mol, MarkedElement.CLASS_KEY, "agent"); 442 layoutBackups.add(new LayoutBackup(mol)); 443 } 444 445 final Map<IChemObject, Color> myHighlight = new HashMap<>(); 446 if (highlightAtomMap) { 447 myHighlight.putAll(makeHighlightAtomMap(reactants, products)); 448 } 449 // user highlight buffer pushes out the atom-map highlight if provided 450 myHighlight.putAll(highlight); 451 highlight.clear(); 452 453 prepareCoords(reactants); 454 prepareCoords(products); 455 prepareCoords(agents); 456 457 // highlight parts 458 for (Map.Entry<IChemObject, Color> e : myHighlight.entrySet()) 459 e.getKey().setProperty(StandardGenerator.HIGHLIGHT_COLOR, e.getValue()); 460 461 // setup the model scale based on bond length 462 final double scale = this.caclModelScale(rxn); 463 final DepictionGenerator copy = this.withParam(BasicSceneGenerator.Scale.class, scale); 464 final RendererModel model = copy.getModel(); 465 466 // reactant/product/agent element generation, we number the reactants, then products then agents 467 List<Bounds> reactantBounds = copy.generate(reactants, model, 1); 468 List<Bounds> productBounds = copy.generate(toList(rxn.getProducts()), model, rxn.getReactantCount()); 469 List<Bounds> agentBounds = copy.generate(toList(rxn.getAgents()), model, rxn.getReactantCount() + rxn.getProductCount()); 470 471 // remove current highlight buffer 472 for (IChemObject obj : myHighlight.keySet()) 473 obj.removeProperty(StandardGenerator.HIGHLIGHT_COLOR); 474 475 // generate a 'plus' element 476 Bounds plus = copy.generatePlusSymbol(scale, fgcol); 477 478 // reset the coordinates to how they were before we invoked depict 479 for (LayoutBackup backup : layoutBackups) 480 backup.reset(); 481 482 final Bounds emptyBounds = new Bounds(); 483 final Bounds title = copy.getParameterValue(BasicSceneGenerator.ShowReactionTitle.class) ? copy.generateTitle(rxn, scale) : emptyBounds; 484 final List<Bounds> reactantTitles = new ArrayList<>(); 485 final List<Bounds> productTitles = new ArrayList<>(); 486 if (copy.getParameterValue(BasicSceneGenerator.ShowMoleculeTitle.class)) { 487 for (IAtomContainer reactant : reactants) 488 reactantTitles.add(copy.generateTitle(reactant, scale)); 489 for (IAtomContainer product : products) 490 productTitles.add(copy.generateTitle(product, scale)); 491 } 492 493 final Bounds conditions = generateReactionConditions(rxn, fgcol, model.get(BasicSceneGenerator.Scale.class)); 494 495 return new ReactionDepiction(model, 496 reactantBounds, productBounds, agentBounds, 497 plus, rxn.getDirection(), dimensions, 498 reactantTitles, 499 productTitles, 500 title, 501 conditions, 502 fgcol); 503 } 504 505 /** 506 * Internal - makes a map of the highlights for reaction mapping. 507 * 508 * @param reactants reaction reactants 509 * @param products reaction products 510 * @return the highlight map 511 */ makeHighlightAtomMap(List<IAtomContainer> reactants, List<IAtomContainer> products)512 private Map<IChemObject, Color> makeHighlightAtomMap(List<IAtomContainer> reactants, 513 List<IAtomContainer> products) { 514 Map<IChemObject, Color> colorMap = new HashMap<>(); 515 Map<Integer, Color> mapToColor = new HashMap<>(); 516 int colorIdx = -1; 517 for (IAtomContainer mol : reactants) { 518 int prevPalletIdx = colorIdx; 519 for (IAtom atom : mol.atoms()) { 520 int mapidx = accessAtomMap(atom); 521 if (mapidx > 0) { 522 if (prevPalletIdx == colorIdx) { 523 colorIdx++; // select next color 524 if (colorIdx >= atomMapColors.length) 525 throw new IllegalArgumentException("Not enough colors to highlight atom mapping, please provide mode"); 526 } 527 Color color = atomMapColors[colorIdx]; 528 colorMap.put(atom, color); 529 mapToColor.put(mapidx, color); 530 } 531 } 532 if (colorIdx > prevPalletIdx) { 533 for (IBond bond : mol.bonds()) { 534 IAtom a1 = bond.getBegin(); 535 IAtom a2 = bond.getEnd(); 536 Color c1 = colorMap.get(a1); 537 Color c2 = colorMap.get(a2); 538 if (c1 != null && c1 == c2) 539 colorMap.put(bond, c1); 540 } 541 } 542 } 543 544 for (IAtomContainer mol : products) { 545 for (IAtom atom : mol.atoms()) { 546 int mapidx = accessAtomMap(atom); 547 if (mapidx > 0) { 548 colorMap.put(atom, mapToColor.get(mapidx)); 549 } 550 } 551 for (IBond bond : mol.bonds()) { 552 IAtom a1 = bond.getBegin(); 553 IAtom a2 = bond.getEnd(); 554 Color c1 = colorMap.get(a1); 555 Color c2 = colorMap.get(a2); 556 if (c1 != null && c1 == c2) 557 colorMap.put(bond, c1); 558 } 559 } 560 561 return colorMap; 562 } 563 accessAtomMap(IAtom atom)564 private Integer accessAtomMap(IAtom atom) { 565 Integer mapidx = atom.getProperty(CDKConstants.ATOM_ATOM_MAPPING, Integer.class); 566 if (mapidx == null) 567 return 0; 568 return mapidx; 569 } 570 generatePlusSymbol(double scale, Color fgcol)571 private Bounds generatePlusSymbol(double scale, Color fgcol) { 572 return new Bounds(StandardGenerator.embedText(font, "+", fgcol, 1 / scale)); 573 } 574 toList(IAtomContainerSet set)575 private List<IAtomContainer> toList(IAtomContainerSet set) { 576 return FluentIterable.from(set.atomContainers()).toList(); 577 } 578 generate(IAtomContainer molecule, RendererModel model, int atomNum)579 private IRenderingElement generate(IAtomContainer molecule, RendererModel model, int atomNum) throws CDKException { 580 581 // tag the atom and bond ids 582 String molId = molecule.getProperty(MarkedElement.ID_KEY); 583 if (molId != null) { 584 int atomId = 0, bondid = 0; 585 for (IAtom atom : molecule.atoms()) 586 setIfMissing(atom, MarkedElement.ID_KEY, molId + "atm" + ++atomId); 587 for (IBond bond : molecule.bonds()) 588 setIfMissing(bond, MarkedElement.ID_KEY, molId + "bnd" + ++bondid); 589 } 590 591 if (annotateAtomNum) { 592 for (IAtom atom : molecule.atoms()) { 593 if (atom.getProperty(StandardGenerator.ANNOTATION_LABEL) != null) 594 throw new UnsupportedOperationException("Multiple annotation labels are not supported."); 595 atom.setProperty(StandardGenerator.ANNOTATION_LABEL, 596 Integer.toString(atomNum++)); 597 } 598 } else if (annotateAtomVal) { 599 for (IAtom atom : molecule.atoms()) { 600 if (atom.getProperty(StandardGenerator.ANNOTATION_LABEL) != null) 601 throw new UnsupportedOperationException("Multiple annotation labels are not supported."); 602 atom.setProperty(StandardGenerator.ANNOTATION_LABEL, 603 atom.getProperty(CDKConstants.COMMENT)); 604 } 605 } else if (annotateAtomMap) { 606 for (IAtom atom : molecule.atoms()) { 607 if (atom.getProperty(StandardGenerator.ANNOTATION_LABEL) != null) 608 throw new UnsupportedOperationException("Multiple annotation labels are not supported."); 609 int mapidx = accessAtomMap(atom); 610 if (mapidx > 0) { 611 atom.setProperty(StandardGenerator.ANNOTATION_LABEL, Integer.toString(mapidx)); 612 } 613 } 614 } 615 616 ElementGroup grp = new ElementGroup(); 617 for (IGenerator<IAtomContainer> gen : gens) 618 grp.add(gen.generate(molecule, model)); 619 620 // cleanup 621 if (annotateAtomNum || annotateAtomMap) { 622 for (IAtom atom : molecule.atoms()) { 623 atom.removeProperty(StandardGenerator.ANNOTATION_LABEL); 624 } 625 } 626 627 return grp; 628 } 629 generate(List<IAtomContainer> mols, RendererModel model, int atomNum)630 private List<Bounds> generate(List<IAtomContainer> mols, RendererModel model, int atomNum) throws CDKException { 631 List<Bounds> elems = new ArrayList<>(); 632 int num = 0; 633 for (IAtomContainer mol : mols) { 634 elems.add(new Bounds(generate(mol, model, atomNum))); 635 atomNum += mol.getAtomCount(); 636 } 637 return elems; 638 } 639 640 /** 641 * Generate a bound element that is the title of the provided molecule. If title 642 * is not specified an empty bounds is returned. 643 * 644 * @param chemObj molecule or reaction 645 * @return bound element 646 */ generateTitle(IChemObject chemObj, double scale)647 private Bounds generateTitle(IChemObject chemObj, double scale) { 648 String title = chemObj.getProperty(CDKConstants.TITLE); 649 if (title == null || title.isEmpty()) 650 return new Bounds(); 651 scale = 1 / scale * getParameterValue(RendererModel.TitleFontScale.class); 652 return new Bounds(MarkedElement.markup(StandardGenerator.embedText(font, title, getParameterValue(RendererModel.TitleColor.class), scale), 653 "title")); 654 } 655 generateReactionConditions(IReaction chemObj, Color fg, double scale)656 private Bounds generateReactionConditions(IReaction chemObj, Color fg, double scale) { 657 String title = chemObj.getProperty(CDKConstants.REACTION_CONDITIONS); 658 if (title == null || title.isEmpty()) 659 return new Bounds(); 660 return new Bounds(MarkedElement.markup(StandardGenerator.embedText(font, title, fg, 1/scale), 661 "conditions")); 662 } 663 664 665 /** 666 * Automatically generate coordinates if a user has provided a molecule without them. 667 * 668 * @param container a molecule 669 * @return if coordinates needed to be generated 670 * @throws CDKException coordinates could not be generated 671 */ ensure2dLayout(IAtomContainer container)672 private boolean ensure2dLayout(IAtomContainer container) throws CDKException { 673 if (!GeometryUtil.has2DCoordinates(container)) { 674 StructureDiagramGenerator sdg = new StructureDiagramGenerator(); 675 sdg.generateCoordinates(container); 676 return true; 677 } 678 return false; 679 } 680 681 /** 682 * Automatically generate coordinates if a user has provided reaction without them. 683 * 684 * @param rxn reaction 685 * @throws CDKException coordinates could not be generated 686 */ ensure2dLayout(IReaction rxn)687 private void ensure2dLayout(IReaction rxn) throws CDKException { 688 if (!GeometryUtil.has2DCoordinates(rxn)) { 689 StructureDiagramGenerator sdg = new StructureDiagramGenerator(); 690 sdg.setAlignMappedReaction(alignMappedReactions); 691 sdg.generateCoordinates(rxn); 692 } 693 } 694 695 /** 696 * Color atom symbols using typical colors, oxygens are red, nitrogens are 697 * blue, etc. 698 * 699 * @return new generator for method chaining 700 * @see StandardGenerator.AtomColor 701 * @see StandardGenerator.Highlighting 702 * @see StandardGenerator.HighlightStyle 703 * @see CDK2DAtomColors 704 */ withAtomColors()705 public DepictionGenerator withAtomColors() { 706 return withAtomColors(new CDK2DAtomColors()); 707 } 708 709 /** 710 * Color atom symbols using provided colorer. 711 * 712 * @return new generator for method chaining 713 * @see StandardGenerator.AtomColor 714 * @see StandardGenerator.Highlighting 715 * @see StandardGenerator.HighlightStyle 716 * @see CDK2DAtomColors 717 * @see org.openscience.cdk.renderer.color.UniColor 718 */ withAtomColors(IAtomColorer colorer)719 public DepictionGenerator withAtomColors(IAtomColorer colorer) { 720 return withParam(StandardGenerator.AtomColor.class, colorer); 721 } 722 723 /** 724 * Change the background color. 725 * 726 * @param color background color 727 * @return new generator for method chaining 728 * @see BackgroundColor 729 */ withBackgroundColor(Color color)730 public DepictionGenerator withBackgroundColor(Color color) { 731 return withParam(BackgroundColor.class, color); 732 } 733 734 /** 735 * Highlights are shown as an outer glow around the atom symbols and bonds 736 * rather than recoloring. The width of the glow can be set but defaults to 737 * 4x the stroke width. 738 * 739 * @return new generator for method chaining 740 * @see StandardGenerator.Highlighting 741 * @see StandardGenerator.HighlightStyle 742 */ withOuterGlowHighlight()743 public DepictionGenerator withOuterGlowHighlight() { 744 return withOuterGlowHighlight(4); 745 } 746 747 /** 748 * Highlights are shown as an outer glow around the atom symbols and bonds 749 * rather than recoloring. 750 * 751 * @param width width of the outer glow relative to the bond stroke 752 * @return new generator for method chaining 753 * @see StandardGenerator.Highlighting 754 * @see StandardGenerator.HighlightStyle 755 */ withOuterGlowHighlight(double width)756 public DepictionGenerator withOuterGlowHighlight(double width) { 757 return withParam(StandardGenerator.Highlighting.class, 758 StandardGenerator.HighlightStyle.OuterGlow) 759 .withParam(StandardGenerator.OuterGlowWidth.class, 760 width); 761 } 762 763 /** 764 * Display atom numbers on the molecule or reaction. The numbers are based on the 765 * ordering of atoms in the molecule data structure and not a systematic system 766 * such as IUPAC numbering. 767 * 768 * Note: A depiction can not have both atom numbers and atom maps visible 769 * (but this can be achieved by manually setting the annotation). 770 * 771 * @return new generator for method chaining 772 * @see #withAtomMapNumbers() 773 * @see StandardGenerator#ANNOTATION_LABEL 774 */ withAtomNumbers()775 public DepictionGenerator withAtomNumbers() { 776 if (annotateAtomMap || annotateAtomVal) 777 throw new IllegalArgumentException("Can not annotated atom numbers, atom values or maps are already annotated"); 778 DepictionGenerator copy = new DepictionGenerator(this); 779 copy.annotateAtomNum = true; 780 return copy; 781 } 782 783 /** 784 * Display atom values on the molecule or reaction. The values need to be assigned by 785 * 786 * <pre>{@code 787 * atom.setProperty(CDKConstants.COMMENT, myValueToBeDisplayedNextToAtom); 788 * }</pre> 789 * 790 * Note: A depiction can not have both atom numbers and atom maps visible 791 * (but this can be achieved by manually setting the annotation). 792 * 793 * @return new generator for method chaining 794 * @see #withAtomMapNumbers() 795 * @see StandardGenerator#ANNOTATION_LABEL 796 */ withAtomValues()797 public DepictionGenerator withAtomValues() { 798 if (annotateAtomNum || annotateAtomMap) 799 throw new IllegalArgumentException("Can not annotated atom values, atom numbers or maps are already annotated"); 800 DepictionGenerator copy = new DepictionGenerator(this); 801 copy.annotateAtomVal = true; 802 return copy; 803 } 804 805 /** 806 * Display atom-atom mapping numbers on a reaction. Each atom map index 807 * is loaded from the property {@link CDKConstants#ATOM_ATOM_MAPPING}. 808 * 809 * Note: A depiction can not have both atom numbers and atom 810 * maps visible (but this can be achieved by manually setting 811 * the annotation). 812 * 813 * @return new generator for method chaining 814 * @see #withAtomNumbers() 815 * @see CDKConstants#ATOM_ATOM_MAPPING 816 * @see StandardGenerator#ANNOTATION_LABEL 817 */ withAtomMapNumbers()818 public DepictionGenerator withAtomMapNumbers() { 819 if (annotateAtomNum) 820 throw new IllegalArgumentException("Can not annotated atom maps, atom numbers or values are already annotated"); 821 DepictionGenerator copy = new DepictionGenerator(this); 822 copy.annotateAtomMap = true; 823 return copy; 824 } 825 826 /** 827 * Adds to the highlight the coloring of reaction atom-maps. The 828 * optional color array is used as the pallet with which to 829 * highlight. If none is provided a set of high-contrast colors 830 * will be used. 831 * 832 * @return new generator for method chaining 833 * @see #withAtomMapNumbers() 834 * @see #withAtomMapHighlight() 835 */ withAtomMapHighlight()836 public DepictionGenerator withAtomMapHighlight() { 837 return withAtomMapHighlight(KELLY_MAX_CONTRAST); 838 } 839 840 /** 841 * Adds to the highlight the coloring of reaction atom-maps. The 842 * optional color array is used as the pallet with which to 843 * highlight. If none is provided a set of high-contrast colors 844 * will be used. 845 * 846 * @param colors array of colors 847 * @return new generator for method chaining 848 * @see #withAtomMapNumbers() 849 * @see #withAtomMapHighlight() 850 */ withAtomMapHighlight(Color[] colors)851 public DepictionGenerator withAtomMapHighlight(Color[] colors) { 852 DepictionGenerator copy = new DepictionGenerator(this); 853 copy.highlightAtomMap = true; 854 copy.atomMapColors = Arrays.copyOf(colors, colors.length); 855 return copy; 856 857 } 858 859 /** 860 * Display a molecule title with each depiction. The title 861 * is specified by setting the {@link org.openscience.cdk.CDKConstants#TITLE} 862 * property. For reactions only the main components have their 863 * title displayed. 864 * 865 * @return new generator for method chaining 866 * @see BasicSceneGenerator.ShowMoleculeTitle 867 */ withMolTitle()868 public DepictionGenerator withMolTitle() { 869 return withParam(BasicSceneGenerator.ShowMoleculeTitle.class, 870 true); 871 } 872 873 /** 874 * Display a reaction title with the depiction. The title 875 * is specified by setting the {@link org.openscience.cdk.CDKConstants#TITLE} 876 * property on the {@link IReaction} instance. 877 * 878 * @return new generator for method chaining 879 * @see BasicSceneGenerator.ShowReactionTitle 880 */ withRxnTitle()881 public DepictionGenerator withRxnTitle() { 882 return withParam(BasicSceneGenerator.ShowReactionTitle.class, 883 true); 884 } 885 886 /** 887 * Specifies that reactions with atom-atom mappings should have their reactants/product 888 * coordinates aligned. Default: true. 889 * 890 * @param val setting value 891 * @return new generator for method chaining 892 */ withMappedRxnAlign(boolean val)893 public DepictionGenerator withMappedRxnAlign(boolean val) { 894 DepictionGenerator copy = new DepictionGenerator(this); 895 copy.alignMappedReactions = val; 896 return copy; 897 } 898 899 /** 900 * Set the color annotations (e.g. atom-numbers) will appear in. 901 * 902 * @param color the color of annotations 903 * @return new generator for method chaining 904 * @see StandardGenerator.AnnotationColor 905 */ withAnnotationColor(Color color)906 public DepictionGenerator withAnnotationColor(Color color) { 907 return withParam(StandardGenerator.AnnotationColor.class, 908 color); 909 } 910 911 /** 912 * Set the size of annotations relative to atom symbols. 913 * 914 * @param scale the scale of annotations 915 * @return new generator for method chaining 916 * @see StandardGenerator.AnnotationFontScale 917 */ withAnnotationScale(double scale)918 public DepictionGenerator withAnnotationScale(double scale) { 919 return withParam(StandardGenerator.AnnotationFontScale.class, 920 scale); 921 } 922 923 /** 924 * Set the color titles will appear in. 925 * 926 * @param color the color of titles 927 * @return new generator for method chaining 928 * @see RendererModel.TitleColor 929 */ withTitleColor(Color color)930 public DepictionGenerator withTitleColor(Color color) { 931 return withParam(RendererModel.TitleColor.class, 932 color); 933 } 934 935 /** 936 * Set the size of titles compared to atom symbols. 937 * 938 * @param scale the scale of titles 939 * @return new generator for method chaining 940 * @see RendererModel.TitleFontScale 941 */ withTitleScale(double scale)942 public DepictionGenerator withTitleScale(double scale) { 943 return withParam(RendererModel.TitleFontScale.class, 944 scale); 945 } 946 947 /** 948 * Display atom symbols for terminal carbons (i.e. Methyl) 949 * groups. 950 * 951 * @return new generator for method chaining 952 * @see StandardGenerator.Visibility 953 */ withTerminalCarbons()954 public DepictionGenerator withTerminalCarbons() { 955 return withParam(StandardGenerator.Visibility.class, 956 SelectionVisibility.disconnected(SymbolVisibility.iupacRecommendations())); 957 } 958 959 /** 960 * Display atom symbols for all atoms in the molecule. 961 * 962 * @return new generator for method chaining 963 * @see StandardGenerator.Visibility 964 */ withCarbonSymbols()965 public DepictionGenerator withCarbonSymbols() { 966 return withParam(StandardGenerator.Visibility.class, 967 SymbolVisibility.all()); 968 } 969 970 /** 971 * Highlight the provided set of atoms and bonds in the depiction in the 972 * specified color. 973 * 974 * Calling this methods appends to the current highlight buffer. The buffer 975 * is cleared after each depiction is generated (e.g. {@link #depict(IAtomContainer)}). 976 * 977 * @param chemObjs set of atoms and bonds 978 * @param color the color to highlight 979 * @return new generator for method chaining 980 * @see StandardGenerator#HIGHLIGHT_COLOR 981 */ withHighlight(Iterable<? extends IChemObject> chemObjs, Color color)982 public DepictionGenerator withHighlight(Iterable<? extends IChemObject> chemObjs, Color color) { 983 DepictionGenerator copy = new DepictionGenerator(this); 984 for (IChemObject chemObj : chemObjs) { 985 if (chemObj instanceof IAtomContainer) { 986 for (IAtom atom : ((IAtomContainer) chemObj).atoms()) 987 copy.highlight.put(atom, color); 988 for (IBond bond : ((IAtomContainer) chemObj).bonds()) 989 copy.highlight.put(bond, color); 990 } 991 else copy.highlight.put(chemObj, color); 992 } 993 return copy; 994 } 995 996 /** 997 * Specify a desired size of depiction. The units depend on the output format with 998 * raster images using pixels and vector graphics using millimeters. By default depictions 999 * are only ever made smaller if you would also like to make depictions fill all available 1000 * space use the {@link #withFillToFit()} option. 1001 * 1002 * Currently the size must either both be precisely specified (e.g. 256x256) or 1003 * automatic (e.g. {@link #AUTOMATIC}x{@link #AUTOMATIC}) you cannot for example 1004 * specify a fixed height and automatic width. 1005 * 1006 * @param w max width 1007 * @param h max height 1008 * @return new generator for method chaining 1009 * @see #withFillToFit() 1010 */ withSize(double w, double h)1011 public DepictionGenerator withSize(double w, double h) { 1012 if (w < 0 && h >= 0 || h < 0 && w >= 0) 1013 throw new IllegalArgumentException("Width and height must either both be automatic or both specified"); 1014 DepictionGenerator copy = new DepictionGenerator(this); 1015 copy.dimensions = w == AUTOMATIC ? Dimensions.AUTOMATIC : new Dimensions(w, h); 1016 return copy; 1017 } 1018 1019 /** 1020 * Specify a desired size of margin. The units depend on the output format with 1021 * raster images using pixels and vector graphics using millimeters. 1022 * 1023 * @param m margin 1024 * @return new generator for method chaining 1025 * @see BasicSceneGenerator.Margin 1026 */ withMargin(double m)1027 public DepictionGenerator withMargin(double m) { 1028 return withParam(BasicSceneGenerator.Margin.class, 1029 m); 1030 } 1031 1032 /** 1033 * Specify a desired size of padding for molecule sets and reactions. The units 1034 * depend on the output format with raster images using pixels and vector graphics 1035 * using millimeters. 1036 * 1037 * @param p padding 1038 * @return new generator for method chaining 1039 * @see RendererModel.Padding 1040 */ withPadding(double p)1041 public DepictionGenerator withPadding(double p) { 1042 return withParam(RendererModel.Padding.class, 1043 p); 1044 } 1045 1046 /** 1047 * Specify a desired zoom factor - this changes the base size of a 1048 * depiction and is used for uniformly making depictions bigger. If 1049 * you would like to simply fill all available space (not recommended) 1050 * use {@link #withFillToFit()}. 1051 * 1052 * The zoom is a scaling factor, specifying a zoom of 2 is double size, 1053 * 0.5 half size, etc. 1054 * 1055 * @param zoom zoom factor 1056 * @return new generator for method chaining 1057 * @see BasicSceneGenerator.ZoomFactor 1058 */ withZoom(double zoom)1059 public DepictionGenerator withZoom(double zoom) { 1060 return withParam(BasicSceneGenerator.ZoomFactor.class, 1061 zoom); 1062 } 1063 1064 /** 1065 * Resize depictions to fill all available space (only if a size is specified). 1066 * This generally isn't wanted as very small molecules (e.g. acetaldehyde) may 1067 * become huge. 1068 * 1069 * @return new generator for method chaining 1070 * @see BasicSceneGenerator.FitToScreen 1071 */ withFillToFit()1072 public DepictionGenerator withFillToFit() { 1073 return withParam(BasicSceneGenerator.FitToScreen.class, 1074 true); 1075 } 1076 1077 /** 1078 * When aromaticity is set on bonds, display this in the diagram. IUPAC 1079 * recommends depicting kekulé structures to avoid ambiguity but it's common 1080 * practice to render delocalised rings "donuts" or "life buoys". With fused 1081 * rings this can be somewhat confusing as you end up with three lines at 1082 * the fusion point. <br> 1083 * By default small rings are renders as donuts with dashed bonds used 1084 * otherwise. You can use dashed bonds always by turning off the 1085 * {@link DelocalisedDonutsBondDisplay}. 1086 * 1087 * @return new generator for method chaining 1088 * @see ForceDelocalisedBondDisplay 1089 * @see DelocalisedDonutsBondDisplay 1090 */ withAromaticDisplay()1091 public DepictionGenerator withAromaticDisplay() { 1092 return withParam(ForceDelocalisedBondDisplay.class, 1093 true); 1094 } 1095 1096 /** 1097 * Low-level option method to set a rendering model parameter. 1098 * 1099 * @param key option key 1100 * @param value option value 1101 * @param <T> option key type 1102 * @param <U> option value type 1103 * @return new generator for method chaining 1104 */ withParam(Class<T> key, U value)1105 public <T extends IGeneratorParameter<S>, S, U extends S> DepictionGenerator withParam(Class<T> key, U value) { 1106 DepictionGenerator copy = new DepictionGenerator(this); 1107 copy.setParam(key, value); 1108 return copy; 1109 } 1110 caclModelScale(Collection<IAtomContainer> mols)1111 private double caclModelScale(Collection<IAtomContainer> mols) { 1112 List<IBond> bonds = new ArrayList<>(); 1113 for (IAtomContainer mol : mols) { 1114 for (IBond bond : mol.bonds()) { 1115 bonds.add(bond); 1116 } 1117 } 1118 return calcModelScaleForBondLength(medianBondLength(bonds)); 1119 } 1120 caclModelScale(IReaction rxn)1121 private double caclModelScale(IReaction rxn) { 1122 List<IAtomContainer> mols = new ArrayList<>(); 1123 for (IAtomContainer mol : rxn.getReactants().atomContainers()) 1124 mols.add(mol); 1125 for (IAtomContainer mol : rxn.getProducts().atomContainers()) 1126 mols.add(mol); 1127 for (IAtomContainer mol : rxn.getAgents().atomContainers()) 1128 mols.add(mol); 1129 return caclModelScale(mols); 1130 } 1131 medianBondLength(Collection<IBond> bonds)1132 private double medianBondLength(Collection<IBond> bonds) { 1133 if (bonds.isEmpty()) 1134 return 1.5; 1135 int nBonds = 0; 1136 double[] lengths = new double[bonds.size()]; 1137 for (IBond bond : bonds) { 1138 Point2d p1 = bond.getBegin().getPoint2d(); 1139 Point2d p2 = bond.getEnd().getPoint2d(); 1140 // watch out for overlaid atoms (occur in multiple group Sgroups) 1141 if (!p1.equals(p2)) 1142 lengths[nBonds++] = p1.distance(p2); 1143 } 1144 Arrays.sort(lengths, 0, nBonds); 1145 return lengths[nBonds / 2]; 1146 } 1147 calcModelScaleForBondLength(double bondLength)1148 private double calcModelScaleForBondLength(double bondLength) { 1149 return getParameterValue(BasicSceneGenerator.BondLength.class) / bondLength; 1150 } 1151 getDefaultOsFont()1152 private static String getDefaultOsFont() { 1153 // TODO: Native Font Support - choose best for Win/Linux/OS X etc 1154 return Font.SANS_SERIF; 1155 } 1156 1157 /** 1158 * Utility class for storing coordinates and bond types and resetting them after use. 1159 */ 1160 private static final class LayoutBackup { 1161 private final Point2d[] coords; 1162 private final IBond.Stereo[] btypes; 1163 private final IAtomContainer mol; 1164 LayoutBackup(IAtomContainer mol)1165 public LayoutBackup(IAtomContainer mol) { 1166 final int numAtoms = mol.getAtomCount(); 1167 final int numBonds = mol.getBondCount(); 1168 this.coords = new Point2d[numAtoms]; 1169 this.btypes = new IBond.Stereo[numBonds]; 1170 this.mol = mol; 1171 for (int i = 0; i < numAtoms; i++) { 1172 IAtom atom = mol.getAtom(i); 1173 coords[i] = atom.getPoint2d(); 1174 if (coords[i] != null) 1175 atom.setPoint2d(new Point2d(coords[i])); // copy 1176 } 1177 for (int i = 0; i < numBonds; i++) { 1178 IBond bond = mol.getBond(i); 1179 btypes[i] = bond.getStereo(); 1180 } 1181 } 1182 reset()1183 void reset() { 1184 final int numAtoms = mol.getAtomCount(); 1185 final int numBonds = mol.getBondCount(); 1186 for (int i = 0; i < numAtoms; i++) 1187 mol.getAtom(i).setPoint2d(coords[i]); 1188 for (int i = 0; i < numBonds; i++) 1189 mol.getBond(i).setStereo(btypes[i]); 1190 } 1191 } 1192 } 1193