1 /*
2  * Copyright (c) 2014 European Bioinformatics Institute (EMBL-EBI)
3  *                    John May <jwmay@users.sf.net>
4  *
5  * Contact: cdk-devel@lists.sourceforge.net
6  *
7  * This program is free software; you can redistribute it and/or modify it
8  * under the terms of the GNU Lesser General Public License as published by
9  * the Free Software Foundation; either version 2.1 of the License, or (at
10  * your option) any later version. All we ask is that proper credit is given
11  * for our work, which includes - but is not limited to - adding the above
12  * copyright notice to the beginning of your source code files, and to any
13  * copyright notice that you may distribute with programs based on this work.
14  *
15  * This program is distributed in the hope that it will be useful, but WITHOUT
16  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
17  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
18  * License for more details.
19  *
20  * You should have received a copy of the GNU Lesser General Public License
21  * along with this program; if not, write to the Free Software
22  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 U
23  */
24 
25 package org.openscience.cdk.renderer.generators;
26 
27 import org.openscience.cdk.interfaces.IAtom;
28 import org.openscience.cdk.interfaces.IAtomContainer;
29 import org.openscience.cdk.interfaces.IBond;
30 import org.openscience.cdk.interfaces.IChemObject;
31 import org.openscience.cdk.renderer.RendererModel;
32 import org.openscience.cdk.renderer.elements.ElementGroup;
33 import org.openscience.cdk.renderer.elements.GeneralPath;
34 import org.openscience.cdk.renderer.elements.IRenderingElement;
35 import org.openscience.cdk.renderer.generators.parameter.AbstractGeneratorParameter;
36 
37 import java.awt.Color;
38 import java.awt.Shape;
39 import java.awt.geom.AffineTransform;
40 import java.awt.geom.Area;
41 import java.awt.geom.RoundRectangle2D;
42 import java.util.Arrays;
43 import java.util.HashMap;
44 import java.util.List;
45 import java.util.Map;
46 
47 /**
48  * Generate an under/overlaid highlight in structure depictions. The highlight
49  * emphasises atoms and bonds. Each atom and bond is optionally assigned an
50  * integer identifier. Entities with identifiers are then highlighted using the
51  * {@link Palette} to determine the color. The size of the highlight is
52  * specified with the {@link HighlightRadius} parameter.
53  *
54  *
55  * Basic usage:
56  * <blockquote><pre>{@code
57  * // create with the highlight generator
58  * AtomContainerRenderer renderer = ...;
59  *
60  * IAtomContainer            m   = ...; // input molecule
61  * Map<IChemObject, Integer> ids = new HashMap<>();
62  *
63  * // set atom/bond ids, atoms with no id will not be highlighted, numbering
64  * // starts at 0
65  * ids.put(m.getAtom(0), 0);
66  * ids.put(m.getAtom(1), 0);
67  * ids.put(m.getAtom(2), 0);
68  * ids.put(m.getAtom(5), 2);
69  * ids.put(m.getAtom(6), 1);
70  *
71  * ids.put(m.getBond(0), 0);
72  * ids.put(m.getBond(1), 0);
73  * ids.put(m.getBond(3), 1);
74  * ids.put(m.getBond(4), 2);
75  *
76  * // attach ids to the structure
77  * m.setProperty(HighlightGenerator.ID_MAP, ids);
78  *
79  * // draw
80  * renderer.paint(m, new AWTDrawVisitor(g2), bounds, true);
81  * }</pre></blockquote>
82  *
83  * By default colours are automatically generated, to assign specific colors
84  * a custom {@link Palette} must be used. Here are some examples of setting
85  * the palette parameter in the renderer.
86  *
87  * <blockquote><pre>
88  * AtomContainerRenderer renderer = ...;
89  *
90  * // opaque colors
91  * renderer.getRenderer2DModel()
92  *         .set(HighlightGenerator.HighlightPalette.class,
93  *              HighlightGenerator.createPalette(Color.RED, Color.BLUE, Color.GREEN));
94  *
95  * // opaque colors (hex)
96  * renderer.getRenderer2DModel()
97  *         .set(HighlightGenerator.HighlightPalette.class,
98  *              HighlightGenerator.createPalette(new Color(0xff0000), Color.BLUE, Color.GREEN));
99  *
100  * // first color is transparent
101  * renderer.getRenderer2DModel()
102  *         .set(HighlightGenerator.HighlightPalette.class,
103  *              HighlightGenerator.createPalette(new Color(0x88ff0000, true), Color.BLUE, Color.GREEN));
104  * </pre></blockquote>
105  *
106  * @author John May
107  * @cdk.module renderextra
108  * @cdk.githash
109  */
110 public final class HighlightGenerator implements IGenerator<IAtomContainer> {
111 
112     /** The atom radius on screen. */
113     private final HighlightRadius  highlightRadius  = new HighlightRadius();
114 
115     /** Color palette to use. */
116     private final HighlightPalette highlightPalette = new HighlightPalette();
117 
118     /** Property key. */
119     public static final String     ID_MAP           = "cdk.highlight.id";
120 
121     /**{@inheritDoc} */
122     @Override
generate(IAtomContainer container, RendererModel model)123     public IRenderingElement generate(IAtomContainer container, RendererModel model) {
124 
125         final Map<IChemObject, Integer> highlight = container.getProperty(ID_MAP);
126 
127         if (highlight == null) return null;
128 
129         final Palette palette = model.getParameter(HighlightPalette.class).getValue();
130         final double radius = model.getParameter(HighlightRadius.class).getValue()
131                 / model.getParameter(BasicSceneGenerator.Scale.class).getValue();
132 
133         final Map<Integer, Area> shapes = new HashMap<Integer, Area>();
134 
135         for (IAtom atom : container.atoms()) {
136 
137             Integer id = highlight.get(atom);
138 
139             if (id == null) continue;
140 
141             Area area = shapes.get(id);
142             Shape shape = createAtomHighlight(atom, radius);
143 
144             if (area == null)
145                 shapes.put(id, new Area(shape));
146             else
147                 area.add(new Area(shape));
148         }
149 
150         for (IBond bond : container.bonds()) {
151 
152             Integer id = highlight.get(bond);
153 
154             if (id == null) continue;
155 
156             Area area = shapes.get(id);
157             Shape shape = createBondHighlight(bond, radius);
158 
159             if (area == null)
160                 shapes.put(id, (area = new Area(shape)));
161             else
162                 area.add(new Area(shape));
163 
164             // punch out the area occupied by atoms highlighted with a
165             // different color
166 
167             IAtom   a1   = bond.getBegin(), a2 = bond.getEnd();
168             Integer a1Id = highlight.get(a1), a2Id = highlight.get(a2);
169 
170             if (a1Id != null && !a1Id.equals(id)) area.subtract(shapes.get(a1Id));
171             if (a2Id != null && !a2Id.equals(id)) area.subtract(shapes.get(a2Id));
172         }
173 
174         // create rendering elements for each highlight shape
175         ElementGroup group = new ElementGroup();
176         for (Map.Entry<Integer, Area> e : shapes.entrySet()) {
177             group.add(GeneralPath.shapeOf(e.getValue(), palette.color(e.getKey())));
178         }
179 
180         return group;
181     }
182 
183     /**
184      * Create the shape which will highlight the provided atom.
185      *
186      * @param atom   the atom to highlight
187      * @param radius the specified radius
188      * @return the shape which will highlight the atom
189      */
createAtomHighlight(IAtom atom, double radius)190     private static Shape createAtomHighlight(IAtom atom, double radius) {
191         double x = atom.getPoint2d().x;
192         double y = atom.getPoint2d().y;
193 
194         return new RoundRectangle2D.Double(x - radius, y - radius, 2 * radius, 2 * radius, 2 * radius, 2 * radius);
195     }
196 
197     /**
198      * Create the shape which will highlight the provided bond.
199      *
200      * @param bond   the bond to highlight
201      * @param radius the specified radius
202      * @return the shape which will highlight the atom
203      */
createBondHighlight(IBond bond, double radius)204     private static Shape createBondHighlight(IBond bond, double radius) {
205 
206         double x1 = bond.getBegin().getPoint2d().x;
207         double x2 = bond.getEnd().getPoint2d().x;
208         double y1 = bond.getBegin().getPoint2d().y;
209         double y2 = bond.getEnd().getPoint2d().y;
210 
211         double dx = x2 - x1;
212         double dy = y2 - y1;
213 
214         double mag = Math.sqrt((dx * dx) + (dy * dy));
215 
216         dx /= mag;
217         dy /= mag;
218 
219         double r2 = radius / 2;
220 
221         Shape s = new RoundRectangle2D.Double(x1 - r2, y1 - r2, mag + radius, radius, radius, radius);
222 
223         double theta = Math.atan2(dy, dx);
224 
225         return AffineTransform.getRotateInstance(theta, x1, y1).createTransformedShape(s);
226     }
227 
228     /**{@inheritDoc} */
229     @Override
getParameters()230     public List<IGeneratorParameter<?>> getParameters() {
231         return Arrays.asList(new IGeneratorParameter<?>[]{highlightRadius, highlightPalette});
232     }
233 
234     /**
235      * Create a palette which uses the provided colors.
236      *
237      * @param colors colors to use in the palette
238      * @return a palette to use in highlighting
239      */
createPalette(final Color[] colors)240     public static Palette createPalette(final Color[] colors) {
241         return new FixedPalette(colors);
242     }
243 
244     /**
245      * Create a palette which uses the provided colors.
246      *
247      * @param colors colors to use in the palette
248      * @return a palette to use in highlighting
249      */
createPalette(final Color color, final Color... colors)250     public static Palette createPalette(final Color color, final Color... colors) {
251         Color[] cs = new Color[colors.length + 1];
252         cs[0] = color;
253         System.arraycopy(colors, 0, cs, 1, colors.length);
254         return new FixedPalette(cs);
255     }
256 
257     /**
258      * Create an auto generating palette which will generate colors using the
259      * provided parameters.
260      *
261      * @param saturation color saturation, 0.0 &lt; x &lt; 1.0
262      * @param brightness color brightness, 0.0 &lt; x &lt; 1.0
263      * @param alpha color alpha (transparency), 0 &lt; x &lt; 255
264      * @return a palette to use in highlighting
265      */
createAutoPalette(float saturation, float brightness, int alpha)266     public static Palette createAutoPalette(float saturation, float brightness, int alpha) {
267         return new AutoGenerated(5, saturation, brightness, alpha);
268     }
269 
270     /**
271      * Create an auto generating palette which will generate colors using the
272      * provided parameters.
273      *
274      * @param saturation color saturation, 0.0 &lt; x &lt; 1.0
275      * @param brightness color brightness, 0.0 &lt; x &lt; 1.0
276      * @param transparent generate transparent colors, 0 &lt; x &lt; 255
277      * @return a palette to use in highlighting
278      */
createAutoGenPalette(float saturation, float brightness, boolean transparent)279     public static Palette createAutoGenPalette(float saturation, float brightness, boolean transparent) {
280         return new AutoGenerated(5, saturation, brightness, transparent ? 200 : 255);
281     }
282 
283     /**
284      * Create an auto generating palette which will generate colors using the
285      * provided parameters.
286      *
287      * @param transparent generate transparent colors
288      * @return a palette to use in highlighting
289      */
createAutoGenPalette(boolean transparent)290     public static Palette createAutoGenPalette(boolean transparent) {
291         return new AutoGenerated(5, transparent ? 200 : 255);
292     }
293 
294     /**
295      * Defines a color palette, the palette should provide a color the specified
296      * identifier (id).
297      */
298     public static interface Palette {
299 
300         /**
301          * Obtain the color in index, id.
302          *
303          * @param id the id of the color
304          * @return a color
305          */
color(int id)306         Color color(int id);
307     }
308 
309     /**
310      * A palette that allows one to define the precise colors of each class. The
311      * colors are passed in the constructor.
312      */
313     private static final class FixedPalette implements Palette {
314 
315         /** Colors of the palette. */
316         private final Color[] colors;
317 
318         /**
319          * Create a fixed palette for the specified colors.
320          *
321          * @param colors the colors in the palette.
322          */
FixedPalette(Color[] colors)323         public FixedPalette(Color[] colors) {
324             this.colors = Arrays.copyOf(colors, colors.length);
325         }
326 
327         /**
328          *{@inheritDoc}
329          */
330         @Override
color(int id)331         public Color color(int id) {
332             if (id < 0) throw new IllegalArgumentException("id should be positive");
333             if (id >= colors.length) throw new IllegalArgumentException("no color has been provided for id=" + id);
334             return colors[id];
335         }
336     }
337 
338     /**
339      * An automatically generating color palette. The palette use the golden
340      * ratio to generate colors with varied hue.
341      *
342      * @see <a href="http://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/">Create Random Colors Programmatically</a>
343      */
344     private static final class AutoGenerated implements Palette {
345 
346         /** Golden ratio. */
347         private static final float PHI    = 0.618033988749895f;
348 
349         /** Starting color - adjust for a different start color. */
350         private static final int   offset = 14;
351 
352         /** The colors. */
353         private Color[]            colors;
354 
355         /** Color alpha. */
356         private final int          alpha;
357 
358         /** The saturation and brightness values. */
359         private final float        saturation, brightness;
360 
361         /**
362          * Create an automatically generating color palette.
363          *
364          * @param n     pre-generate this many colors
365          * @param alpha transparency (0-255)
366          */
AutoGenerated(int n, int alpha)367         public AutoGenerated(int n, int alpha) {
368             this(n, 0.45f, 0.95f, alpha);
369         }
370 
371         /**
372          * Create an automatically generating color palette.
373          *
374          * @param n          pre-generate this many colors
375          * @param saturation color saturation (0-1f)
376          * @param brightness color brightness (0-1f)
377          * @param alpha      transparency (0-255)
378          */
AutoGenerated(int n, float saturation, float brightness, int alpha)379         public AutoGenerated(int n, float saturation, float brightness, int alpha) {
380             this.colors = new Color[n];
381             this.alpha = alpha;
382             this.saturation = saturation;
383             this.brightness = brightness;
384             fill(colors, 0, n - 1);
385         }
386 
387         /**
388          * Fill the indices, from - to inclusive, in the colors array with
389          * generated colors.
390          *
391          * @param colors indexed colors
392          * @param from   first index
393          * @param to     last index
394          */
fill(Color[] colors, int from, int to)395         private void fill(Color[] colors, int from, int to) {
396             if (alpha < 255) {
397                 for (int i = from; i <= to; i++) {
398                     Color c = Color.getHSBColor((offset + i) * PHI, saturation, brightness);
399                     colors[i] = new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha);
400                 }
401             } else {
402                 for (int i = from; i <= to; i++)
403                     colors[i] = Color.getHSBColor((offset + i) * PHI, saturation, brightness);
404             }
405         }
406 
407         /**{@inheritDoc} */
408         @Override
color(int id)409         public Color color(int id) {
410             if (id < 0) throw new IllegalArgumentException("id should be positive");
411             if (id >= colors.length) {
412                 int org = colors.length;
413                 colors = Arrays.copyOf(colors, id * 2);
414                 fill(colors, org, colors.length - 1);
415             }
416             return colors[id];
417         }
418     }
419 
420     /**
421      * Magic number with unknown units that defines the radius around an atom,
422      * e.g. used for highlighting atoms.
423      */
424     public static class HighlightRadius extends AbstractGeneratorParameter<Double> {
425 
426         /**
427          * Returns the default value.
428          *
429          * @return 10.0
430          */
431         @Override
getDefault()432         public Double getDefault() {
433             return 10.0;
434         }
435     }
436 
437     /** Default color palette. */
438     private static final Palette DEFAULT_PALETTE = createAutoGenPalette(true);
439 
440     /** Defines the color palette used to provide the highlight colors. */
441     public static class HighlightPalette extends AbstractGeneratorParameter<Palette> {
442 
443         /**
444          * Returns the default value.
445          *
446          * @return an auto-generating palette
447          */
448         @Override
getDefault()449         public Palette getDefault() {
450             return DEFAULT_PALETTE;
451         }
452     }
453 }
454