Monday, January 30, 2012

JavaFX 2 Presents the Quadratic Formula

I recently needed to check some homework answers related to use of the quadratic formula. After realizing that it was getting tedious to do these by hand, I thought of using my calculator to solve them. However, I realized that I could write a simple application using JavaFX to calculate the results and that approach seemed more interesting than using the calculator. This post demonstrates that simple application and provides the relatively straightforward code required to make it happen.

The next couple of screen snapshots demonstrate the simple application in example. They include the initial appearance of the application followed by a couple images showing different values calculated by the application.

The last screen snapshot shows the output when one of the coefficients provided does not allow the quadratic formula to be applied. To provide this application slightly broader application than solely quadratic equation solving, I have added code to detect when the 'a' coefficient is zero and the 'b' coefficient is non-zero. This is a linear equation and the simple application solves it as well as the quadratic equations provided. The next screen snapshot demonstrates the output when coefficients for a linear equation are provided.

The first code listing has nothing to do with the JavaFX presentation layer, but is instead a simple call that provides the basic back-end quadratic formula calculations. I include it here because it is use by the JavaFX example application. It is somewhat interesting in its own right because there are so many ways to calculate a square root in Java. The best-known approach is to use Math.sqrt(double), but the choices are often more varied when using BigDecimal instead of double. Because the purpose of my post is to focus on the JavaFX aspect of this and because my chosen approach is accurate enough for its purposes, I've taken the "easy way" out here and simply use Math.sqrt(double).

SimplisticQuadraticFormula.java
package dustin.examples;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;

/**
 * Class encapsulating quadratic formula implementation.
 * 
 * @author Dustin
 */
public class SimplisticQuadraticFormula
{
   /** Default precision used for specifying scale. */
   private static final int DEFAULT_PRECISION = MathContext.DECIMAL64.getPrecision();

   /** Convenient representation of zero as BigDecimal. */
   private static final BigDecimal ZERO = new BigDecimal("0");

   /** Convenient representation of two as BigDecimal. */
   private static final BigDecimal TWO = new BigDecimal("2");

   /** Convenient representation of four as BigDecimal. */
   private static final BigDecimal FOUR = new BigDecimal("4");

   /**
    * Calculate intercepts with x-axis.
    * 
    * @param a Coefficient 'a' from a quadratic equation to be solved.
    * @param b Coefficient 'b' from a quadratic equation to be solved.
    * @param c Coefficient 'c' from a quadratic equation to be solved.
    * @return The x-intercepts or solutions to the quadratic equation (two values)
    *     or a single value if solution to a linear equation. Note that two
    *     solutions are always provided for quadratic equations even if they are
    *     the same.
    * @throws NumberFormatException Thrown when x-intercepts cannot be calculated.
    */
   public static List<BigDecimal> calculateXIntercepts(
      final BigDecimal a, final BigDecimal b, final BigDecimal c)
   {
      final List<BigDecimal> intercepts = new ArrayList<BigDecimal>();
      if (a.compareTo(ZERO) == 0 && b.compareTo(ZERO) == 0)
      {
         // neither quadratic nor linear
         throw new NumberFormatException("Must have coefficient for one of x terms.");       
      }
      else if (a.compareTo(ZERO) == 0)  // linear equation
      {
         intercepts.add(c.setScale(DEFAULT_PRECISION).negate().divide(b, RoundingMode.HALF_UP));
      }
      else
      {
         final BigDecimal intercept1 =
            calculateNumeratorWithAddition(a, b, c)
               .divide(calculateDenominator(a), RoundingMode.HALF_UP);
         intercepts.add(intercept1);
         final BigDecimal intercept2 =
            calculateNumeratorWithSubtraction(a, b, c)
               .divide(calculateDenominator(a), RoundingMode.HALF_DOWN);
         intercepts.add(intercept2);
      }
      return intercepts;
   }

   /**
    * Calculate axis of symmetry, if applicable.
    * 
    * @param a Coefficient 'a' from a quadratic equation to be solved.
    * @param b Coefficient 'b' from a quadratic equation to be solved.
    * @return The "x" axis of symmetry.
    * @throws NumberFormatException Thrown if the provided 'a' coefficient is
    *    zero because cannot divide by zero.
    */
   public static BigDecimal calculateAxisOfSymmetry(final BigDecimal a, final BigDecimal b)
   {
      if (a.compareTo(ZERO) == 0)
      {
         throw new NumberFormatException(
            "Cannot calculate axis of symmetry based on x-intercepts when a is zero.");
      }
      return b.setScale(DEFAULT_PRECISION).negate().divide(a.multiply(TWO), RoundingMode.HALF_UP);
   }

   /**
    * Calculate numerator of quadratic formula where the terms are added.
    * 
    * @param a Coefficient 'a' from a quadratic equation to be solved.
    * @param b Coefficient 'b' from a quadratic equation to be solved.
    * @param c Coefficient 'c' from a quadratic equation to be solved.
    * @return Value of numerator in quadratic formula where terms are added.
    * @throws NumberFormatException Thrown if no real solution is available.
    */
   private static BigDecimal calculateNumeratorWithAddition(
      final BigDecimal a, final BigDecimal b, final BigDecimal c)
   {
      return b.negate().add(calculateSquareRootPortion(a, b, c));
   }

   /**
    * Calculate numerator of quadratic formula where the terms are subtracted.
    * 
    * @param a Coefficient 'a' from a quadratic equation to be solved.
    * @param b Coefficient 'b' from a quadratic equation to be solved.
    * @param c Coefficient 'c' from a quadratic equation to be solved.
    * @return Value of numerator in quadratic formula where terms are subtracted.
    * @throws NumberFormatException Thrown if no real solution is available.
    */
   private static BigDecimal calculateNumeratorWithSubtraction(
      final BigDecimal a, final BigDecimal b, final BigDecimal c)
   {
      return b.negate().subtract(calculateSquareRootPortion(a, b, c));
   }

   /**
    * Calculate denominator of quadratic formula.
    * 
    * @param a Coefficient of 'a' from a quadratic equation to be solved.
    * @return Value of denominator in quadratic formula.
    * @throws NumberFormatException Thrown in 0 is provided for coefficient 'a'
    *    because denominator cannot be zero.
    */
   private static BigDecimal calculateDenominator(final BigDecimal a)
   {
      if (a.compareTo(ZERO) == 0)
      {
         throw new NumberFormatException("Denominator cannot be zero.");
      }
      return a.multiply(TWO);
   }

   /**
    * Calculates value of square root portion of quadratic formula.
    * 
    * @param a Coefficient 'a' from a quadratic equation to be solved.
    * @param b Coefficient 'b' from a quadratic equation to be solved.
    * @param c Coefficient 'c' from a quadratic equation to be solved.
    * @return The square root portion of the quadratic formula applied with
    *    the three provided co-efficients.
    * @throws NumberFormatException Thrown if there is no solution (no
    *    intersection of the x-axis) or if a number is encountered that cannot
    *    be handled with BigDecmal return type.
    */
   private static BigDecimal calculateSquareRootPortion(
      final BigDecimal a, final BigDecimal b, final BigDecimal c)
   {
      BigDecimal sqrt;
      final BigDecimal subtrahend = a.multiply(c).multiply(FOUR);
      final BigDecimal insideSqrt = b.pow(2).subtract(subtrahend);
      if (insideSqrt.compareTo(ZERO) < 0)
      {
         throw new NumberFormatException("Cannot be solved: no x-intercepts.");
      }
      else
      {
         final double value = insideSqrt.doubleValue();
         final double sqrtDouble = Math.sqrt(value);
         sqrt = new BigDecimal(sqrtDouble);  // may throw NumberFormatException
      }
      return sqrt;
   }
}

With the calculations portion in place, it is time to move to the focus of this post (the presentation via JavaFX 2). The following code listing provides the JavaFX 2 class (written in pure Java) used for the application. Note that much of this presentation layer code could have been written using FXML.

QuadraticCalculator.java
package dustin.examples;

import java.math.BigDecimal;
import java.util.List;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFieldBuilder;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

/**
 * JavaFX-based application for solving quadratic equations.
 * 
 * @author Dustin
 */
public class QuadraticCalculator extends Application
{
   /** Coefficient A used in quadratic formula. */
   private TextField coefficientA =
      TextFieldBuilder.create().promptText("Enter Coefficient A").build();

   /** Coeffecient B used in quadratic forumal. */
   private TextField coefficientB =
      TextFieldBuilder.create().promptText("Enter Coefficient B").build();

   /** Coeffecient C (constant) used in quadratic formula. */
   private TextField coefficientC =
      TextFieldBuilder.create().promptText("Enter Coefficient C").build();

   /** First x-intercept. */
   private TextField xIntercept1 =
      TextFieldBuilder.create().disable(true).editable(false).build();

   /** Second x-intercept. */
   private TextField xIntercept2 =
      TextFieldBuilder.create().disable(true).editable(false).build();

   /** Axis of symmetry. */
   private TextField symmetryAxis =
      TextFieldBuilder.create().disable(true).editable(false).build();

   /**
    * Extract Image with provided name.
    * 
    * @param imageName Name of image to be provided.
    * @return Loaded image.
    */
   private Image getImage(final String imageName)
   {
      final String jarFileUrl =
         this.getClass().getProtectionDomain().getCodeSource().getLocation().toString();
      final String url = "jar:" + jarFileUrl + "!/" + imageName;
      System.out.println(url);
      return new Image(url, true);
   }

   /**
    * Provide a read-only horizontal box with quadratic equation and quadratic
    * formula and with a button that can be clicked to calculate solution to
    * quadration equation with provided coefficients.
    * 
    * @return Horizontal box with quadratic equation and quadratic formula.
    */
   private HBox buildEquationsBox()
   {
      final HBox equationsBox = new HBox();
      equationsBox.setAlignment(Pos.CENTER);
      equationsBox.setSpacing(50);
      final Image quadraticEquation = getImage("quadraticEquation-transparent.png");
      final ImageView equationView = new ImageView(quadraticEquation);
      equationsBox.getChildren().add(equationView);
      final Image quadraticFormula = getImage("quadraticFormula-transparent.png");
      final ImageView formulaView = new ImageView(quadraticFormula);
      equationsBox.getChildren().add(formulaView);
      final Button calculateButton = new Button("Calculate");
      calculateButton.setOnAction(
         new EventHandler<ActionEvent>()
         {
            public void handle(ActionEvent t)
            {
               final BigDecimal a = extractBigDecimal(coefficientA.getText());
               final BigDecimal b = extractBigDecimal(coefficientB.getText());
               final BigDecimal c = extractBigDecimal(coefficientC.getText());
               try
               {
                  final List<BigDecimal> intercepts =
                     SimplisticQuadraticFormula.calculateXIntercepts(a, b, c);
                  xIntercept1.setText(intercepts.get(0).toPlainString());
                  xIntercept1.setDisable(false);
                  if (intercepts.size() > 1)
                  {
                     xIntercept2.setText(intercepts.get(1).toEngineeringString());
                     xIntercept2.setDisable(false);
                  }
                  else
                  {
                     xIntercept2.setText("-");
                     xIntercept2.setDisable(true);
                  }
                  if (a.compareTo(new BigDecimal("0")) != 0)
                  {
                     final BigDecimal axis =
                        SimplisticQuadraticFormula.calculateAxisOfSymmetry(a, b);
                     symmetryAxis.setText(axis.toPlainString());
                     symmetryAxis.setDisable(false);
                  }
                  else
                  {
                     symmetryAxis.setText("-");
                     symmetryAxis.setDisable(true);
                  }
               }
               catch (NumberFormatException nfe)
               {
                  xIntercept1.setText("-");
                  xIntercept1.setDisable(true);
                  xIntercept2.setText("-");
                  xIntercept2.setDisable(true);
                  symmetryAxis.setText("-");
                  symmetryAxis.setDisable(true);
               }
            }
         });
      equationsBox.getChildren().add(calculateButton);
      return equationsBox;
   }

   /**
    * Converts provided String to BigDecimal.
    * 
    * @param possibleNumber String to be converted to an instance of BigDecimal.
    * @return The BigDecimal corresponding to the provided String or Double.NaN
    *     if the conversion cannot be performed.
    */
   private BigDecimal extractBigDecimal(final String possibleNumber)
   {
      BigDecimal extractedNumber;
      try
      {
         extractedNumber = new BigDecimal(possibleNumber);
      }
      catch (NumberFormatException nfe)
      {
         extractedNumber = null;
      }
      return extractedNumber;
   }

   /**
    * Provide horizontal box with labels of coefficients and fields to enter
    * coefficient values.
    * 
    * @return Horizontal box for entering coefficients.
    */
   private HBox buildEntryBox()
   {
      final HBox entryBox = new HBox();
      entryBox.setSpacing(10);
      final Label aCoeff = new Label("a = ");
      entryBox.getChildren().add(aCoeff);
      entryBox.getChildren().add(this.coefficientA);
      final Label bCoeff = new Label("b = ");
      entryBox.getChildren().add(bCoeff);
      entryBox.getChildren().add(this.coefficientB);
      final Label cCoeff = new Label("c = ");
      entryBox.getChildren().add(cCoeff);
      entryBox.getChildren().add(this.coefficientC);
      return entryBox;
   }

   /**
    * Construct the output box with solutions based on quadratic formula.
    * 
    * @return Output box with solutions of applying quadratic formula given
    *    provided input coefficients.
    */
   private HBox buildOutputBox()
   {
      final HBox outputBox = new HBox();
      outputBox.setSpacing(10);
      final Label x1 = new Label("x1 = ");
      outputBox.getChildren().add(x1);
      outputBox.getChildren().add(this.xIntercept1);
      final Label x2 = new Label("x2 = ");
      outputBox.getChildren().add(x2);
      outputBox.getChildren().add(this.xIntercept2);
      final Label axis = new Label("axis = ");
      outputBox.getChildren().add(axis);
      outputBox.getChildren().add(this.symmetryAxis);
      return outputBox;
   }

   /**
    * Build overall presentation of application.
    * 
    * @return Vertical box representing input and output of application.
    */
   private VBox buildOverallVerticalLayout()
   {
      final VBox vbox = new VBox();
      vbox.setSpacing(25);
      vbox.getChildren().add(buildEquationsBox());
      vbox.getChildren().add(buildEntryBox());
      vbox.getChildren().add(buildOutputBox());
      vbox.setAlignment(Pos.CENTER);
      return vbox;
   }

   /**
    * Start the JavaFX application for solving quadratic equations.
    * 
    * @param stage Primary stage.
    * @throws Exception JavaFX-related exception.
    */
   @Override
   public void start(final Stage stage) throws Exception
   {
      final Group groupRoot = new Group();
      groupRoot.getChildren().add(buildOverallVerticalLayout());
      final Scene scene = new Scene(groupRoot, 600, 150, Color.LIGHTGRAY);
      stage.setTitle("Quadratic Formula: JavaFX Style");
      stage.setScene(scene);
      stage.show();
   }

   /**
    * Main function for running the JavaFX-based quadratic equation solver.
    * 
    * @param arguments 
    */
   public static void main(final String[] arguments)
   {
      Application.launch(arguments);
   }
}

The JavaFX 2 code above loads two images with transparent backgrounds to display the standard form of a quadratic equation and to display the quadratic formula. In a previous post, I loaded these images from an external URL, but I loaded them from the application's JAR in this example. These images are shown next independent of the application.

quadraticEquation-transparent.png
quadraticFormula-transparent.png

There are several ways this simplistic application could be improved. One improvement would be to make sizes adjustable and bound to one another so the window could be dragged larger or smaller and still look good. Another improvement would be to provide some type of status message on the interface, especially for situations where solutions could not be calculated. A third improvement that would be to present a graph of the quadratic equation specified with the provided coefficients to visually indicate the x-intercepts and axis of symmetry.

JavaFX 2 makes it fairly straight-forward to write simple, user-friendly applications.

5 comments:

Nasser said...

That is nice. But I can do the same in Mathematica, and run it also as an applet in a browser, and the code is only 4 lines long :).

Manipulate[Solve[a x^2+b x +c==0],
{{a, 1, "a"}, -10, 10, .1},
{{b, 1, "b"}, -10, 10, .1},
{{c, 1, "c"}, -10, 10, .1}
]

--Nasser

Unknown said...

If desired, rather than using an image, you can also have JavaFX render the quadratic equation using MathML in an embedded WebView as is demonstrated here:
https://gist.github.com/1775799

Very nice series of articles you are putting together on JavaFX Dustin.

Dustin said...

Unknown,

Thanks for the compliment and thanks for sharing this great idea for presentation of the quadratic formula as an alternative to text images.

Dustin

Christian Ullenboom said...

Nice example of JavaFX. One could also reduce one constant because we have BigDecimal.ZERO already (and BigDecimal.ONE/TEN, but not for 2 and 4).

For similar tasks one could also take a look at http://docs.oracle.com/javase/6/docs/api/java/awt/geom/QuadCurve2D.html#solveQuadratic(double[], double[])

Dustin said...

Christian,

Thanks for the feedback. Those are excellent ideas. I had forgotten about the BigDecimal constants BigDecimal.ONE, BigDecimal.TEN, and BigDecimal.ZERO and the existence of QuadCurve2D.solveQuadratic(double[] eqn, double[] res) is new to me.

Thanks for pointing these out!

Dustin