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