Main Page

Previous Next

Transforming the User Coordinate System

We said when we started learning how to draw on a component that the drawing operations are specified in a user-coordinate system, and the user coordinates are converted to a device coordinate system. The conversion of coordinates from user system to device system is taken care of by the methods in the graphics context object that we use to do the drawing, and they do this by applying a transformation to the user coordinates. The term 'transformation' refers to the computational operations that result in the conversion.

By default, the origin, the (0, 0) point, in the user coordinate system corresponds to the (0, 0) point in the device coordinates system. The axes are also coincident too, with positive x heading from left to right, and positive y from top to bottom. However you can move the origin of the user coordinate system relative to its default position. Such a move is called a translation.

Click To expand

A fixed value, deltaX say, is added to each x coordinate, and another value, deltaY say, is added to every y coordinate and the effect of this is to move the origin of the user coordinate system relative to the device coordinate system: everything will be shifted to the right and down compared to where it would have been without the translation. Of course, the deltaX and deltaY values can be negative, in which case it would shift things to the left and up.

A translation is one kind of affine transformation. Affine is a funny word. Some say it goes back to Laurel and Hardy where Ollie says, "This is affine mess you've got us into", but I don't subscribe to that. An affine transformation is actually a linear transformation that leaves straight lines still straight and parallel lines still parallel. As well as translations, there are other kinds of affine transformation that you can define:

  • Rotation – the user coordinates system is rotated through a given angle about its origin.

  • Scale – the x and y coordinates are each multiplied by a scaling factor, and the multipliers for x and y can be different. This enables you to enlarge or reduce something in size. If the scale factor for one coordinate axis is negative, then objects will be reflected in the other axis. Setting the scale factor for x coordinates to –1, for example, will make all positive coordinates negative and vice versa so everything is reflected in the y axis.

  • Shear – this is perhaps a less familiar operation. It adds a value to each x coordinate that depends on the y coordinate, and adds a value to each y coordinate that depends on the x coordinate. You supply two values to specify a shear, sX and sY say, and they change the coordinates in the following way:

    Each x coordinate becomes (x + sX * y)
    Each y coordinate becomes (y + sY * x)

    The effect of this can be visualized most easily if you first imagine a rectangle that is drawn normally. A shearing transform can squash it by tilting the sides – rather like when you flatten a carton – but keep opposite sides straight and parallel.

    Click To expand

The illustration shows:

  • A rotation of -(/4 radians, which is –45 degrees. Rotation angles are expressed in radians and a positive angle rotates everything from the positive x-axis towards the positive y-axis – therefore clockwise. The rotation in the illustration is negative and therefore counterclockwise.

  • A scaling transformation corresponding to an x scale of 2.5 and a y scale of 1.5.

  • A shearing operation where only the x coordinates have a shear factor. The factor for the y coordinates is 0 so they are unaffected and the transformed shape is the same height as the original.

The AffineTransform Class

In Java, the AffineTransform class in the java.awt.geom package represents an affine transformation. Every Graphics2D graphics context has one. The default AffineTransform object in a graphics context is the identity transform, which leaves user coordinates unchanged. It is applied to the user coordinate system anyway for everything you draw, but all the coordinates are unaltered by default. You can retrieve a copy of the current transform for a graphics context object by calling its getTransform() method. For example:

AffineTransform at = g2D.getTransform();    // Get current transform

While this retrieves a copy of the current transform for a graphics context, you can also set it with another transform object:

g2D.setTransform(at);

You can retrieve the transform currently in effect with getTransform(), set it to some other operation before you draw some shapes, and then restore the original transform later with setTransform() when you're finished. The fact that getTransform() returns a reference to a copy rather than a reference to the original transform object is important. It means you can alter the existing transform and then restore the copy later.

Although the default transform object for a graphics context leaves everything unchanged, you could set it to do something by calling one of its member functions. All of these have a return type of void so none of them return anything:

Transform Default

Description

setToTranslation
(double deltaX, double deltaY)

This method makes the transform a translation of deltaX in x and deltaY in y. This replaces whatever the previous transform was for the graphics context. You could apply this to the transform for a graphics context with the statements:

// Save current transform and set a new one

AffineTransform at = g2D.getTransform();

at.setToTranslation(5.0, 10.0);

The effect of the new transform will be to shift everything that is drawn in the graphics context, g2D, 5.0 to the right, and down by 10.0. This will apply to everything that is drawn in g2D subsequent to the statement that sets the new transform.

setToRotation
(double angle)

You call this method for a transform object to make it a rotation of angle radians about the origin. This replaces the previous transform. To rotate the axes 30 degrees clockwise, you could write:

g2D.getTransform().setToRotation(30*Math.PI/180);

This statement gets the current transform object for g2D and sets it to be the rotation specified by the expression 30*Math.PI/180. Since ( radians is 180 degrees, this expression produces the equivalent to 30 degrees in radians.

setToRotation
(double angle, double deltaX, double deltaY)

This method defines a rotation of angle radians about the point deltaX,deltaY. It is equivalent to three successive transform operations – a translation by deltaX, deltaY, then a rotation through angle radians about the new position of the origin and then a translation back by -deltaX,-deltaY to restore the previous origin point.

You could use this to draw a shape rotated about the shape's reference point. For example, if the reference point for a shape was at shapeX,shapeY, you could draw the shape rotated through (/3 radians with the following:

g2D.getTransform().setToRotation(Math.PI/3,
shapeX, shapeY);
// Draw the shape...

The coordinate system has been rotated about the point shapeX,shapeY, and will remain so until you change the transformation in effect. You would probably want to restore the original transform after drawing the shape rotated.

setToScale
(double scaleX, double scaleY)

This method sets the transform object to scale the x coordinates by scaleX, and the y coordinates by scaleY. To draw everything half scale you could set the transformation with the statement:

g2D.getTransform().setToScale(0.5, 0.5);

setToShear
(double shearX, double shearY)

The x coordinates are converted to x+shearX*y, and the y coordinates are converted to y+shearY*x.

All of these methods that we have discussed here replace the transform in an AffineTransform object. We can modify the existing transform object in a graphics context, too.

Modifying the Transformation for a Graphics Context

Modifying the current transform for a Graphics2D object involves calling a method for the Graphics2D object. The effect in each case is to add whatever transform you are applying to whatever the transform did before. You can add each of the four kinds of transforms that we discussed before using the following methods defined in the Graphics2D class:

translate(double deltaX, double deltaY)

translate(int deltaX, int deltaY)

rotate(double angle)

rotate(double angle, double deltaX, double deltaY)

scale(double scaleX, double scaleY)

shear(double shearX, double shearY)

Each of these adds or concatenates the transform specified to the existing transform object for a Graphics2D object. Therefore you can cause a translation of the coordinate system followed by a rotation about the new origin position with the statements:

g2D.translate(5, 10);                    // Translate the origin
g2D.rotate(Math.PI/3);                   // Clockwise rotation 60 degrees
g2D.draw(line);                          // Draw in translate and rotated space

Of course, you can apply more than two transforms to the user coordinate system – as many as you like. However, it is important to note that the order in which you apply the transforms matters. To see why, look at the example below.

Click To expand

This shows just two transforms in effect, but it should be clear that the sequence in which they are applied makes a big difference. This is because the second transform is always applied relative to the new position of the coordinate system after the first transform has been applied. If you need more convincing that the order in which you apply transforms matters, you can apply some transforms to yourself. Stand with your back to any wall in the room. Now apply a translation – take three steps forward. Next apply a rotation – turn through 45 degrees clockwise. Make a mental note of where you are. If you now go back and stand with your back to the wall in the original position and first turn through 45 degrees before you take the three steps forward, you will clearly be in quite a different place in the room from the first time around.

Next on our affine tour – how we can create completely new AffineTransform objects.

Creating AffineTransform Objects

Of course, there are constructors for AffineTransform objects: the default 'identity' constructor and a number of other constructors, but we don't have space to go into them here. The easiest way to create transform objects is to call a static member of the AffineTransform class. There are four static methods corresponding to the four kinds of transform that we discussed earlier:

getTranslateInstance(double deltaX, double deltaY)

getRotateInstance(double angle)

getScaleInstance(double scaleX, double scaleY)

getShearInstance(double shearX, double shearY)

Each of these returns an AffineTransform object containing the transform that you specify by the arguments. To create a transform to rotate the user space by 90 degrees, you could write:

AffineTransform at = AffineTransform.getRotateInstance(Math.PI/2);

Once you have an AffineTransform object, you can apply it to a graphics context by passing it as an argument to the setTransform() method. It has another use too: you can use it to transform a Shape object. The createTransformedShape() method for the AffineTransform object does this. Suppose we define a Rectangle object with the statement:

Rectangle rect = new Rectangle(10, 10, 100, 50);

We now have a rectangle that is 100 wide by 50 high, at position 10,10. We can create a transform object with the statement:

AffineTransform at = getTranslateInstance(25, 30);

This is a translation in x of 25, and a translation in y of 30. We can create a new Shape from our rectangle with the statement:

Shape transRect = at.createTransformedShape(rect);

Our new transRect object will look the same as the original rectangle but translated by 25 in x and 30 in y, so its top-left corner will now be at (35, 40).

Click To expand

However, even though it will still look like a rectangle it will not be a Rectangle object. The createTransformedShape() method always returns a GeneralPath object since it has to work with any transform. This is because some transformations will deform a shape – applying a shear to a rectangle results in a shape that is no longer a rectangle. The method also has to apply any transform to any Shape object, and returning a GeneralPath shape makes this possible.

Let's try some of this out. A good place to do this is with our shape classes. At the moment we draw each shape or text element in the place where the cursor happens to be. Let's use a translation to change how this works. We will redefine each nested class to Element so that it translates the user coordinate system to where the shape should be, and then draws itself at the origin (0, 0). You could try to do this yourself before reading on. You just need to apply some of the transform methods we have been discussing.

Try It Out – Translation

To make this work we will need to save the position for each element that is passed to the constructor – this is the start point recorded in the mousePressed() method – and use this to create a translate transform in the draw() method for the element. Since we are going to store the position of every class object that has Element as a base, we might as well store the location in a data member of the base class. We can redefine the base class, Element, to do this:

public abstract class Element {
  public Element(Color color) {  
    this.color = color;  
  }

  public Color getColor(){  
    return color;  
  }

  // Set or reset highlight color
  public void setHighlighted(boolean highlighted) {
    this.highlighted = highlighted;
  }

  public Point getPosition() {  
    return position;  
  }

  public abstract java.awt.Rectangle getBounds();
  public abstract void modify(Point start, Point last);
  public abstract void draw(Graphics2D g2D);  

  protected Color color;                           // Color of a shape
  protected boolean highlighted = false;           // Highlight flag
  final static Point origin = new Point();         // Point 0,0
  protected Point position;                        // Element position

  // Definitions for our shape classes...
}

You might consider passing the start point to the Element constructor, but this wouldn't always work. This is because we need to figure out what the reference point is in some cases – for rectangles, for example. The position of a rectangle will be the top left corner, which is not necessarily the start point. We have included a method to retrieve the position of an element, as we are sure to need it. We also have added another member, origin, which is the point (0, 0). This will be useful in all the derived classes, as we will now draw every element at that point. Since we only need one, it is static, and since we won't want to change it, it is final.

Let's start with the nested class, Line.

Translating Lines

We need to update the constructor first of all:

public Line(Point start, Point end, Color color) {
  super(color);
  position = start;
  line = new Line2D.Double(origin, new Point(end.x – position.x, 
                                             end.y – position.y));
}

We've saved the point start in position, and created the Line2D.Double shape as the origin. Of course, we have to adjust the coordinates of the end point so that it is relative to (0, 0).

We can now implement the draw()method to use a transform to move the coordinate system to where the line should be drawn. We can economize on the code in the element classes a little by thinking about this because a lot of the code is essentially the same. Here's how we would implement the method for the Element.Line class directly:

public void draw(Graphics2D g2D) {
  g2D.setPaint(highlighted ? Color.MAGENTA : color);  // Set the line color
  AffineTransform old = g2D.getTransform();      // Save the current transform
  g2D.translate(position.x, position.y);         // Translate to position
  g2D.draw(line);                                // Draw the line
  g2D.setTransform(old);                         // Restore original transform
}

Before you do this, let's cover what this does. To draw the line in the right place, we just have to apply a translation to the coordinate system before the draw() operation. Saving a copy of the old transform is most important, as that enables us to restore the original scheme after we've drawn the line. If we don't do this, subsequent draw operations in the same graphics context will have more and more translations applied cumulatively, so objects get further and further away from where they should be. Only one line of code here involves the element itself, however:

g2D.draw(line);                                 // Draw the line

All the rest will be common to most of the types of shapes – text being the exception. We could add an overloaded draw() method to the base class, Element, that we can define like this:

protected void draw(Graphics2D g2D, Shape element) {
  g2D.setPaint(highlighted ? Color.magenta : color);  // Set the element color
  AffineTransform old = g2D.getTransform();      // Save the current transform
  g2D.translate(position.x, position.y);         // Translate to position
  g2D.draw(element);                             // Draw the element
  g2D.setTransform(old);                         // Restore original transform
}

You may need to add an import for java.awt.geom.AffineTransform. This will draw any Shape object after applying a translation to the point, position. We can now call this method from the draw() method in the Element.Line class:

public void draw(Graphics2D g2D) {
  draw(g2D, line);                              // Call base draw method
}

You can now go ahead and implement the draw() method in exactly the same way for all the nested classes to Element, with the exception of the Element.Text class. Just pass the underlying Shape reference for each class as the second argument to the overloaded draw() method. We can't use the base class helper method in the Element.Text because text is not a Shape object. We will come back to the class defining text as a special case.

We must think about the bounding rectangle for a line now. We don't want the bounding rectangle for a line to be at (0, 0). We want it to be defined in terms of the coordinate system before it is translated. This is because when we use it for highlighting, no transforms are in effect. For that to work the bounding rectangle must be in the same reference frame.

This means that we must apply the translation to the bounding rectangle that corresponds to the Line2D.Double shape. A base class helper method will come in handy here too:

protected java.awt.Rectangle getBounds(java.awt.Rectangle bounds) { 
  AffineTransform at = AffineTransform.getTranslateInstance(position.x, 
                                                            position.y);
  return at.createTransformedShape(bounds).getBounds();
}

Just add this method to the code for the Element class.

We first create an AffineTransform object that applies a translation to the point, position. Then we apply the createTransformedShape() method to the rectangle that is passed as the argument – which will be the bounding rectangle for a shape at (0, 0) – to get a corresponding shape translated to its proper position. Even though we get a GeneralPath object back, we can get a rectangle from that quite easily by calling its getBounds() method. Thus our helper method accepts a reference to an object of type java.awt.Rectangle, and returns a reference to the rectangle that results from translating this to the point, position. This is precisely what we want to do with the bounding rectangles we get with our shapes defined at the origin. We can now use this to implement the getBounds() method for the Element.Line class:

public java.awt.Rectangle getBounds() { 
  return getBounds(line.getBounds());
}

We just pass the reference to the line member of the class as the argument to the base class version of getBounds(), and return the rectangle that is returned by that method. The getBounds() methods for the nested classes Rectangle, Circle, and Curve will be essentially the same – just change the argument to the base class getBounds() call to the Shape reference corresponding to each class. To implement the getBounds() method for the Text class, just pass the bounds member of that class as the argument to the base class getBounds() method.

We must also update the modify() method, and this is going to be specific to each class. To adjust the end point of a line so that it is relative to the start point at the origin, we must change the method in the Element.Line class as follows:

public void modify(Point start, Point last) {
  line.x2 = last.x – position.x;
  line.y2 = last.y – position.y;
}

That's the Element.Line class complete. We can apply the same thing to all the other classes in the Element class.

Translating Rectangles

Here's the changes to Element.Rectangle constructor:

public Rectangle(Point start, Point end, Color color) {
  super(color);
  position = new Point(Math.min(start.x, end.x),
                       Math.min(start.y, end.y));
  rectangle = new Rectangle2D.Double(origin.x,
                                     origin.y,
                                     Math.abs(start.x – end.x),     // Width
                                     Math.abs(start.y – end.y));    // & height 
}

The expressions for the coordinates for the point, position, ensure that we do set it as the location of the top-left corner. The rectangle object is defined with its top-left corner at the origin, and its width and height as before. We have to adjust the modify() method to adjust the location stored in position, and leave the rectangle defined at the origin:

public void modify(Point start, Point last) {      
  position.x = Math.min(start.x, last.x);
  position.y = Math.min(start.y, last.y);
  rectangle.width = Math.abs(start.x – last.x);
  rectangle.height = Math.abs(start.y – last.y);
}

You should already have added the revised version of the draw() and getBounds() methods for an Element.Rectangle object essentially the same as that for lines.

Translating Circles

The Element.Circle class constructor is also very easy:

public Circle(Point center, Point circum, Color color) {
  super(color);
 
  // Radius is distance from center to circumference
  double radius = center.distance(circum); 
  position = new Point(center.x – (int)radius,
                       center.y – (int)radius);
      
  circle = new Ellipse2D.Double(origin.x, origin.y,     // Position – top-left
                                2.*radius, 2.*radius ); // Width & height
}

The radius is calculated as before, and we make the top-left corner of the Ellipse2D.Double object the origin point. Thus position is calculated as for the top-left corner in the previous version of the constructor. We can adjust the modify() method to record the new coordinates of position:

public void modify(Point center, Point circum) {
      double radius = center.distance(circum);
      position.x = center.x – (int)radius;
      position.y = center.y – (int)radius;
      circle.width = circle.height = 2*radius;
    }

The draw() and getBounds() methods are already done, so it's curves next.

Translating Curves

The Element.Curve class is just as simple:

    public Curve(Point start, Point next, Color color) {
      super(color);
      curve = new GeneralPath();
      position = start;
      curve.moveTo(origin.x, origin.y);
      curve.lineTo(next.x – position.x,
                   next.y – position.y);
    }

We store the start point in position, and create the curve starting at (0, 0). The end point has to be adjusted so that it is defined relative to (0, 0). Adding a new segment in the modify() method also has to be changed to take account of the new origin for the curve relative to the start point:

    public void modify(Point start, Point next) {
      curve.lineTo(next.x – start.x,
                   next.y – start.y);
    }

We just subtract the coordinates of the original start point that we saved in position from the point, next. The methods for drawing the curve and getting the bounding rectangle have already been updated, so the last piece is the Element.Text class.

Translating Text

The first step is to remove the declaration for the member, position, from this class, as we will now be using the member of the same name that is inherited from the base class.

The only changes we need to make to the constructor are as follows:

    public Text(Font font, String text, Point position,
JavaScript Editor
 Java Tutorials Free JavaScript Editor


Big data base scan of mri brain tissue. download