Lab 6

Suppose we build an expression in the variables x and y from the following primitives:

For example, the expressions can all be built that way.

If we compute the value of such an expression at any point with -1 ≤ x, y ≤ 1, then the result will also be between -1 and 1. (How do we prove this?) For example, if we evaluate the above four example expressions for the x = 0.5 and y = -0.5 we obtain the values

Now consider an image of, for example, 800 by 800 pixels. Each pixel has an x- and y-coordinate. For the x-coordinate, xCoordinate, we have that 0 ≤ xCoordinate < 800. We want to distribute these x-coordinates evenly over the interval [-1, 1]. This can be done by mapping xCoordinate to 2 * xCoordinate / 800 - 1. For example, the x-coordinate 600 is mapped to 0.5. In the same way, we can map the y-coordinate, yCoordinate, to a point in the interval [-1, 1]. For example, the y-coordinate 200 is mapped to -0.5.

As we have just seen, we can assign to each pixel of an image a pair (x, y) with -1 ≤ x, y ≤ 1. Recall that the value of the above introduced expressions at such a point (x, y) is an element in the interval [-1, 1]. That is, such an expression assigns to each pixel an element in [-1, 1]. For example, the pixel (600, 200) is mapped to (0.5, -0.5) and the expression average(y, (x * cos(pi * y))) subsequently maps that to -0.25.

A black and white image assigns to each pixel a grey scale value in the interval [0, 255]. As we have seen, we can use the above expressions to map each pixel to an element in the interval [-1, 1]. Hence, by transforming the interval [-1, 1] to the interval [0, 255], we obtain a grey scale value for each pixel. For a given value in the interval [-1, 1], we have that 255 * (1 + value) / 2.0 is in the interval [0, 255]. For example, for the pixel (600, 200), the expression average(y, (x * cos(pi * y))) gives the grey scale 255 * (1 + -0.25) / 2.0 = 96. So every expression defines a gray scale value for each pixel and, hence, defines a black and white image. The image defined by the expression average(y, (x * cos(pi * y))) is given below.

A color image assigns to each pixel an amount of red, an amount of green and an amount of blue. All three values are in the interval [0, 255]. Hence, given three expressions, we obtain the amount of red, green and blue for each pixel, that is, we obtain a color image. For example, if we take the expression average(y, (x * cos(pi * y))) for red, the expression (x * cos(pi * y)) for green and the expression (x * y) for blue we obtain the following image.

The app Generator generates an image from three randomly created expressions. The utility class Expressions creates random expressions. As we can see, the utility Expressions uses numerous classes from the package lab.art. Its API can be found here. This lab consists of implementing some of the classes of lab.art.

Aggregation

We introduce the class Expression to represent expressions in general, and the classes Product, Average, Sine, Cosine, VariableX and VariableY to represent expressions of the form

respectively. Note that a product has two subexpressions. For example, the expression (x * cos(pi * y)) has x as its first subexpression and cos(pi * y) as its second subexpression. Similarly, an average expression has also two subexpressions. Sine and cosine expressions only have a single subexpression. For example, the expression cos(pi * y) has the subexpression y. The variables x and y do not have any subexpressions.

Inheritance

Since both product and average expressions have two subexpressions, we introduce the class BinaryExpression. Both the Product and Average class extend the BinaryExpression class, since a product expression is-a binary expression and an average expression is-a binary expression. A binary expression has two subexpressions, which is captured by aggregation.

Sine and cosine expressions have only one subexpression. For that reason, we introduce the class UnaryExpression. The Sine and Cosine class both extend the UnaryExpression class, since a sine expression is-a unary expression and a cosine expression is-a unary expression as well. A unary expression has just one subexpression, which is captured by aggregation.

Abstract classes

Since the classes BinaryExpression and UnaryExpression have only been introduced to prevent code duplication, we want to prevent clients from instantiating these classes. Therefore, both classes are abstract.

The class Expression plays a central role. For example, a BinaryExpression is-an Expression and has two Expressions. Since the class Expression "combines" the abstract classes BinaryExpression and UnaryExpression, it is abstract itself as well.

Consider the (x * cos(pi * y)) expression. This expression is represented by a Product object, which is-an Expression and has two Expressions. To evaluate this expression at a point, say (0.5, -0.5), we have to evaluate the subexpressions x and cos(pi * y) at that point as well. Since these subexpressions are represented by Expression objects, the Expression class needs to contain the evaluate method. Note though that we can only provide a concrete implementation of the evaluate method in the classes Product, Average, Sine, Cosine, VariableX and VariableY. Therefore, the evaluate method of the Expression class is abstract.

Required Tasks

  1. Import the jar file lab6.jar into eclipse. This jar contains the classes Cosine, CosineOperator, Product, ProductOperator, Sine, SineOperator, UnaryExpression, UnaryOperator, and VariableY of the package lab.art.
  2. Create a package named lab.art.
  3. Add the class Expression to the package lab.art. Its source code can be found here. Its API can be found here.
  4. Implement the class VariableX. Its API can be found here. Include javadoc.
    1. Implement the constructor. Don't forget to delegate to the superclass Expression.
    2. Implement the evaluate method.
    3. Implement the toString method.
    Test the class using the JUnit test VariableXTester. Its source code can be found here.
  5. Add the class BinaryOperator to the package lab.art. Its source code can be found here. Its API can be found here.
  6. Implement the class AverageOperator. Its API can be found here. Include javadoc.
    1. Implement the constructor. Don't forget to delegate to the superclass BinaryOperator.
    2. Implement the operation method.
    Test the class using the JUnit test AverageOperatorTester. Its source code can be found here.
  7. Implement the class BinaryExpression. Its API can be found here. Include javadoc.
    1. Introduce attributes and their private accessors and mutators.
    2. Implement the constructor. Don't forget to delegate to the superclass Expression.
    3. Implement the equals method.
    4. Implement the hashCode method.
    5. The method toString takes an object of type String as its argument. According to the precondition, the implementer may assume that this string contains %s twice. For example, the string "average(%s, %s)" satisfies the precondition. Implement the toString method as follows.
      public String toString(String pattern)
      {
         String first = this.??????????.toString();
         String second = this.??????????.toString();
         return String.format(pattern, first, second);
      }
      
      In the above method, first the string representation of the first subexpression is stored in first. Next, the string representation of the second subexpression is stored in second. Finally, first and second take the place of the first and second %s in pattern and the resulting string is returned. For example, if first is "x" and second is "y" and pattern is "average(%s, %s)" then the result is "average(x, y)".
    6. The method evaluate takes two values of type double and an object of type BinaryOperator as its arguments. The BinaryOperator class contains a single method, called operation, that takes two doubles as its argument and returns a double. The method that takes doubles a and b as its arguments and returns (a + b) / 2 is an example of such a method. Implement the evaluate method as follows.
      public double evaluate(double x, double y, BinaryOperator operator)
      {
         double first = this.??????????.evaluate(x, y);
         double second = this.??????????.evaluate(x, y);
         return operator.operation(first, second);
      }
      
      In the above method, the first subexpression is evaluated at the given x- and y-coordinate. Next, the second subexpression is evaluated at the given x- and y-coordinate. Finally, the operation of the operator is applied to first and second and the result is returned.
    Test the class using the JUnit test BinaryExpressionTester. Its source code can be found here.
  8. Implement the class Average. Its API can be found here. Include javadoc.
    1. Implement the constructor. Don't forget to delegate to the superclass BinaryExpression.
    2. Implement the toString method. Delegate to the toString method of the superclass BinaryExpression.
    3. Implement the evaluate method as follows.
      public double evaluate(double x, double y)
      {
         BinaryOperator average = ??????????;
         return ??????????;
      }
      
      Delegate to the evaluate method of the superclass BinaryExpression.
    Test the class using the JUnit test AverageTester. Its source code can be found here.
  9. Submit your classes before Tuesday March 17, 4pm. To submit, log into a computer of the Prism lab (clips showing how to log in from home and transfer files can be found here).

Optional Tasks

  1. Submit your favourite generated random art by opening a terminal and running the following command.
    submit 1030 art art1.jpg
    
    You may submit multiple (differently named) files.
  2. Add to the class Expressions the following method.
    /**
     * Returns the expression represented by the given string.
     * 
     * @param expression string representation of an expression.
     * @return the expression represented by the given string.
     * @throws Exception if the given string does not represent an expression.
     */
    public static Expression fromString(String expression) throws Exception
    
  3. Test the method using the JUnit test FromStringTester. Its source code can be found here. If the JUnit test produces as out "Run tester again", do so.

This assignment is based on a nifty assignment designed by Christopher A Stone.