com.sciencegadgets.client.algebra.EquationHTML.java Source code

Java tutorial

Introduction

Here is the source code for com.sciencegadgets.client.algebra.EquationHTML.java

Source

/*******************************************************************************
 *     This file is part of ScienceGadgets, a collection of educational tools
 *     Copyright (C) 2012-2015 by John Gralyan
 *
 *     ScienceGadgets is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Affero General Public License as
 *     published by the Free Software Foundation, either version 3 of
 *     the License, or (at your option) any later version.
 *
 *     ScienceGadgets 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 Affero General Public License for more details.
 *
 *     You should have received a copy of the GNU Affero General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *     
 *     Contact us at info@sciencegadgets.org
 *******************************************************************************/
package com.sciencegadgets.client.algebra;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map.Entry;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.Style.Visibility;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.HTML;
import com.sciencegadgets.client.JSNICalls;
import com.sciencegadgets.client.algebra.EquationTree.EquationNode;
import com.sciencegadgets.client.ui.CSS;
import com.sciencegadgets.shared.MathAttribute;
import com.sciencegadgets.shared.TypeSGET;
import com.sciencegadgets.shared.TypeSGET.Operator;
import com.sciencegadgets.shared.dimensions.UnitAttribute;
import com.sciencegadgets.shared.dimensions.UnitHTML;

public class EquationHTML extends HTML {

    private static final String FENCED = CSS.FENCED;

    EquationTree mTree;
    public boolean autoFillParent = true;
    private boolean hasSmallUnits = true;
    private boolean hasSubscripts = true;
    private boolean isStacked = false;
    public boolean pilot = false;
    private Element left = null;
    private Element right = null;
    private final HashMap<Element, EquationNode> displayMap = new HashMap<Element, EquationNode>();

    public EquationHTML(EquationTree mTree) {
        this(mTree, true, true, false);
    }

    public EquationHTML(EquationTree mTree, boolean isStacked) {
        this(mTree, true, true, isStacked);
    }

    public EquationHTML(EquationTree mTree, boolean hasSmallUnits, boolean hasSubscripts, boolean isStacked) {
        this.mTree = mTree;
        this.setStyleName(CSS.EQUATION);
        this.hasSmallUnits = hasSmallUnits;
        this.hasSubscripts = hasSubscripts;
        this.isStacked = isStacked;

        left = makeHTMLNode(mTree.getLeftSide(), this.getElement());
        makeHTMLNode(mTree.getEquals(), this.getElement());
        right = makeHTMLNode(mTree.getRightSide(), this.getElement());

    }

    private EquationHTML(String html) {
        super(html);
        this.setStyleName(CSS.EQUATION);
    }

    public EquationHTML(Element element) {
        super(JSNICalls.elementToString(element));
        this.setStyleName(CSS.EXPRESSION);
    }

    public Element getLeft() {
        return left;
    }

    public Element getRight() {
        return right;
    }

    @Override
    protected void onLoad() {
        super.onLoad();
        if (pilot) {
            matchHeightsAndAlign(this.getElement());
        }
        if (autoFillParent) {
            resizeEquation();
        }
    }

    public EquationHTML clone() {
        return new EquationHTML(this.getHTML());
    }

    /**
     * Recursive creation of the display tree. Makes a display node equivalent
     * of
     * 
     * @param mlNode
     * <br/>
     *            and adds it to<br/>
     * @param displayParentEl
     */
    private Element makeHTMLNode(EquationNode mNode, Element displayParentEl) {
        EquationNode mParent = mNode.getParent();

        String id = mNode.getId();
        TypeSGET type = mNode.getType();
        TypeSGET parentType = mParent.getType();
        boolean isSecondChild = false;

        // make new display node with appropriate properties
        Element container = DOM.createDiv();
        Element nodeHtml = container;
        container.setId(id);

        String functionName = null;

        // Add class names based on parents
        switch (parentType) {
        case Fraction:
        case Exponential:
            boolean isFirstChild = mNode.equals(mParent.getFirstChild());
            isSecondChild = mNode.equals(mParent.getChildAt(1));
            if (isFirstChild) {
                container.addClassName(parentType.asChild(true));
            } else if (isSecondChild) {
                container.addClassName(parentType.asChild(false));
            } else {
                JSNICalls.error("Wrong children for a " + parentType + " " + mNode.getParent());
            }
            break;
        case Sum:
            if (TypeSGET.Operation.equals(type)) {
                String txt = mNode.getSymbol();
                if (TypeSGET.Operator.PLUS.getSign().equals(txt)) {
                    container.addClassName(CSS.PLUS);
                } else if (TypeSGET.Operator.MINUS.getSign().equals(txt)) {
                    container.addClassName(CSS.MINUS);
                } else {
                    JSNICalls.error("Operator in sum is neither + nor - in:\n" + mNode.toString());
                }
            }
        case Log:
        case Trig:
        case Equation:
        case Term:
            container.addClassName(parentType.asChild());
            break;
        }

        // Add line to fraction
        if (TypeSGET.Fraction.equals(parentType) && isSecondChild) {
            Element horizontal = DOM.createDiv();
            horizontal.addClassName(CSS.FRACTION_LINE);
            displayParentEl.appendChild(horizontal);
        }

        // Add parentheses (fence) to certain elements
        switch (parentType) {
        case Term:// Sums in Terms
            if (TypeSGET.Sum.equals(type)) {
                nodeHtml = fence(nodeHtml, container);
            }
            break;
        case Exponential:// All but Variables and unitless Numbers
            if (!TypeSGET.Variable.equals(type)//
                    && !(TypeSGET.Number.equals(type) && "".equals(mNode.getAttribute(MathAttribute.Unit)))) {
                nodeHtml = fence(nodeHtml, container);
            }
            break;
        }

        // Add function css class names
        switch (type) {
        case Log:
            functionName = "log";
            Element base = DOM.createDiv();
            base.addClassName(TypeSGET.Log.asLogBase());
            base.setInnerText(mNode.getAttribute(MathAttribute.LogBase));
            nodeHtml.insertFirst(base);

            // fall through
        case Trig:
            if (functionName == null) {
                functionName = mNode.getAttribute(MathAttribute.Function);
            }
            Element funcNameDisplay = DOM.createDiv();
            funcNameDisplay.setInnerText(functionName);
            funcNameDisplay.addClassName(CSS.FUNCTION_NAME);
            nodeHtml.insertFirst(funcNameDisplay);
            break;
        case Fraction:
            // if (!(TypeML.Exponential.equals(parentType) && mNode.getIndex()
            // == 1)) {
            // container.getStyle().setVerticalAlign(VerticalAlign.MIDDLE);
            // }
        }

        // Addition to tree
        displayParentEl.appendChild(container);

        Element unit = null;
        switch (type) {
        case Sum:
        case Term:
        case Fraction:
        case Exponential:
        case Log:
        case Trig:
            // Recursive creation
            for (EquationNode child : mNode.getChildren()) {
                makeHTMLNode(child, nodeHtml);
            }
            break;
        case Number:
            UnitAttribute unitName = mNode.getUnitAttribute();
            if (!"".equals(unitName.toString())) {
                unit = UnitHTML.create(unitName, id, hasSmallUnits);
            }
            // falls through
        case Variable:
            String text = mNode.getXMLNode().getInnerText();
            if (hasSubscripts && text.length() > 1) {
                if (text.startsWith(Operator.MINUS.getSign())) {
                    nodeHtml = fence(nodeHtml, container);
                }
                try {
                    new BigDecimal(text);
                    nodeHtml.setInnerText(text);
                } catch (NumberFormatException e) {
                    // non-numbers, characters after the first are subscripts
                    // note - constants are number nodes with character text

                    // The following shouldn't count as the large character
                    // before subscripts
                    boolean startsMinus = text.startsWith(Operator.MINUS.getSign());
                    boolean startsDelta = text.startsWith("\u0394");
                    boolean startsSqrt = text.startsWith("\u221A");

                    int substringEnd = startsMinus || startsDelta || startsSqrt ? 2 : 1;

                    nodeHtml.setInnerText(text.substring(0, substringEnd));
                    Element subscript = DOM.createDiv();
                    subscript.addClassName(CSS.SUBSCRIPT);
                    subscript.setInnerText(text.substring(substringEnd));
                    nodeHtml.appendChild(subscript);
                }
            } else {
                nodeHtml.setInnerText(text);
            }
            break;
        case Operation:
            String txt = mNode.getSymbol();

            if (TypeSGET.Operator.EQUALS.getSign().equals(txt)) {
                if (isStacked) {
                    nodeHtml.getStyle().setDisplay(Display.BLOCK);
                    break;
                }
            }

            nodeHtml.setInnerText(txt);
            break;
        }

        if (unit != null) {
            nodeHtml.appendChild(unit);
        }

        container.addClassName(type.getCSSClassName());
        displayMap.put(nodeHtml, mNode);
        return nodeHtml;
    }

    private Element fence(Element nodeHtml, Element container) {
        String containerClass = container.getClassName();
        if (!(containerClass.contains(TypeSGET.Trig.asChild())
                || containerClass.contains(TypeSGET.Log.asChild()))) {

            nodeHtml = container.appendChild(DOM.createDiv());
            nodeHtml.addClassName(FENCED);
        }
        return nodeHtml;
    }

    /**
     * Resizes the equation to fill the panel
     */
    public void resizeEquation() {

        this.getElement().getStyle().clearFontSize();

        double widthRatio = (double) this.getParent().getOffsetWidth() / getOffsetWidth();
        double heightRatio = (double) this.getParent().getOffsetHeight() / getOffsetHeight();

        double smallerRatio = (widthRatio > heightRatio) ? heightRatio : widthRatio;
        // *90 for looser fit, *100 for percent
        // double fontPercent = smallerRatio * 90;

        double fontPercent = smallerRatio * 100;

        this.getElement().getStyle().setFontSize((fontPercent), Unit.PCT);
    }

    /**
     * Matches the heights of all the children of an {@link TypeSGET#Equation},
     * {@link TypeSGET#Term} or {@link TypeSGET#Sum} by:<br/>
     * 1.Lifting centers to the tallest denominator using padding-bottom<br/>
     * 2.Matching tops to tallest height with padding-top<br/>
     * <b>Note:</b> All children of these nodes are initially aligned at their
     * baseline
     */
    private void matchHeightsAndAlign(Element curEl) {

        EquationNode curNode = displayMap.get(curEl);

        TypeSGET curType = null;
        if (curNode != null) {
            curType = curNode.getType();
        }
        if (curEl.getChildCount() > 0) {
            for (int i = 0; i < curEl.getChildCount(); i++) {
                if (Node.ELEMENT_NODE == curEl.getChild(i).getNodeType()) {
                    matchHeightsAndAlign((Element) curEl.getChild(i));
                }
            }
        }

        if (!(TypeSGET.Equation.equals(curType) || TypeSGET.Term.equals(curType) || TypeSGET.Sum.equals(curType)
                || TypeSGET.Exponential.equals(curType))) {
            return;
        }

        LinkedList<Element> childrenHorizontal = new LinkedList<Element>();
        LinkedList<Element> fractionChildrenHorizontal = new LinkedList<Element>();

        addChildrenIfInline(curEl, childrenHorizontal, curType);

        double pxPerEm = getPxPerEm(curEl);

        // Fractions must be centered, find the tallest numerator or denominator
        // to match using padding to allow centering
        int tallestFracChild = 0;
        for (Element child : childrenHorizontal) {
            // Find the tallest denominator to match centers
            if (child.getClassName().contains(TypeSGET.Fraction.getCSSClassName())) {
                if (child.getFirstChildElement().getClassName().contains("fenced")) {
                    child = child.getFirstChildElement();
                }
                fractionChildrenHorizontal.add(child);
                for (int i = 0; i < 2; i++) {
                    int fracChildHeight = ((Element) child.getChild(i)).getClientHeight();
                    if (fracChildHeight > tallestFracChild) {
                        tallestFracChild = fracChildHeight;
                    }
                }
            }
        }
        // Match fraction horizontal lines inline
        for (Element fractionChild : fractionChildrenHorizontal) {

            int numHeight = ((Element) fractionChild.getChild(0)).getClientHeight();
            int denHeight = ((Element) fractionChild.getChild(2)).getClientHeight();

            Style s = fractionChild.getStyle();
            s.setPaddingTop((tallestFracChild - numHeight) / pxPerEm, Unit.EM);
            s.setPaddingBottom((tallestFracChild - denHeight) / pxPerEm, Unit.EM);
            // s.setBottom((tallestFracChild - denHeight) / pxPerEm,
            // Unit.EM);
        }

        // Find highest top and lowest bottom to match heights
        // int lowestBottom = 0;
        // int highestTop = 999999999;
        // for (Element child : childrenHorizontal) {
        // int childTop = child.getAbsoluteTop();
        // if (childTop < highestTop) {
        // highestTop = childTop;
        // }
        // int childBottom = child.getAbsoluteBottom();
        // if (childBottom > lowestBottom) {
        // lowestBottom = childBottom;
        // }
        // }
        //
        // highestTop = curEl.getAbsoluteTop();
        // lowestBottom = curEl.getAbsoluteBottom();

        // Lift exponents of fraction bases to top
        // if (TypeSGET.Exponential.equals(curType)) {
        // Element base = ((Element) curEl.getChild(0));
        // Element exp = ((Element) curEl.getChild(1));
        // int lift = (exp.getOffsetTop() - base.getOffsetTop());
        // exp.getStyle().setBottom(lift / pxPerEm, Unit.EM);

        // Align inline siblings flush using padding at highest and lowest
        // } else {
        // for (Element child : childrenHorizontal) {
        // Style s = child.getStyle();
        //
        // // Fractions with some padding don't need to be aligned
        // if (fractionChildrenHorizontal.contains(child)) {
        // if (!"0em".equals(s.getPaddingTop())
        // || !"0em".equals(s.getPaddingBottom())) {
        // continue;
        // }
        // }else
        // if(child.getClassName().contains(TypeML.Operation.toString())) {
        // continue;
        // }
        //
        // int childTopPad = child.getAbsoluteTop() - highestTop;
        // int childBottomPad = lowestBottom - child.getAbsoluteBottom();
        //
        // s.setPaddingTop(childTopPad / pxPerEm, Unit.EM);
        // s.setPaddingBottom(childBottomPad / pxPerEm, Unit.EM);
        // }
        // }

    }

    public static double getPxPerEm(Element element) {
        Element dummy = DOM.createDiv();
        element.appendChild(dummy);
        dummy.getStyle().setHeight(1000, Unit.EM);
        double pxHeight = dummy.getOffsetHeight();
        double pxPerEm = pxHeight / ((double) 1000);
        dummy.removeFromParent();
        return pxPerEm;
    }

    private void addChildrenIfInline(Element curEl, LinkedList<Element> childrenInline, TypeSGET curType) {
        switch (curType) {
        case Exponential:
            // Only the base of an exponent is considered inline
            childrenInline.add((Element) curEl.getChild(0));
            break;
        case Fraction:
            break;
        case Equation:
        case Sum:
        case Term:
        case Trig:
        case Log:
            NodeList<Node> children = curEl.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                childrenInline.add((Element) children.getItem(i));
            }
            break;
        }
    }

    public Element getHTMLElement(EquationNode eNode) {
        for (Entry<Element, EquationNode> entry : displayMap.entrySet()) {
            if (eNode == entry.getValue()) {
                return entry.getKey();
            }
        }
        return null;
    }

    public EquationNode getEquationNode(Element element) {
        return displayMap.get(element);
    }
}