org.openscience.cdk.app.DepictController.java Source code

Java tutorial

Introduction

Here is the source code for org.openscience.cdk.app.DepictController.java

Source

/*
 * Copyright (C) 2015  John May
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
 * USA
 */

package org.openscience.cdk.app;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.simolecule.centres.CdkLabeller;
import org.openscience.cdk.CDKConstants;
import org.openscience.cdk.depict.Abbreviations;
import org.openscience.cdk.depict.Depiction;
import org.openscience.cdk.depict.DepictionGenerator;
import org.openscience.cdk.exception.CDKException;
import org.openscience.cdk.graph.Cycles;
import org.openscience.cdk.interfaces.IAtom;
import org.openscience.cdk.interfaces.IAtomContainer;
import org.openscience.cdk.interfaces.IBond;
import org.openscience.cdk.interfaces.IChemObject;
import org.openscience.cdk.interfaces.IChemObjectBuilder;
import org.openscience.cdk.interfaces.IReaction;
import org.openscience.cdk.interfaces.IStereoElement;
import org.openscience.cdk.io.MDLV2000Reader;
import org.openscience.cdk.renderer.RendererModel;
import org.openscience.cdk.renderer.SymbolVisibility;
import org.openscience.cdk.renderer.color.CDK2DAtomColors;
import org.openscience.cdk.renderer.color.IAtomColorer;
import org.openscience.cdk.renderer.color.UniColor;
import org.openscience.cdk.renderer.generators.standard.StandardGenerator;
import org.openscience.cdk.renderer.generators.standard.StandardGenerator.Visibility;
import org.openscience.cdk.sgroup.Sgroup;
import org.openscience.cdk.sgroup.SgroupKey;
import org.openscience.cdk.sgroup.SgroupType;
import org.openscience.cdk.silent.AtomContainer;
import org.openscience.cdk.silent.SilentChemObjectBuilder;
import org.openscience.cdk.smiles.SmilesParser;
import org.openscience.cdk.smiles.smarts.SmartsPattern;
import org.openscience.cdk.stereo.TetrahedralChirality;
import org.openscience.cdk.tools.manipulator.AtomContainerManipulator;
import org.openscience.cdk.tools.manipulator.ReactionManipulator;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.imageio.ImageIO;
import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Chemical structure depiction controller.
 */
@Controller
public class DepictController {

    private final ExecutorService smartsExecutor = Executors.newFixedThreadPool(4);

    // chem object builder to create objects with
    private final IChemObjectBuilder builder = SilentChemObjectBuilder.getInstance();

    // we make are raster depictions slightly smalled by default (40px bond length)
    private final DepictionGenerator generator = new DepictionGenerator();
    private SmilesParser smipar = new SmilesParser(builder);

    private final Abbreviations abbreviations = new Abbreviations();
    private final Abbreviations reagents = new Abbreviations();

    public DepictController() throws IOException {
        int count = this.abbreviations.loadFromFile("/org/openscience/cdk/app/abbreviations.smi");
        this.reagents.loadFromFile("/org/openscience/cdk/app/reagents.smi");
        abbreviations.setContractOnHetero(false);
    }

    /**
     * Home page redirect.
     */
    @RequestMapping("/")
    public String redirect() {
        return "redirect:/depict.html";
    }

    /**
     * Restful entry point.
     *
     * @param smi      SMILES to depict
     * @param fmt      output format
     * @param style    preset style COW (Color-on-white), COB, BOW, COW
     * @param anon     rendering anonymised graphs
     * @param zoom     zoom factor (1=100%=none)
     * @param annotate annotations to add
     * @param w        width of the image
     * @param h        height of the image
     * @param sma      highlight SMARTS pattern
     * @return the depicted structure
     * @throws CDKException something not okay with input
     * @throws IOException  problem reading/writing request
     */
    @RequestMapping("depict/{style}/{fmt}")
    public HttpEntity<?> depict(@RequestParam("smi") String smi, @PathVariable("fmt") String fmt,
            @PathVariable("style") String style,
            @RequestParam(value = "suppressh", defaultValue = "true") String suppressh,
            @RequestParam(value = "hdisp", defaultValue = "bridgehead") String hDisplayParam,
            @RequestParam(value = "anon", defaultValue = "off") String anon,
            @RequestParam(value = "zoom", defaultValue = "1.3") double zoom,
            @RequestParam(value = "annotate", defaultValue = "none") String annotate,
            @RequestParam(value = "w", defaultValue = "-1") int w,
            @RequestParam(value = "h", defaultValue = "-1") int h,
            @RequestParam(value = "abbr", defaultValue = "reagents") String abbr,
            @RequestParam(value = "sma", defaultValue = "") String sma,
            @RequestParam(value = "showtitle", defaultValue = "false") boolean showTitle,
            @RequestParam(value = "smalim", defaultValue = "100") int smaLimit,
            @RequestParam(value = "alignrxnmap", defaultValue = "true") boolean alignRxnMap,
            @RequestParam Map<String, String> params) throws CDKException, IOException {

        // backwards compatibility
        HydrogenDisplayType hDisplayType = HydrogenDisplayType.Minimal;
        if ("false".equalsIgnoreCase(suppressh) || "f".equalsIgnoreCase(suppressh)) {
            hDisplayType = HydrogenDisplayType.Provided;
        } else {
            if (hDisplayParam != null) {
                switch (hDisplayParam.toLowerCase()) {
                case "suppressed":
                    hDisplayType = HydrogenDisplayType.Minimal;
                    break;
                case "provided":
                    hDisplayType = HydrogenDisplayType.Provided;
                    break;
                case "stereo":
                    hDisplayType = HydrogenDisplayType.StereoOnly;
                    break;
                case "bridgeheadtetrahedral":
                case "bridgehead":
                case "default":
                case "smart":
                    hDisplayType = HydrogenDisplayType.BridgeHeadTetrahedralOnly;
                    break;
                }
            }
        }

        // Note: DepictionGenerator is immutable
        DepictionGenerator myGenerator = generator.withSize(w, h).withZoom(zoom);

        // Configure style preset
        myGenerator = withStyle(myGenerator, style);

        myGenerator = myGenerator.withAnnotationScale(0.7).withAnnotationColor(Color.RED);

        // align rxn maps
        myGenerator = myGenerator.withMappedRxnAlign(alignRxnMap);

        // Improved depiction of anatomised graphs, e.g. ***1*****1**
        if (anon.equalsIgnoreCase("on")) {
            myGenerator = myGenerator.withParam(Visibility.class, new SymbolVisibility() {
                @Override
                public boolean visible(IAtom iAtom, List<IBond> list, RendererModel rendererModel) {
                    return list.isEmpty();
                }
            });
        }

        final boolean isRxn = !smi.contains("V2000") && isRxnSmi(smi);
        final boolean isRgp = smi.contains("RG:");
        IReaction rxn = null;
        IAtomContainer mol = null;
        List<IAtomContainer> mols = null;

        Set<IChemObject> highlight;

        if (isRxn) {
            rxn = smipar.parseReactionSmiles(smi);
            for (IAtomContainer component : rxn.getReactants().atomContainers())
                setHydrogenDisplay(component, hDisplayType);
            for (IAtomContainer component : rxn.getProducts().atomContainers())
                setHydrogenDisplay(component, hDisplayType);
            for (IAtomContainer component : rxn.getAgents().atomContainers())
                setHydrogenDisplay(component, hDisplayType);
            highlight = findHits(sma, rxn, mol, smaLimit);
            abbreviate(rxn, abbr, annotate);
        } else {
            mol = loadMol(smi);
            setHydrogenDisplay(mol, hDisplayType);
            highlight = findHits(sma, rxn, mol, smaLimit);
            abbreviate(mol, abbr, annotate);
        }

        // Add annotations
        switch (annotate) {
        case "number":
            myGenerator = myGenerator.withAtomNumbers();
            abbr = "false";
            break;
        case "mapidx":
            myGenerator = myGenerator.withAtomMapNumbers();
            break;
        case "atomvalue":
            myGenerator = myGenerator.withAtomValues();
            break;
        case "colmap":
            myGenerator = myGenerator.withAtomMapHighlight(
                    new Color[] { new Color(169, 199, 255), new Color(185, 255, 180), new Color(255, 162, 162),
                            new Color(253, 139, 255), new Color(255, 206, 86), new Color(227, 227, 227) })
                    .withOuterGlowHighlight(6d);
            break;
        case "cip":
            if (isRxn) {
                for (IAtomContainer part : ReactionManipulator.getAllAtomContainers(rxn)) {
                    annotateCip(part);
                }
            } else {
                annotateCip(mol);
            }
            break;
        }

        // add highlight from atom/bonds hit by the provided SMARTS
        switch (style) {
        case "nob":
            myGenerator = myGenerator.withHighlight(highlight, new Color(0xffaaaa));
            break;
        case "bow":
        case "wob":
            myGenerator = myGenerator.withHighlight(highlight, new Color(0xff0000));
            break;
        default:
            myGenerator = myGenerator.withHighlight(highlight, new Color(0xaaffaa));
            break;
        }

        if (showTitle) {
            if (isRxn)
                myGenerator = myGenerator.withRxnTitle();
            else
                myGenerator = myGenerator.withMolTitle();
        }

        // pre-render the depiction
        final Depiction depiction = isRxn ? myGenerator.depict(rxn)
                : isRgp ? myGenerator.depict(mols, mols.size(), 1) : myGenerator.depict(mol);

        final String fmtlc = fmt.toLowerCase(Locale.ROOT);
        switch (fmtlc) {
        case Depiction.SVG_FMT:
            return makeResponse(depiction.toSvgStr().getBytes(), "image/svg+xml");
        case Depiction.PDF_FMT:
            return makeResponse(depiction.toPdfStr().getBytes(), "application/pdf");
        case Depiction.PNG_FMT:
        case Depiction.JPG_FMT:
        case Depiction.GIF_FMT:
            ByteArrayOutputStream bao = new ByteArrayOutputStream();
            ImageIO.write(depiction.toImg(), fmtlc, bao);
            return makeResponse(bao.toByteArray(), "image/" + fmtlc);
        }

        throw new IllegalArgumentException("Unsupported format.");
    }

    private void annotateCip(IAtomContainer part) {
        CdkLabeller.label(part);
        for (IAtom atom : part.atoms()) {
            if (atom.getProperty("cip.label") != null)
                atom.setProperty(StandardGenerator.ANNOTATION_LABEL,
                        StandardGenerator.ITALIC_DISPLAY_PREFIX + atom.getProperty("cip.label"));
        }
        for (IBond bond : part.bonds()) {
            if (bond.getProperty("cip.label") != null)
                bond.setProperty(StandardGenerator.ANNOTATION_LABEL,
                        StandardGenerator.ITALIC_DISPLAY_PREFIX + bond.getProperty("cip.label"));
        }
    }

    private void setHydrogenDisplay(IAtomContainer mol, HydrogenDisplayType hDisplayType) {
        switch (hDisplayType) {
        case Minimal:
            AtomContainerManipulator.suppressHydrogens(mol);
            break;
        case StereoOnly: {
            AtomContainerManipulator.suppressHydrogens(mol);
            List<IStereoElement> ses = new ArrayList<>();
            for (IStereoElement se : mol.stereoElements()) {
                switch (se.getConfigClass()) {
                case IStereoElement.Tetrahedral: {
                    IAtom focus = (IAtom) se.getFocus();
                    if (focus.getImplicitHydrogenCount() == 1) {
                        focus.setImplicitHydrogenCount(0);
                        IAtom hydrogen = sproutHydrogen(mol, focus);
                        IStereoElement tmp = se.map(Collections.singletonMap(focus, hydrogen));
                        ses.add(new TetrahedralChirality(focus, (IAtom[]) tmp.getCarriers().toArray(new IAtom[4]),
                                tmp.getConfig()));
                    } else {
                        ses.add(se);
                    }
                }
                    break;
                case IStereoElement.CisTrans: {
                    IBond focus = (IBond) se.getFocus();
                    IAtom beg = focus.getBegin();
                    IAtom end = focus.getEnd();
                    if (beg.getImplicitHydrogenCount() == 1) {
                        beg.setImplicitHydrogenCount(0);
                        sproutHydrogen(mol, beg);
                    }
                    if (end.getImplicitHydrogenCount() == 1) {
                        end.setImplicitHydrogenCount(0);
                        sproutHydrogen(mol, end);
                    }
                    // don't need to update stereo element
                    ses.add(se);
                }
                    break;
                default:
                    ses.add(se);
                    break;
                }
            }
            mol.setStereoElements(ses);
        }
            break;
        case BridgeHeadTetrahedralOnly: {
            Cycles.markRingAtomsAndBonds(mol);
            AtomContainerManipulator.suppressHydrogens(mol);
            List<IStereoElement> ses = new ArrayList<>();
            for (IStereoElement se : mol.stereoElements()) {
                switch (se.getConfigClass()) {
                case IStereoElement.Tetrahedral: {
                    IAtom focus = (IAtom) se.getFocus();
                    if (focus.getImplicitHydrogenCount() == 1
                            && shouldAddH(mol, focus, mol.getConnectedBondsList(focus))) {
                        focus.setImplicitHydrogenCount(0);
                        IAtom hydrogen = sproutHydrogen(mol, focus);
                        IStereoElement tmp = se.map(Collections.singletonMap(focus, hydrogen));
                        ses.add(new TetrahedralChirality(focus, (IAtom[]) tmp.getCarriers().toArray(new IAtom[4]),
                                tmp.getConfig()));
                    } else {
                        ses.add(se);
                    }
                }
                    break;
                default:
                    ses.add(se);
                    break;
                }
            }
            mol.setStereoElements(ses);
        }
            break;
        case Provided:
        default:
            break;
        }
    }

    private boolean shouldAddH(IAtomContainer mol, IAtom atom, Iterable<IBond> bonds) {
        int count = 0;
        for (IBond bond : bonds) {
            if (bond.isInRing()) {
                ++count;
            } else {
                IAtom nbr = bond.getOther(atom);
                for (IStereoElement se : mol.stereoElements()) {
                    if (se.getConfigClass() == IStereoElement.TH && se.getFocus().equals(nbr)) {
                        count++;
                    }
                }
            }
        }
        return count == 3;
    }

    private IAtom sproutHydrogen(IAtomContainer mol, IAtom focus) {
        IAtom hydrogen = mol.getBuilder().newAtom();
        hydrogen.setAtomicNumber(1);
        hydrogen.setSymbol("H");
        hydrogen.setImplicitHydrogenCount(0);
        mol.addAtom(hydrogen);
        mol.addBond(mol.indexOf(focus), mol.getAtomCount() - 1, IBond.Order.SINGLE);
        return hydrogen;
    }

    private void contractHydrates(IAtomContainer mol) {
        Set<IAtom> hydrate = new HashSet<>();
        for (IAtom atom : mol.atoms()) {
            if (atom.getAtomicNumber() == 8 && atom.getImplicitHydrogenCount() == 2
                    && mol.getConnectedAtomsList(atom).size() == 0)
                hydrate.add(atom);
        }
        if (hydrate.size() < 2)
            return;
        @SuppressWarnings("unchecked")
        List<Sgroup> sgroups = mol.getProperty(CDKConstants.CTAB_SGROUPS, List.class);

        if (sgroups == null)
            mol.setProperty(CDKConstants.CTAB_SGROUPS, sgroups = new ArrayList<>());
        else
            sgroups = new ArrayList<>(sgroups);

        Sgroup sgrp = new Sgroup();
        for (IAtom atom : hydrate)
            sgrp.addAtom(atom);
        sgrp.putValue(SgroupKey.CtabParentAtomList, Collections.singleton(hydrate.iterator().next()));
        sgrp.setType(SgroupType.CtabMultipleGroup);
        sgrp.setSubscript(Integer.toString(hydrate.size()));

        sgroups.add(sgrp);
    }

    private boolean add(Set<IAtom> set, Set<IAtom> atomsToAdd) {
        boolean res = true;
        for (IAtom atom : atomsToAdd) {
            if (!set.add(atom))
                res = false;
        }
        return res;
    }

    private void abbreviate(IReaction rxn, String mode, String annotate) {
        Multimap<IAtomContainer, Sgroup> sgroupmap = ArrayListMultimap.create();
        switch (mode.toLowerCase()) {
        case "true":
        case "on":
        case "yes":
            for (IAtomContainer mol : rxn.getReactants().atomContainers()) {
                contractHydrates(mol);
                Set<IAtom> atoms = new HashSet<>();
                List<Sgroup> newSgroups = new ArrayList<>();
                for (Sgroup sgroup : abbreviations.generate(mol)) {
                    if (add(atoms, sgroup.getAtoms()))
                        newSgroups.add(sgroup);
                }
                sgroupmap.putAll(mol, newSgroups);
            }
            for (IAtomContainer mol : rxn.getProducts().atomContainers()) {
                contractHydrates(mol);
                Set<IAtom> atoms = new HashSet<>();
                List<Sgroup> newSgroups = new ArrayList<>();
                for (Sgroup sgroup : abbreviations.generate(mol)) {
                    if (add(atoms, sgroup.getAtoms()))
                        newSgroups.add(sgroup);
                }
                sgroupmap.putAll(mol, newSgroups);
            }
            for (IAtomContainer mol : rxn.getAgents().atomContainers()) {
                contractHydrates(mol);
                reagents.apply(mol);
                abbreviations.apply(mol);
            }
            break;
        case "groups":
            for (IAtomContainer mol : rxn.getAgents().atomContainers()) {
                contractHydrates(mol);
                abbreviations.apply(mol);
            }
            break;
        case "reagents":
            for (IAtomContainer mol : rxn.getAgents().atomContainers()) {
                contractHydrates(mol);
                reagents.apply(mol);
            }
            break;
        }

        Set<String> include = new HashSet<>();
        for (Map.Entry<IAtomContainer, Sgroup> e : sgroupmap.entries()) {
            final IAtomContainer mol = e.getKey();
            final Sgroup abbrv = e.getValue();
            int numAtoms = mol.getAtomCount();
            if (abbrv.getBonds().isEmpty()) {
                include.add(abbrv.getSubscript());
            } else {
                int numAbbr = abbrv.getAtoms().size();
                double f = numAbbr / (double) numAtoms;
                if (numAtoms - numAbbr > 1 && f <= 0.4) {
                    include.add(abbrv.getSubscript());
                }
            }
        }

        for (Map.Entry<IAtomContainer, Collection<Sgroup>> e : sgroupmap.asMap().entrySet()) {
            final IAtomContainer mol = e.getKey();

            List<Sgroup> sgroups = mol.getProperty(CDKConstants.CTAB_SGROUPS);
            if (sgroups == null)
                sgroups = new ArrayList<>();
            else
                sgroups = new ArrayList<>(sgroups);
            mol.setProperty(CDKConstants.CTAB_SGROUPS, sgroups);

            for (Sgroup abbrv : e.getValue()) {
                if (include.contains(abbrv.getSubscript()))
                    sgroups.add(abbrv);
            }
        }
    }

    private void abbreviate(IAtomContainer mol, String mode, String annotate) {
        switch (mode.toLowerCase()) {
        case "true":
        case "on":
        case "yes":
        case "groups":
            contractHydrates(mol);
            abbreviations.apply(mol);
            break;
        case "reagents":
            contractHydrates(mol);
            break;
        }
        // remove abbreviations of mapped atoms
        if ("mapidx".equals(annotate)) {
            List<Sgroup> sgroups = mol.getProperty(CDKConstants.CTAB_SGROUPS);
            List<Sgroup> filtered = new ArrayList<>();
            if (sgroups != null) {
                for (Sgroup sgroup : sgroups) {
                    // turn off display short-cuts
                    if (sgroup.getType() == SgroupType.CtabAbbreviation
                            || sgroup.getType() == SgroupType.CtabMultipleGroup) {
                        boolean okay = true;
                        for (IAtom atom : sgroup.getAtoms()) {
                            if (atom.getProperty(CDKConstants.ATOM_ATOM_MAPPING) != null) {
                                okay = false;
                                break;
                            }
                        }
                        if (okay)
                            filtered.add(sgroup);
                    } else {
                        filtered.add(sgroup);
                    }
                }
                mol.setProperty(CDKConstants.CTAB_SGROUPS, filtered);
            }
        }
    }

    private boolean isRxnSmi(String smi) {
        return smi.split(" ")[0].contains(">");
    }

    private IAtomContainer loadMol(String str) throws CDKException {
        if (str.contains("V2000")) {
            try (MDLV2000Reader mdlr = new MDLV2000Reader(new StringReader(str))) {
                return mdlr.read(new AtomContainer(0, 0, 0, 0));
            } catch (CDKException | IOException e3) {
                throw new CDKException("Could not parse input");
            }
        } else {
            try {
                return smipar.parseSmiles(str);
            } catch (CDKException ex) {
                SmilesParser smipar2 = new SmilesParser(builder);
                smipar2.kekulise(false);
                return smipar2.parseSmiles(str);
            }
        }
    }

    private HttpEntity<byte[]> makeResponse(byte[] bytes, String contentType) {
        HttpHeaders header = new HttpHeaders();
        String type = contentType.substring(0, contentType.indexOf('/'));
        String subtype = contentType.substring(contentType.indexOf('/') + 1, contentType.length());
        header.setContentType(new MediaType(type, subtype));
        header.add("Access-Control-Allow-Origin", "*");
        header.set(HttpHeaders.CACHE_CONTROL, "max-age=31536000");
        header.setContentLength(bytes.length);
        return new HttpEntity<>(bytes, header);
    }

    /**
     * Set the depiction style.
     *
     * @param generator the generator
     * @param style     style type
     * @return configured depiction generator
     */
    private static DepictionGenerator withStyle(DepictionGenerator generator, String style) {
        switch (style) {
        case "cow":
            generator = generator.withAtomColors(new CDK2DAtomColors()).withBackgroundColor(Color.WHITE)
                    .withOuterGlowHighlight();
            break;
        case "cot":
            generator = generator.withAtomColors(new CDK2DAtomColors()).withBackgroundColor(new Color(0, 0, 0, 0))
                    .withOuterGlowHighlight();
            break;
        case "bow":
            generator = generator.withAtomColors(new UniColor(Color.BLACK)).withBackgroundColor(Color.WHITE);
            break;
        case "bot":
            generator = generator.withAtomColors(new UniColor(Color.BLACK))
                    .withBackgroundColor(new Color(0, 0, 0, 0));
            break;
        case "wob":
            generator = generator.withAtomColors(new UniColor(Color.WHITE)).withBackgroundColor(Color.BLACK);
            break;
        case "cob":
            generator = generator.withAtomColors(new CobColorer()).withBackgroundColor(Color.BLACK)
                    .withOuterGlowHighlight();
            break;
        case "nob":
            generator = generator.withAtomColors(new NobColorer()).withBackgroundColor(Color.BLACK)
                    .withOuterGlowHighlight();
            break;
        }
        return generator;
    }

    /**
     * Color-on-black atom colors.
     */
    private static final class CobColorer implements IAtomColorer {
        private final CDK2DAtomColors colors = new CDK2DAtomColors();

        @Override
        public Color getAtomColor(IAtom atom) {
            Color res = colors.getAtomColor(atom);
            if (res.equals(Color.BLACK))
                return Color.WHITE;
            else
                return res;
        }

        @Override
        public Color getAtomColor(IAtom atom, Color color) {
            Color res = colors.getAtomColor(atom);
            if (res.equals(Color.BLACK))
                return Color.WHITE;
            else
                return res;
        }
    }

    /**
     * Neon-on-black atom colors.
     */
    private static final class NobColorer implements IAtomColorer {
        private final CDK2DAtomColors colors = new CDK2DAtomColors();
        private final Color NEON = new Color(0x00FF0E);

        @Override
        public Color getAtomColor(IAtom atom) {
            Color res = colors.getAtomColor(atom);
            if (res.equals(Color.BLACK))
                return NEON;
            else
                return res;
        }

        @Override
        public Color getAtomColor(IAtom atom, Color color) {
            Color res = colors.getAtomColor(atom, color);
            if (res.equals(Color.BLACK))
                return NEON;
            else
                return res;
        }
    }

    /**
     * Find matching atoms and bonds in the reaction or molecule.
     *
     * @param sma SMARTS pattern
     * @param rxn reaction
     * @param mol molecule
     * @return set of matched atoms and bonds
     */
    private Set<IChemObject> findHits(final String sma, final IReaction rxn, final IAtomContainer mol,
            final int limit) {

        Set<IChemObject> highlight = new HashSet<>();
        if (!sma.isEmpty()) {
            SmartsPattern smartsPattern;
            try {
                smartsPattern = SmartsPattern.create(sma, null);
            } catch (Exception | Error e) {
                return Collections.emptySet();
            }
            if (mol != null) {
                for (Map<IChemObject, IChemObject> m : smartsPattern.matchAll(mol).limit(limit).uniqueAtoms()
                        .toAtomBondMap()) {
                    for (Map.Entry<IChemObject, IChemObject> e : m.entrySet()) {
                        highlight.add(e.getValue());
                    }
                }
            } else if (rxn != null) {
                for (Map<IChemObject, IChemObject> m : smartsPattern.matchAll(rxn).limit(limit).uniqueAtoms()
                        .toAtomBondMap()) {
                    for (Map.Entry<IChemObject, IChemObject> e : m.entrySet()) {
                        highlight.add(e.getValue());
                    }
                }
            }
        }
        return highlight;

    }
}