@Override
 protected void defineProperties() throws OmDeveloperException {
   super.defineProperties();
   defineString(PROPERTY_ALT);
   defineBoolean(PROPERTY_TEXTFONT);
   setBoolean(PROPERTY_TEXTFONT, false);
 }
  @Override
  public void produceVisibleOutput(QContent qc, boolean bInit, boolean bPlain) throws OmException {
    // Get actual current value of string
    String sCurrent = getQuestion().applyPlaceholders(sEquation);

    if (bPlain) {
      // Put text equivalent
      Element eDiv =
          qc.createElement("div"); // Can't use span because they aren't allowed to contain things
      eDiv.setAttribute("style", "display:inline");
      qc.addInlineXHTML(eDiv);
      XML.createText(eDiv, (bSpaceBefore ? " " : "") + getString("alt") + (bSpaceAfter ? " " : ""));
      qc.addTextEquivalent(getString("alt"));

      // Put each placeholder
      for (int i = 0; i < apPlaces.length; i++) {
        // Check content is not hidden
        Place p = apPlaces[i];
        if ((!p.bImplicit && !p.qc.isDisplayed()) || (p.bImplicit && !p.qc.isChildDisplayed()))
          continue;

        // Label followed by content of placeholder
        Element ePlace = XML.createChild(eDiv, "div");
        if (p.sLabel != null) {
          Element eLabel = XML.createChild(ePlace, p.sLabelFor == null ? "span" : "label");
          XML.createText(eLabel, p.sLabel + " ");
          qc.addTextEquivalent(p.sLabel);
          if (p.sLabelFor != null)
            eLabel.setAttribute(
                "for", LabelComponent.getLabel(getQDocument(), bPlain, p.sLabelFor));
        }
        qc.setParent(ePlace);
        p.qc.produceOutput(qc, bInit, bPlain);
        qc.unsetParent();
      }
    } else {
      // Check background colour, foreground, and zoom
      Color cBackground = getBackground();
      if (cBackground == null) cBackground = Color.white;
      Color cForeground =
          getQuestion().isFixedColour()
              ? convertRGB(getQuestion().getFixedColourFG())
              : Color.black;
      double dZoom = getQuestion().getZoom();

      // Hash to filename/identifier
      String sFilename =
          "eq"
              + (sCurrent.hashCode()
                  + cBackground.hashCode() * 3
                  + cForeground.hashCode() * 7
                  + (new Double(dZoom)).hashCode() * 11)
              + (getBoolean(PROPERTY_TEXTFONT) ? "t" : "e")
              + ".png";

      // Make actual image if needed, also get placeholder positions
      if (!sFilename.equals(sSent)) {
        e = Equation.create(sCurrent, (float) dZoom);
        if (getBoolean(PROPERTY_TEXTFONT)) e.setFont("Verdana", new int[] {13, 11, 9});
        BufferedImage bi = e.render(cForeground, cBackground, true);
        qc.addResource(sFilename, "image/png", QContent.convertPNG(bi));
        for (int i = 0; i < apPlaces.length; i++) {
          Point p = e.getPlaceholder(apPlaces[i].sID);
          apPlaces[i].iActualX = p.x;
          apPlaces[i].iActualY = p.y;
        }
        sSent = sFilename;
      }

      Element eEnsureSpaces = qc.createElement("div");
      eEnsureSpaces.setAttribute("class", "equation");
      qc.addInlineXHTML(eEnsureSpaces);

      // If there's a space before, add one here too (otherwise IE eats it)
      if (bSpaceBefore) XML.createText(eEnsureSpaces, " ");

      String sImageID = QDocument.ID_PREFIX + getID() + "_img";
      Element eImg = XML.createChild(eEnsureSpaces, "img");
      eImg.setAttribute("id", sImageID);
      eImg.setAttribute("onmousedown", "return false;"); // Prevent Firefox drag/drop
      eImg.setAttribute("src", "%%RESOURCES%%/" + sFilename);
      eImg.setAttribute("alt", getString("alt"));
      eImg.setAttribute("style", "vertical-align:-" + (e.getHeight() - e.getBaseline()) + "px;");

      if (bSpaceAfter) XML.createText(eEnsureSpaces, " ");

      qc.addTextEquivalent(getString("alt"));

      String sJavascript = "addOnLoad(function() { inlinePositionFix('" + sImageID + "'";

      for (int i = 0; i < apPlaces.length; i++) {
        Place p = apPlaces[i];

        // Must get the label even though not using it, just to indicate that
        // it's been used
        if (p.sLabelFor != null) LabelComponent.getLabel(getQDocument(), bPlain, p.sLabelFor);

        int iEffectiveWidth = (int) Math.round(dZoom * p.iWidth),
            iEffectiveHeight = (int) Math.round(dZoom * p.iHeight);

        String sPlaceholderID = QDocument.ID_PREFIX + getID() + "_" + p.sID;
        Element ePlace = XML.createChild(eEnsureSpaces, "div");
        ePlace.setAttribute("class", "placeholder");
        ePlace.setAttribute("id", sPlaceholderID);
        ePlace.setAttribute(
            "style",
            "width:"
                + iEffectiveWidth
                + "px; "
                + "height:"
                + iEffectiveHeight
                + "px; "
                + "visibility:hidden;");

        QComponent qcPlaceComponent = p.qc;

        if (qcPlaceComponent.isPropertyDefined(PROPERTY_FORCEWIDTH)
            && qcPlaceComponent.isPropertyDefined(PROPERTY_FORCEHEIGHT)) {
          qcPlaceComponent.setInteger(PROPERTY_FORCEWIDTH, iEffectiveWidth);
          qcPlaceComponent.setInteger(PROPERTY_FORCEHEIGHT, iEffectiveHeight);
        }

        if (p.sLabel != null) qc.addTextEquivalent(p.sLabel);
        qc.setParent(ePlace);
        qcPlaceComponent.produceOutput(qc, bInit, bPlain);
        qc.unsetParent();

        sJavascript += ",['" + sPlaceholderID + "'," + p.iActualX + "," + p.iActualY + "]";
      }
      sJavascript += "); });";

      if (apPlaces.length > 0) // No JS needed if there weren't any placeholders
      {
        Element eScript = XML.createChild(eEnsureSpaces, "script");
        eScript.setAttribute("type", "text/javascript");
        XML.createText(eScript, sJavascript);
      }
    }
  }
  @Override
  protected void initChildren(Element eThis) throws OmException {
    Node nPrevious = eThis.getPreviousSibling();
    if (nPrevious != null && nPrevious instanceof Text) {
      String sText = ((Text) nPrevious).getData();
      if (sText.length() > 0 && Character.isWhitespace(sText.charAt(sText.length() - 1)))
        bSpaceBefore = true;
    }
    Node nAfter = eThis.getNextSibling();
    if (nAfter != null && nAfter instanceof Text) {
      String sText = ((Text) nAfter).getData();
      if (sText.length() > 0 && Character.isWhitespace(sText.charAt(0))) bSpaceAfter = true;
    }

    List<Place> lPlaces = new LinkedList<Place>();
    int iPlace = 0;
    StringBuffer sbText = new StringBuffer();
    for (Node n = eThis.getFirstChild(); n != null; n = n.getNextSibling()) {
      if (n instanceof Element) {
        Element eplace = (Element) n;
        if (!eplace.getTagName().equals("eplace"))
          throw new OmFormatException("<equation> may only contain text and <eplace> tags");
        Element[] aeChildren = XML.getChildren(eplace);
        QComponent qcChild;
        boolean bImplicit = false;
        if (aeChildren.length != 1) // Treats more than one child as inside <t>
        {
          qcChild = getQDocument().build(this, eplace, "t");
          bImplicit = true;
        } else // Treats single child as specific component (auto-sizing works)
        qcChild = getQDocument().build(this, aeChildren[0], null);
        addChild(qcChild); // Must be stored in standard child array so it
        // can be found etc.

        // See if width/height is specified
        int iWidth, iHeight;
        if (eplace.hasAttribute("width") && eplace.hasAttribute("height")) {
          try {
            iWidth = Integer.parseInt(eplace.getAttribute("width"));
            iHeight = Integer.parseInt(eplace.getAttribute("height"));
          } catch (NumberFormatException nfe) {
            throw new OmFormatException("<equation> <eplace>: width= and height= must be integers");
          }
        } else {
          Dimension d = qcChild.getApproximatePixelSize();
          if (d == null)
            throw new OmFormatException(
                "<equation> <eplace>: Except for components that support automatic "
                    + "size estimation and fixing, <eplace> must include width= and height=");
          iWidth = d.width;
          iHeight = d.height;
        }

        Place p = new Place();
        p.sID = "p" + (iPlace++);
        p.qc = qcChild;
        p.iWidth = iWidth;
        p.iHeight = iHeight;
        p.bImplicit = bImplicit;
        if (!eplace.hasAttribute("label"))
          throw new OmFormatException("<equation> <eplace>: Must include label=");
        if (eplace.hasAttribute("label")) p.sLabel = eplace.getAttribute("label");
        else p.sLabel = null;
        if (eplace.hasAttribute("for")) p.sLabelFor = eplace.getAttribute("for");
        else if (qcChild instanceof Labelable) p.sLabelFor = qcChild.getID();
        lPlaces.add(p);

        // Add in the equation format text representing the placeholder
        sbText.append("\\placeholder{" + p.sID + "}{" + p.iWidth + "," + p.iHeight + "}");
      } else if (n instanceof Text) {
        sbText.append(n.getNodeValue());
      }
    }
    sEquation = sbText.toString();
    apPlaces = lPlaces.toArray(new Place[0]);
  }