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 < x < 1.0 262 * @param brightness color brightness, 0.0 < x < 1.0 263 * @param alpha color alpha (transparency), 0 < x < 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 < x < 1.0 275 * @param brightness color brightness, 0.0 < x < 1.0 276 * @param transparent generate transparent colors, 0 < x < 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