private IRenderingElement generateMixtureSgroup(Sgroup sgroup) {
    // draw the brackets
    // TODO - mixtures normally have attached Sgroup data
    // TODO - e.g. COMPONENT_FRACTION, ACTIVITY_TYPE, WEIGHT_PERCENT
    List<SgroupBracket> brackets = sgroup.getValue(SgroupKey.CtabBracket);
    if (brackets != null) {

      SgroupType type = sgroup.getType();
      String subscript = "?";
      switch (type) {
        case CtabComponent:
          Integer compNum = sgroup.getValue(SgroupKey.CtabComponentNumber);
          if (compNum != null) subscript = "c" + Integer.toString(compNum);
          else subscript = "c";
          break;
        case CtabMixture:
          subscript = "mix";
          break;
        case CtabFormulation:
          subscript = "f";
          break;
      }

      return generateSgroupBrackets(sgroup, brackets, subscript, null);
    } else {
      return new ElementGroup();
    }
  }
  /**
   * Hide the atoms and bonds of a contracted abbreviation. If the abbreviations is attached we
   * remap the attachment symbol to display the name. If there are no attachments the symbol we be
   * added later ({@see #generateSgroups}).
   *
   * @param container molecule
   * @param sgroup abbreviation group display shortcut
   */
  private static void contractAbbreviation(
      IAtomContainer container, Map<IAtom, String> symbolRemap, Sgroup sgroup) {

    final Set<IBond> crossing = sgroup.getBonds();
    final Set<IAtom> atoms = sgroup.getAtoms();

    // only do 0,1 attachments for now
    if (crossing.size() > 1) return;

    for (IAtom atom : atoms) {
      StandardGenerator.hide(atom);
    }
    for (IBond bond : container.bonds()) {
      if (atoms.contains(bond.getAtom(0)) || atoms.contains(bond.getAtom(1)))
        StandardGenerator.hide(bond);
    }
    for (IBond bond : crossing) {
      StandardGenerator.unhide(bond);
      IAtom a1 = bond.getAtom(0);
      IAtom a2 = bond.getAtom(1);
      StandardGenerator.unhide(a1);
      if (atoms.contains(a1)) symbolRemap.put(a1, sgroup.getSubscript());
      StandardGenerator.unhide(a2);
      if (atoms.contains(a2)) symbolRemap.put(a2, sgroup.getSubscript());
    }
  }
 private IRenderingElement generateMultipleSgroup(Sgroup sgroup) {
   // just draw the brackets - multiplied group parts have already been hidden in prep phase
   List<SgroupBracket> brackets = sgroup.getValue(SgroupKey.CtabBracket);
   if (brackets != null) {
     return generateSgroupBrackets(
         sgroup, brackets, (String) sgroup.getValue(SgroupKey.CtabSubScript), null);
   } else {
     return new ElementGroup();
   }
 }
  /**
   * Generates polymer Sgroup elements.
   *
   * @param sgroup the Sgroup
   * @return the rendered elements (empty if no brackets defined)
   */
  private IRenderingElement generatePolymerSgroup(Sgroup sgroup) {
    // draw the brackets
    List<SgroupBracket> brackets = sgroup.getValue(SgroupKey.CtabBracket);
    if (brackets != null) {

      SgroupType type = sgroup.getType();

      String subscript = sgroup.getValue(SgroupKey.CtabSubScript);
      String connectivity = sgroup.getValue(SgroupKey.CtabConnectivity);

      switch (type) {
        case CtabCopolymer:
          subscript = "co";
          String subtype = sgroup.getValue(SgroupKey.CtabSubType);
          if ("RAN".equals(subtype)) subscript = "ran";
          else if ("BLK".equals(subtype)) subscript = "blk";
          else if ("ALT".equals(subtype)) subscript = "alt";
          break;
        case CtabCrossLink:
          subscript = "xl";
          break;
        case CtabAnyPolymer:
          subscript = "any";
          break;
        case CtabGraft:
          subscript = "grf";
          break;
        case CtabMer:
          subscript = "mer";
          break;
        case CtabMonomer:
          subscript = "mon";
          break;
        case CtabModified:
          subscript = "mod";
          break;
        case CtabStructureRepeatUnit:
          if (subscript == null) subscript = "n";
          if (connectivity == null) connectivity = "eu";
          break;
      }

      return generateSgroupBrackets(sgroup, brackets, subscript, connectivity);
    } else {
      return new ElementGroup();
    }
  }
 private IRenderingElement generateAbbreviationSgroup(Sgroup sgroup) {
   String label = sgroup.getSubscript();
   // already handled by symbol remapping
   if (sgroup.getBonds().size() > 0 || label == null || label.isEmpty()) {
     return new ElementGroup();
   }
   // we're showing a label where there were no atoms before, we put it in the
   // middle of all of those which were hidden
   final Point2d labelCoords = GeometryUtil.get2DCenter(sgroup.getAtoms());
   ElementGroup group = new ElementGroup();
   for (Shape outline :
       atomGenerator
           .generatePseudoSymbol(label, HydrogenPosition.Right)
           .resize(1 / scale, 1 / -scale)
           .getOutlines()) group.add(GeneralPath.shapeOf(outline, foreground));
   return MarkedElement.markupAtom(group, null);
 }
  /**
   * Hide the repeated atoms and bonds of a multiple group. We hide al atoms that belong to the
   * group unless they are defined in the parent atom list. Any bond to those atoms that is not a
   * crossing bond or one connecting atoms in the parent list is hidden.
   *
   * @param container molecule
   * @param sgroup multiple group display shortcut
   */
  private static void hideMultipleParts(IAtomContainer container, Sgroup sgroup) {

    final Set<IBond> crossing = sgroup.getBonds();
    final Set<IAtom> atoms = sgroup.getAtoms();
    final Set<IAtom> parentAtoms = sgroup.getValue(SgroupKey.CtabParentAtomList);

    for (IBond bond : container.bonds()) {
      if (parentAtoms.contains(bond.getAtom(0)) && parentAtoms.contains(bond.getAtom(1))) continue;
      if (atoms.contains(bond.getAtom(0)) || atoms.contains(bond.getAtom(1)))
        StandardGenerator.hide(bond);
    }
    for (IAtom atom : atoms) {
      if (!parentAtoms.contains(atom)) StandardGenerator.hide(atom);
    }
    for (IBond bond : crossing) {
      StandardGenerator.unhide(bond);
    }
  }
  /**
   * Generate the Sgroup elements for the provided atom contains.
   *
   * @param container molecule
   * @return Sgroup rendering elements
   */
  IRenderingElement generateSgroups(IAtomContainer container) {

    ElementGroup result = new ElementGroup();
    List<Sgroup> sgroups = container.getProperty(CDKConstants.CTAB_SGROUPS);

    if (sgroups == null || sgroups.isEmpty()) return result;

    for (Sgroup sgroup : sgroups) {

      switch (sgroup.getType()) {
        case CtabAbbreviation:
          result.add(generateAbbreviationSgroup(sgroup));
          break;
        case CtabMultipleGroup:
          result.add(generateMultipleSgroup(sgroup));
          break;
        case CtabAnyPolymer:
        case CtabMonomer:
        case CtabCrossLink:
        case CtabCopolymer:
        case CtabStructureRepeatUnit:
        case CtabMer:
        case CtabGraft:
        case CtabModified:
          result.add(generatePolymerSgroup(sgroup));
          break;
        case CtabComponent:
        case CtabMixture:
        case CtabFormulation:
          result.add(generateMixtureSgroup(sgroup));
          break;
        case CtabGeneric:
          // not strictly a polymer but okay to draw as one
          result.add(generatePolymerSgroup(sgroup));
          break;
      }
    }

    return result;
  }
  /**
   * If the molecule has display shortcuts (abbreviations or multiple group sgroups) certain parts
   * of the structure are hidden from display. This method marks the parts to hide and in the case
   * of abbreviations, remaps atom symbols. Appart from additional property flags, the molecule is
   * unchanged by this method.
   *
   * @param container molecule input
   * @param symbolRemap a map that will hold symbol remapping
   */
  static void prepareDisplayShortcuts(IAtomContainer container, Map<IAtom, String> symbolRemap) {

    List<Sgroup> sgroups = container.getProperty(CDKConstants.CTAB_SGROUPS);
    if (sgroups == null || sgroups.isEmpty()) return;

    // select abbreviations that should be contracted
    for (Sgroup sgroup : sgroups) {
      if (sgroup.getType() == SgroupType.CtabAbbreviation) {
        Boolean expansion = sgroup.getValue(SgroupKey.CtabExpansion);
        // abbreviation is displayed as expanded
        if (expansion != null && expansion == Boolean.TRUE) continue;
        // no or empty label, skip it
        if (sgroup.getSubscript() == null || sgroup.getSubscript().isEmpty()) continue;
        contractAbbreviation(container, symbolRemap, sgroup);
      } else if (sgroup.getType() == SgroupType.CtabMultipleGroup) {
        hideMultipleParts(container, sgroup);
      }
    }
  }
  private IRenderingElement generateSgroupBrackets(
      Sgroup sgroup,
      List<SgroupBracket> brackets,
      String subscriptSuffix,
      String superscriptSuffix) {

    // brackets are square by default (style:0)
    Integer style = sgroup.getValue(SgroupKey.CtabBracketStyle);
    boolean round = style != null && style == 1;
    ElementGroup result = new ElementGroup();

    Set<IAtom> atoms = sgroup.getAtoms();
    Set<IBond> crossingBonds = sgroup.getBonds();

    // easy to depict in correct orientation, we just
    // point each bracket at the atom of a crossing
    // bond that is 'in' the group - this scales
    // to more than two brackets

    // first we need to pair the brackets with the bonds
    Map<SgroupBracket, IBond> pairs =
        crossingBonds.size() == brackets.size()
            ? bracketBondPairs(brackets, crossingBonds)
            : Collections.<SgroupBracket, IBond>emptyMap();

    if (!pairs.isEmpty()) {

      SgroupBracket suffixBracket = null;
      Vector2d suffixBracketPerp = null;

      for (Map.Entry<SgroupBracket, IBond> e : pairs.entrySet()) {

        final SgroupBracket bracket = e.getKey();
        final IBond bond = e.getValue();
        final IAtom inGroupAtom =
            atoms.contains(bond.getAtom(0)) ? bond.getAtom(0) : bond.getAtom(1);

        final Point2d p1 = bracket.getFirstPoint();
        final Point2d p2 = bracket.getSecondPoint();

        final Vector2d perp = VecmathUtil.newPerpendicularVector(VecmathUtil.newUnitVector(p1, p2));

        // point the vector at the atom group
        Point2d midpoint = VecmathUtil.midpoint(p1, p2);
        if (perp.dot(VecmathUtil.newUnitVector(midpoint, inGroupAtom.getPoint2d())) < 0) {
          perp.negate();
        }

        perp.scale(bracketDepth);

        Path2D path = new Path2D.Double();
        if (round) {
          // bracket 1 (cp: control point)
          path.moveTo(p1.x + perp.x, p1.y + perp.y);
          Point2d cpb1 = new Point2d(midpoint);
          cpb1.add(VecmathUtil.negate(perp));
          path.quadTo(cpb1.x, cpb1.y, p2.x + perp.x, p2.y + p2.y);
        } else {
          path.moveTo(p1.x + perp.x, p1.y + perp.y);
          path.lineTo(p1.x, p1.y);
          path.lineTo(p2.x, p2.y);
          path.lineTo(p2.x + perp.x, p2.y + perp.y);
        }
        result.add(GeneralPath.outlineOf(path, stroke, foreground));

        if (suffixBracket == null) {
          suffixBracket = bracket;
          suffixBracketPerp = perp;
        } else {
          // is this bracket better as a suffix?
          Point2d sp1 = suffixBracket.getFirstPoint();
          Point2d sp2 = suffixBracket.getSecondPoint();
          double bestMaxX = Math.max(sp1.x, sp2.x);
          double thisMaxX = Math.max(p1.x, p2.x);
          double bestMaxY = Math.max(sp1.y, sp2.y);
          double thisMaxY = Math.max(p1.y, p2.y);

          // choose the most eastern or.. the most southern
          double xDiff = thisMaxX - bestMaxX;
          double yDiff = thisMaxY - bestMaxY;
          if (xDiff > EQUIV_THRESHOLD || (xDiff > -EQUIV_THRESHOLD && yDiff < -EQUIV_THRESHOLD)) {
            suffixBracket = bracket;
            suffixBracketPerp = perp;
          }
        }
      }

      // write the labels
      if (suffixBracket != null) {

        Point2d subSufPnt = suffixBracket.getFirstPoint();
        Point2d supSufPnt = suffixBracket.getSecondPoint();

        // try to put the subscript on the bottom
        double xDiff = subSufPnt.x - supSufPnt.x;
        double yDiff = subSufPnt.y - supSufPnt.y;
        if (yDiff > EQUIV_THRESHOLD || (yDiff > -EQUIV_THRESHOLD && xDiff > EQUIV_THRESHOLD)) {
          Point2d tmpP = subSufPnt;
          subSufPnt = supSufPnt;
          supSufPnt = tmpP;
        }

        // subscript/superscript suffix annotation
        if (subscriptSuffix != null && !subscriptSuffix.isEmpty()) {
          TextOutline subscriptOutline =
              leftAlign(
                  makeText(
                      subscriptSuffix.toLowerCase(Locale.ROOT),
                      subSufPnt,
                      suffixBracketPerp,
                      labelScale));
          result.add(GeneralPath.shapeOf(subscriptOutline.getOutline(), foreground));
        }
        if (superscriptSuffix != null && !superscriptSuffix.isEmpty()) {
          TextOutline superscriptOutline =
              leftAlign(
                  makeText(
                      superscriptSuffix.toLowerCase(Locale.ROOT),
                      supSufPnt,
                      suffixBracketPerp,
                      labelScale));
          result.add(GeneralPath.shapeOf(superscriptOutline.getOutline(), foreground));
        }
      }
    } else if (brackets.size() == 2) {

      final Point2d b1p1 = brackets.get(0).getFirstPoint();
      final Point2d b1p2 = brackets.get(0).getSecondPoint();
      final Point2d b2p1 = brackets.get(1).getFirstPoint();
      final Point2d b2p2 = brackets.get(1).getSecondPoint();

      final Vector2d b1vec = VecmathUtil.newUnitVector(b1p1, b1p2);
      final Vector2d b2vec = VecmathUtil.newUnitVector(b2p1, b2p2);

      final Vector2d b1pvec = VecmathUtil.newPerpendicularVector(b1vec);
      final Vector2d b2pvec = VecmathUtil.newPerpendicularVector(b2vec);

      // Point the vectors at each other
      if (b1pvec.dot(VecmathUtil.newUnitVector(b1p1, b2p1)) < 0) b1pvec.negate();
      if (b2pvec.dot(VecmathUtil.newUnitVector(b2p1, b1p1)) < 0) b2pvec.negate();

      // scale perpendicular vectors by how deep the brackets need to be
      b1pvec.scale(bracketDepth);
      b2pvec.scale(bracketDepth);

      // bad brackets
      if (Double.isNaN(b1pvec.x)
          || Double.isNaN(b1pvec.y)
          || Double.isNaN(b2pvec.x)
          || Double.isNaN(b2pvec.y)) return result;

      Path2D path = new Path2D.Double();

      if (round) {
        // bracket 1 (cp: control point)
        path.moveTo(b1p1.x + b1pvec.x, b1p1.y + b1pvec.y);
        Point2d cpb1 = VecmathUtil.midpoint(b1p1, b1p2);
        cpb1.add(VecmathUtil.negate(b1pvec));
        path.quadTo(cpb1.x, cpb1.y, b1p2.x + b1pvec.x, b1p2.y + b1pvec.y);

        // bracket 2 (cp: control point)
        path.moveTo(b2p1.x + b2pvec.x, b2p1.y + b2pvec.y);
        Point2d cpb2 = VecmathUtil.midpoint(b2p1, b2p2);
        cpb2.add(VecmathUtil.negate(b2pvec));
        path.quadTo(cpb2.x, cpb2.y, b2p2.x + b2pvec.x, b2p2.y + b2pvec.y);
      } else {
        // bracket 1
        path.moveTo(b1p1.x + b1pvec.x, b1p1.y + b1pvec.y);
        path.lineTo(b1p1.x, b1p1.y);
        path.lineTo(b1p2.x, b1p2.y);
        path.lineTo(b1p2.x + b1pvec.x, b1p2.y + b1pvec.y);

        // bracket 2
        path.moveTo(b2p1.x + b2pvec.x, b2p1.y + b2pvec.y);
        path.lineTo(b2p1.x, b2p1.y);
        path.lineTo(b2p2.x, b2p2.y);
        path.lineTo(b2p2.x + b2pvec.x, b2p2.y + b2pvec.y);
      }

      result.add(GeneralPath.outlineOf(path, stroke, foreground));

      // work out where to put the suffix labels (e.g. ht/hh/eu) superscript
      // and (e.g. n, xl, c, mix) subscript
      // TODO: could be improved
      double b1MaxX = Math.max(b1p1.x, b1p2.x);
      double b2MaxX = Math.max(b2p1.x, b2p2.x);
      double b1MaxY = Math.max(b1p1.y, b1p2.y);
      double b2MaxY = Math.max(b2p1.y, b2p2.y);

      Point2d subSufPnt = b2p2;
      Point2d supSufPnt = b2p1;
      Vector2d subpvec = b2pvec;

      double bXDiff = b1MaxX - b2MaxX;
      double bYDiff = b1MaxY - b2MaxY;

      if (bXDiff > EQUIV_THRESHOLD || (bXDiff > -EQUIV_THRESHOLD && bYDiff < -EQUIV_THRESHOLD)) {
        subSufPnt = b1p2;
        supSufPnt = b1p1;
        subpvec = b1pvec;
      }

      double xDiff = subSufPnt.x - supSufPnt.x;
      double yDiff = subSufPnt.y - supSufPnt.y;

      if (yDiff > EQUIV_THRESHOLD || (yDiff > -EQUIV_THRESHOLD && xDiff > EQUIV_THRESHOLD)) {
        Point2d tmpP = subSufPnt;
        subSufPnt = supSufPnt;
        supSufPnt = tmpP;
      }

      // subscript/superscript suffix annotation
      if (subscriptSuffix != null && !subscriptSuffix.isEmpty()) {
        TextOutline subscriptOutline =
            leftAlign(
                makeText(subscriptSuffix.toLowerCase(Locale.ROOT), subSufPnt, subpvec, labelScale));
        result.add(GeneralPath.shapeOf(subscriptOutline.getOutline(), foreground));
      }
      if (superscriptSuffix != null && !superscriptSuffix.isEmpty()) {
        TextOutline superscriptOutline =
            leftAlign(
                makeText(
                    superscriptSuffix.toLowerCase(Locale.ROOT), supSufPnt, subpvec, labelScale));
        result.add(GeneralPath.shapeOf(superscriptOutline.getOutline(), foreground));
      }
    }
    return result;
  }