Home >> Media Group >> Research >> ViPER
Downloads -- Documentation -- Developers -- Discussion
LAMP     The Language and Media Processing Laboratory

Charles's Guide to ViPER-GT

Notes on Adding New Spatial Types to ViPER-GT

Currently, ViPER allows the user to define the following data types which are visible on the canvas.

  • point
  • bbox (bounding boxes: edges are parallel to the axis)
  • obox (oriented boxes: boxes that do not need to be parallel to the axes)
  • polygon (closed polygons)
  • circle
  • ellipse (oriented ellipses)

Suppose you want a data type that is not one of the ones listed above. You can extend the data types by creating your own. This requires some knowledge of Java as well as how ViPER is set up to handle data types.

Review

The basic unit of an aggregate data type in ViPER is called a descriptor. A descriptor is analagous to a Pascal record, C structure, or C++ class. It is a collection of fields. In ViPER, these fields are called attributes.

A descriptor consists of one or more attributes. Each attribute has a data type, which is one of the data types listed above. For example, suppose you wish to track the motion of people in a video clip. You might have a descriptor called Person. The descriptor, Person, could consist of two attributes: Head and Torso. Head can have data type ellipse. Torso can have type bbox.

From now on, we'll talk about attributes. In particular, we'll discuss creating your own data types which you can then associate with an attribute.

Representation

Internally, an attribute consists of objects over the entire duration of the clip. Think of an attribute as an array of objects, where each element corresponds to a frame. The internal representation for a circle data type is Circle. This is a Java class defined in the viper.api.geometry package. It's useful to define the class used for the internal representation in this package.

For purposes of creating and editing a shape on a canvas, there are three additional classes.

  • Creator class

    At any given frame, an attribute either appears on the canvas, or it doesn't. If it doesn't, then you create that attribute. The creation of an attribute in a particular frame depends on the attribute data type. For example, the class associated with the creation of a circle is called a CircleCreator.

    If you want to create your own data type, you need to write a Creator class. Details of how to do this is explained further below.

  • Editor class

    If an attribute already appears on the canvas, then you can edit it. The management of editing is handled by an Editor object. Each attribute that appears on a canvas at a frame has an Editor object which manages it. For example, if you see two ellipses and an oriented box, then there are at least three editors.

    Not every attribute is displayed on the canvas. For an attribute to be displayed, it must:

    • have a data type that allows displaying (the ones listed above)
    • be valid in that frame
    • have the descriptor tab set valid (i.e., green)

    Each descriptor has a tab. Each tab has a colored ball. If the ball is green, then all valid displayable attributes appear on the canvas. If it is red, then none of the attributes appear.

    This allows the user to selectively decide which descriptor (and associated attributes) appear on the canvas.

    Only valid (displayable) attributes in a given frame have an editor associated with it.

    If an attribute is valid, but the tab is red, there is still an Editor object associated with it, although you can not edit it until the descriptor tab is set back to green.

    A circle data type has a CircleCanvasEditor object that manages the editing of the shape on the canvas.

  • Node class

    Each Editor object contains a Node object. The Node object is added to the canvas. The Node class is a subclass of PPath. PPath objects can be displayed on a Piccolo canvas. ViPER uses the Piccolo canvas. Piccolo is a GUI toolkit that provides zooming capabilities, and was developed by Ben Bederson and his research group.

    The Editor object manages the Node object. Thus, for every Editor object, there is a Node object, which is displayed on the Piccolo canvas. The Editor objects handle the events on the canvas, and modify/update the Node object for display.

    For example, a CircleCanvasEditor object contains a CircleNode object.

Developing a SquareCreator Class

Suppose you wish to create a Square data type. Although there is already a bounding box data type, making the bounding box square (i.e., equal width and height) is not simple. You might want to create a datatype where this requirement is enforced. It is possible to extend bounding boxes to squares, but this would make manipulating bounding boxes more complicated.

Creator classes are derived from an abstract base class, CanvasCreator, which is itself derived from an abstract base class, PInputManager. PInputManager has the following useful methods:

  • mousePressed
  • mouseMoved
  • mouseDragged
  • mouseReleased
  • keyPressed
  • keyReleased

These are the events which the Creator class responds to as the user draws the shape.

ViperDataPLayer

A single instance of the ViperDataPLayer class manages the events on the canvas. The ViperDataPLayer object's purpose is to:

  • determine whether the canvas is in edit/create mode
  • handle the processing of events (mouse and key events)
  • pass these events to the appropriate Creator or Editor delegate object

In particular, the following events are first handled by the ViperDataPLayer object.

  • mousePressed
  • mouseReleased
  • mouseDragged
  • mouseMoved
  • keyPressed
  • keyReleased

This event is then passed to the delegate object.

At any point in time, the canvas is either in "edit" mode or in "create" mode. In edit mode, the user selects a shape to edit (at which point it becomes "selected"), and then edits it, e.g., moves it, resizes it, etc.

In create mode, the user selects a NULL attribute entry from the table by clicking on a valid descriptor. A creator object is constructed. The user can then create the object. Once the object has been created, the data for that object is stored internally, the creator object is removed, and an editor object now manages the shape displayed on the canvas

Why two modes?

The reason for having an editing and a creational mode is to make it easier to separate the user interface controls. For example, when you first create an oriented box, you pick an orientation. That orientation is fixed until the oriented box has been created. Once created, the object converts to edit mode, which allows you to change the orientation.

Piccolo

Piccolo is a zoomable interface developed by Dr. Ben Bederson. It is written in Java, and there is a Piccolo API for developers that wish to use Piccolo in their development. Piccolo makes it easy to pan and zoom on a canvas. The canvas used in ViPER is a Piccolo canvas.

Piccolo differs from Swing in the way it displays objects on a canvas. In Swing, the programmer must keep a data structure of some kind to hold all information used to display objects on a canvas. The programmer uses Graphics2D object to draw the shapes one at a time, and this is displayed on the canvas.

In Piccolo, the programmer adds (drawable) objects as children to a tree node. To draw, Piccolo traverses the tree and draws these objects. Thus, Swing forces explicit drawing, while Piccolo does it implicitly.

For basic data types, such as the ones used in ViPER, you only require a minimal knowledge of how Piccolo works.

Creator Class, in Detail

Instance Variables

A Creator class usually consists of at least one PPath instance variable. A PPath is an object that can be displayed on a Piccolo canvas. A PPath can be used to display shapes supported by Java. For example, you can tell a PPath to draw an Ellipse2D, or a Rectangle2D.

Since we're only displaying a square, we only need one PPath instance variable. This can be written as:

public SquareCreator extends CanvasCreator {
   PPath square = new PPath() ;
}

Some datatypes, such as the ellipse, have two or more PPath() instance variables. For example, the ellipse is displayed using a bounding box, an ellipse, and a handle for the bounding box. In this example, EllipseCreator would have 3 PPath instance variables.

Constructor

The typical constructor for a creator class looks like:

public SquareCreator( ViperDataPLayer.CreatorAssistant asst, Attribute attr )
{
   super( asst, attr ) ;
   displaySelected() ;
}

CreatorAssistant

The CreatorAssistant is an object that is an instance of an inner class of the ViperDataPLayer. Recall that a ViperDataPLayer object is the object in control of the events of the canvas. A Creator object needs some access to the ViperDataPLayer, and it is provided by the CreatorAssistant object, instead of the ViperDataPLayer object directly. This prevents a Creator object from having too much access to the ViperDataPLayer object.

Attribute

Recall that an attribute are fields of a descriptor. The Attribute type stores the data values of an attribute over the entire duration of a video clip. Attribute is an interface. The interface has methods to retrieve and set attribute data at frames specified by the user. As a programmer, you create a class that implements these methods, and objects of this class are stored internally, and represents the actual data being recorded when using ViPER.

When a constructor is called, a new Attribute of the appropriate type is created (e.g., Square).

Constructor in Detail

The square PPath() object is initialized in the declaration of the instance variable, as in:

PPath square = new PPath() ;

It can also be put in the constructor, if you prefer.

These are the two statements in a SquareCreator constructor:

super( asst, attr ) ;
displaySelected() ;

The first calls the constructor to the superclass, CanvasCreator. A superclass can have instance variables, which are not accesible by the subclass, except via methods provided by the superclass. In this case, we are telling the superclass's constructor to hold the assistant and the attribute.

displaySelected() is an abstract method in CanvasCreator. Its purpose is to set the color and stroke width of the shape being selected. Without this method, the shape would appear on the canvas in a default color (i.e., black) and a default stroke thickness, which would seem strange to the user.

We want to allow the user of ViPER to control the color of the shapes displayed. Currently, the colors used are:

Color Name Default Description
selected red Used for the selected attribute
unselected green Used for all unselected data that isn't in DWRT mode
dwrtSelected blue The data is selected and the frame is displayed with respect to it
dwrtUnselected gold The frame is displayed with respect to this item, but it is not selected

For now, don't worry about "display with respect to". "selected" means the user has selected it for editing or creating.

Since "creating" must be completed (or cancelled) before any further action can be taken, you can assume that it is always selected.

Writing displaySelected()

Here's a typical way to implement displaySelected():

public void displaySelected()
{
   Highlightable colorTable = getColorTable() ;

   square.setStroke( colorTable.getSelectedStroke() ) ;
   square.setStrokePaint( colorTable.getSelectedColor() ) ;
}

getColorTable() is a method defined in the CanvasCreator base class from which SquareCreator is derived.

getColorTable() returns a HighlightSingleton object which implements the Highlightable interface. The Highlightable interface defines the following methods:

  • getSelectedStroke()
  • getSelectedColor()

By using the colors from the colorTable, a user can redefine these colors without having to rewrite the Creator classes.

Here's an example of displaySelected() for a circle, which has two PPath objects: circle, which is the drawn circle, and refLine, which is the drawn radius of the circle.:

public void displaySelected()
{
   Highlightable colorTable = getColorTable() ;

   circle.setStroke( colorTable.getSelectedStroke() ) ;
   circle.setStrokePaint( colorTable.getSelectedColor() ) ;

   refLine.setStroke( colorTable.getSelectedStroke() ) ;
   refLine.setStrokePaint( colorTable.getSelectedColor() ) ;
}

Occasionally, you might select a different color than those defined by the HighlightSingleton. For example, an oriented box has a handle. The handle color is set to orange so it stands out more.

Handling Events

In general, the events you need to implement for a Creator class are:

  • mousePressed

    This usually initiates the drawing of the object about to be created. A mousePressed() events provides you information about the (x,y) coordinate on the canvas where the mouse button has been pressed.

  • mouseDragged

    This continues the drawing, usually providing an intermediate drawing, which the user sees.

  • mouseReleased

    This completes the drawing, defining a second (x,y) point where the mouse was released.

Some shapes are complicated enough that a simple mousePressed(), mouseDragged(), mouseReleased() action is not sufficient. For example, in an oriented box, the mousePressed() event defines an initial point, the mouseReleased() event defines a line starting at the initial point. Another mousePressed() event is needed to define the height.

In order to implement more complex shapes, state information is required. For example, for an oriented box, you can define a variable whose value is set to PHASE_ONE (some constant) when you are in the first phase of drawing, then set to PHASE_TWO (another constant) if you are in the second phase.

For now, our drawing is sufficiently simple that we do not need more than a single mousePressed(), mouseDragged(), and mouseReleased() event.

How to Draw a Square

It's worth your time to sit and think about how a user should draw the desired shape.

For a rectangle, this is easy. Two corners define a rectangle. Thus, the mousePressed() event defines one corner. The mouseReleased() event defines the other corner. Do the math necessary, and you have a rectangle.

A square is not so simple. We can't, for example, expect the user to be careful enough to guarantee the second point makes a square relative to the first point.

We could allow the user to draw a rectangle, and then once the mouseReleased() event occurs, we convert the result to a square. However, as the user is dragging the mouse, s/he would see a rectangle, and not get the desired visual feedback. When the user is drawing a square on the canvas, the user expects to see a square.

We can still use the basic rectangle model to draw a square. Here's how it can be done. The mousePressed() event defines one corner of a square. After pressing the mouse, the use is dragging the mouse. This generates mouseDragged() events which give the second (x, y) coordinates. Recall that two corners define a rectangle. A rectangle has a width and a height, which can be computed.

Pick the larger of the width or height of the rectangle, and make that both the height and width of the square. The dragged coordinates will always lie on top of one edge or the other.

We're still not quite done. In Java, squares (or, more generally, rectangles) are defined by the (x,y) coordinate of the upper left hand corner, and the height and width of the square.

Suppose the initial coordinate is (x1, y1) and the second coordinate is (x2, y2). If x2 < x1, i.e., the second coordinate is "left" the first, then the upper left hand corner is no longer (x1, y1). So, to determine the coordinates of the square, you need to know the relation of (x2, y2) to (x1, y1).

In particular, imagine the plane divided into quadrants at (x1, y1). You need to determine if (x2, y2) is to the northeast, northwest, southeast, or southwest of (x1, y1), and use that to determine the square.

We'll write a method called:

Rectangle2D makeSquare( Point2D press, Point2D drag ) ;

which takes two points and returns back a square. We can use this method to help draw a square.

Writing makeSquare()

The basic algorithm is simple:

  1. Compute height and width based on the initial point and the drag point (which are defined by mousePressed() and mouseDragged()).
  2. Pick the larger of the two sizes to be the size of the square
  3. If the drag point is "left" of the initial point, set the x coordinate to the x coordinate of the initial point minus the size.
  4. If the drag point is "above" the initial point, set the y coordinate to the y coordinate of the initial point minus the size, otherwise set it to the y coordinate of the initial point.
  5. Return a rectangle with upper left hand coordinate (x, y) and width and height both set to size.
private Rectangle2D makeSquare( Point2D init, Point2D drag )
{
   // Compute height and width
   double width = Math.abs( press.getX() - init.getX() ) ;
   double height = Math.abs( press.getY() - init.getY() ) ;

   // Pick larger to be size
   double size = width > height ? width : height ;

   // Compute x coordinate of rectangle
   double x = isAbove( drag, init ) ? init.getX() - size : init.getX() ;

   // Compute y coordinate of rectangle
   double y = isLeft( drag, init ) ? init.getY() - size : init.getY() ;

   // Return square
   return new Rectangle2D.Double( x, y, size, size ) ;
}

private static boolean isAbove( Point2D first, Point2D second )
{
   return first.getY() < second.getY() ;
}

private static boolean isLeft( Point2D first, Point2D second )
{
   return first.getX() < second.getX() ;
}

Implementing mouse event methods

Once makeSquare() has been written, it's easy enough to write the mouse event methods.

Let's begin with mousePressed().:

Point2D pressPoint, dragPoint ;
public void mousePressed( PInputEvent e )
{
   super.mousePressed( e ) ;

   pressPoint = e.getPosition() ;
   // Give a default value to drag point for now
   dragPoint = pressPoint ;

   updateSquare() ;
   getAssistant().addShape( square ) ;
}

private Square updateSquare()
{
   Rectangle2D square2D = makeSquare( pressPoint, dragPoint ) ;

   // Recall square is an instance variable of type PPath
   square.setPathTo( square2D ) ;

   // Return value ignored except in mouseReleased()
   return square2D ;
}

Pay attention to the last statement in mousePressed().:

getAssistant().addShape( square ) ;

This statement should be made in mousePressed() after the shape has been updated.

Let's look closer at what's happening in this statement. getAssistant() returns a reference to CreatorAssistant object, Recall this object was passed as parameter in the constructor. CreatorAssistant provides a few methods that allow Creator objects to interact with the ViperDataPLayer object.

addShape() is a method in CreatorAssistant that adds a PPath to the canvas. Recall that the canvas is managed by the ViperDataPLayer object, and the CreatorAssistant is an inner class of ViperDataPLayer, which means it can access methods of ViperDataPLayer.

If you do not call the addShape() method, the shape does not appear on the canvas.

Notes:

  • We call super.mousePressed( e ) so the superclass can do what it needs to do (if anything).
  • pressPoint is the initial point, when the mouse is initially clicked, but not yet released. It defines one corner of the square.
  • dragPoint is the second point. Initially, we set the dragPoint to the pressPoint.
  • pressPoint and dragPoint are instance variables which makes it easier to access in the mouse methods.
  • updateSquare() is put in its own method since mousePressed(), mouseDragged() and mouseReleased() are all going to call this method.
  • A Rectangle2D object is returned back.
  • We call square.setPathTo( square2D ). This causes square to display as the square2D. In Piccolo, a PPath can be set to any class which implements the java.awt.Shape interface. Square2D is a Rectangle2D object, which implements the java.awt.Shape interface.

Implementing mouseDragged()

Now, we proceed to mouseDragged().:

public void mouseDragged( PInputEvent e )
{
    super.mouseDragged( e ) ;
    dragPoint = e.getPosition() ;
    updateSquare() ;
}

This is very simple. It updates the drag point, which is the second point of the square. updateSquare() then updates the square on the canvas.

Implementing mouseReleased()

Finally we write mouseReleased().:

public void mouseReleased( PInputEvent e )
{
    super.mouseDragged( e ) ;
    dragPoint = e.getPosition() ;
    Rectangle2D square2D = updateSquare() ;

    // Switch to edit listener, before updating in mediator
    getAssistant().switchListener() ;

    // Retrieve data from square
    int x = (int) square2D.getX() ;
    int y = (int) square2D.getY() ;
    int size = (int) square2D.getWidth() ;

    // Update value in mediator
    Square sq = new Square( x, y, size ) ;
    setAttrValueInMediator( sq ) ;
}

Once the mouse is released, we're ready to update the value internally. In this case, we're using Square, which is a subclass of Attribute. This class needs to be implemented.

The call:

getAssistant().switchListener() ;

switches the ViperDataPLayer object from creator mode to edit mode.

After that, we update the value of the attribute in the mediator. The mediator is an object that manages the data. Basically, the user edits a visual representation of the data in the canvas, which is managed by a Creator class. Once the shape has been created, we need to store that internally, and this is done by setting the appropriate value in the mediator.

Recall that each data type has several classes or interface associated with it: a Creator class, an Editor class, a Node class, and an Attribute interface. In particular, you create subclasses or implementation of these classes/interfaces. Three of the classes are used to manage shapes that appear on the canvas (Creator, Editor, Node). Attribute is used to store the information internally.

The call:

setAttrValueInMediator( sq ) ;

is used to set the value of the attribute at the current frame. We used a Square object to store this information. Later on, we'll discuss how to implement an Attribute.

Alternate choice

Our solution requires writing your own class that implements the Attribute interface. In particular, you need to write a Square class. However, there's already a built-in class that works: BoundingBox. This is the Attribute that's used with the data type, bbox.

This raises an interesting question: how does ViPER know that a BoundingBox is a "square" or a "bbox"? This information is stored in an Attribute Configuration object. However, as a person developing a new data type, you won't need to worry about that.

Here's how mouseReleased() would look if we used BoundedBox.:

public void mouseReleased( PInputEvent e )
{
    super.mouseDragged( e ) ;
    dragPoint = e.getPosition() ;
    Rectangle2D square2D = updateSquare() ;

    // Switch to edit listener, before updating in mediator
    getAssistant().switchListener() ;

    // Retrieve data from square
    int x = (int) square2D.getX() ;
    int y = (int) square2D.getY() ;
    int size = (int) square2D.getWidth() ;

    // Update value in mediator
    BoundedBox sq = new BoundedBox( x, y, size, size ) ;
    setAttrValueInMediator( sq ) ;
}

It's almost identical to the previous version.

Updating the value in the mediator

setAttrValueInMediator() is a method in CanvasCreator which uses the CreatorAssistant object to set the value in the current frame. The value is only updated once the entire shape is completely drawn.

This is done primarily for efficiency reasons. When an update to the mediator occurs, there is an update of the canvas. If we called update to the mediator during mouseDragged() events (which occurs many times as the mouse is dragged), this would cause performance problems. It would also invoke code in the ViperDataPLayer object, which may affect ViPER in undesirable ways.

Thus, we make the call to setAttrValueInMediator() once, after the shape has been drawn, and once we've switched from create to edit mode (using the call, getAssistant().switchListener()).

Internal Data

The user performs editing actions on canvas. Typical editing operations include moving a shape to a new location, and resizing the shape. Once the editing operation is completed, the new shape configuration is stored in "internal data".

I use the term "internal data" to refer to the permanent location for the data.

For efficiency reasons, it's useful to edit a local copy of the data, and only commit the values once the editing operation is complete. For example, suppose the user drags the circle from one side of the canvas to the other. As it is dragged, the circle's position is being changed. However, rather than continuously commit this data to "internal data", the update is made locally, which I call "local data". The information about the location of the shape must be updated (otherwise, how would we see the changes on the canvas), however, it can be done locally.

Once the user has released the mouse button, indicating the circle is now at the desired location, this location is then committed to internal data.

Editor Class, in Detail

Once a shape has been created on the canvas, the Creator object associated with the shape is replaced by an Editor object. We use a separate Editor object because the kinds of control needed to create a shape is likely to be different from the control needed to edit the shape.

A Creator object contained PPath instance variables which were put on the canvas, and displayed. In an Editor object, you don't work with PPath objects directly. Instead, you create a Node class. This is done by subclassing PPath, and implementing Attribute. An Editor object contains one instance of a Node object, which matches the type of the Editor.

For example, we're going to write a SquareCanvasEditor class and a SquareNode class. SquareNode is a subclass of PPath, which means a Piccolo canvas can display a SquareNode.

Here's an outline of a SquareNode class definition:

public class SquareNode extends PPath implements Attributable
{

}

Writing a Node class requires:

  • Determining what PPath instance variables are needed for highlighting
  • Providing methods that allow the Editor class to update state information for the Node class
  • Implementing the following methods required by the Attributable interface
    • setAttribute( Attribute attr )
    • getAttribute()
    • getUpdatedAttribute()

The Node object is what's displayed on the canvas.

Division of Labor

The Editor object handles the mouse and key events. The events usually requires resizing the shape (i.e., the Node object) or moving the shape (i.e., translating its location). Thus, when you write a Node class, you need to write methods that allow the Editor to pass information to the Node.

Implementing Attributable interface

Each Node class should implement the following the methods from the Attributable interface:

-- setAttribute( Attribute attr ) -- getAttribute() -- getUpdatedAttribute()

Implementing setAttribute()

setAttribute( Attribute attr )

This method takes an Attribute object, and it should update the Node object so it displays the information contained in the attribute at the current frame. This is a typical implementation.:

public void setAttribute( Attribute attr )
{
   this.attr = attr ; 
   Instant now = mediator.getMajorMoment() ;
   Square square = (Square) attr.getAttrValueAtInstant( now ) ;
  
   if ( square != null )
      setPath( square ) ;
}

Let's look at each line.:

LINE 1: 
 this.attr = attr ;

This saves the attribute to an instance variable so it can be fetched later on.:

LINES 2 and 3: 
 Instant now = mediator.getMajorMoment() ;
 Square square = (Square) attr.getAttrValueAtInstant( now ) ;

From the attribute, it extracts the data value for the current frame. mediator.getMajorMoment() fetches an object that represents the current frame being displayed.:

LINE 4 and 5:         
 if ( square != null )
    setPath( square ) ;

This sets the PPath to the square. Recall there are two different representations for a data value. There's the internal representation which stores the data. Those are subclasses of Attribute. There's also the displayed representation on the Piccolo canvas. That's a PPath object (or, in this case, a Node class, which is a subclass of PPath).

Implementing setPath()

There are two ways to store information about the shape to be drawn.

  1. Have an instance variable store a copy of the Attribute variable.

    For example, we could create a Square instance variable, and this would hold the information that we would use to update the Node object.:

    Square sq ;  // An instance variable
    
  2. Use another type to store the information. For example, we need to store information about a Square object, so we could declare the following instance variables:

    double x, y, size ;
    
This can be updated through methods provided by SquareNode by the SquareCanvasEditor

We'll use the first solution. There's not a strong preference for why we pick this.:

private void setPath( Square square )
{
   sq = new Square( square ) ; // make a copy
   redraw() ;
}

redraw() is a method you implement to update the current Node for display. Since this is a complex method, we'll discuss it after the covering the remaining Attributable methods.

Implementing getAttribute()

This is simple to implement:

public void getAttribute() 
{
   return attr ;
}

Recall attr is an instance variable.

Implementing getUpdatedAttribute()

Also easy to implement:

public Object getUpdateAttributable()
{
   Square copySquare = new Square( localSquare ) ;
   return localSquare ;
}

Recall that an Editor object contains information about the location and size of a shape as it appears on the canvas. For efficiency reasons, this information is not updated the editing action is complete. For example, suppose you want to drag a square from one side of the screen to the other. You select the square by pressing the mouse when the cursor is in the square, drag the mouse, and release the mouse button when the square is in its final location.

The Editor object maintains information about the location and size of the square at all times. However, this information is not updated in the Attribute object (which stores the actual data for the frame) until the mouse is released. This is done for efficiency reasons. Updating values in the Attribute causes reset operations to occur, which causes other actions to be performed, which would not only affect the speed of the operation, but cause other code to run that we might not want to run.

In our example, we assume that the local information of the square object is stored in an instance variable of type Square. This happens to be the same type used to store a Square in the attribute. However, there's nothing preventing someone implementing an Editor object from using their own internal, representation for a shape.

In this case, it's easier to implement getUpdatedAttribute() which must return an object of the type that's used in the Attribute object, since we can just use a copy constructor (which needs to be implemented, since Java doesn't have copy constructors by default).

Highlighting

The user needs visual feedback to know what kind of operations can be performed on the Square. As the designer of the class, you need to figure out what operations the user can perform, and then figure out the appropriate feedback.

There are two typical operations: translating and resizing

Translation

To translate means to shift the square to some other location, but preserve its height and width. The user might select this option if the mouse is inside the Square. We need a way for the user to know that the square can be moved.

For bounding and oriented boxes, the four sides of the boxes are highlighted. This is done by creating a PPath that is magenta in color, and thicker than the original.

It's the responsibility of the Editor to inform the Node object that the cursor is inside the square, and then the Node responds by highlighting the entire square. Thus, the Node does not handle mouse or key events. Its purpose is for display.

Resizing

To keep things simple, the square can only be resized if the cursor is near one of the four corners. The user knows resizing is possible because a circle is highlighted around a corner when the cursor is near it.

Implementing highlighting

First, we declare additional PPaths instance variables for highlighting:

PPath highlightSq, highlightCircle ;

We can initialize the instance variable directly, or in the constructor. Here's an example of doing it in the constructor.:

public SquareNode( ... )
{
   // ... some code

   highlightSq = new PPath() ;
   highlightCircle = new PPath() ;
   // ... more code
}

highlightSq causes the square to be highlighted in magenta, and is used when the user wants to drag a square to another location. highlightSq creates a circle around one of the corners of the square to tell the user that a resizing operation can be performed.

These are both PPath objects that are drawn on the canvas. We want to control its appearance as well as whether it appears or not, based on what the user is doing.

Initially, we need to set the color and stroke thickness. This does not display anything yet. Here's the code to set the color and stroke. Assume this is part of the code for class SquareNode.:

// Instance variable
Highlightable colorTable = HighlightSingleton.colorTable ;

// Constructor for SquareNode which creates the PPath objects
// and sets their color and stroke
public SquareNode( ... )
{
   // ... some code

   highlightSq = new PPath() ;
   highlightCircle = new PPath() ;

   // Set stroke width and paint
   highlightSq.setStroke( colorTable.getMediumHighlightStroke() ) ;
   highlightSq.setStrokePaint( colorTable.getHighlightColor() ) ;

   highlightCircle.setStroke( colorTable.getMediumHighlightStroke() ) ;
   highlightCircle.setStrokePaint( colorTable.getHighlightColor() ) ;

   // Add as children to node
   addChild( highlightSq ) ;
   addChild( highlightCircle ) ;
}

As in the Creator class, we use colorTable to pick the color and stroke. For highlighting, we have one method in Highlightable for picking the highlighting color.:

colorTable.getHighlightColor() 

This method currently returns magenta.

There are three methods for selecting how thick the line (i.e., stroke) is:

colorTable.getThinHighlightStroke() 
colorTable.getMediumHighlightStroke() 
colorTable.getThickHighlightStroke() 

You can pick whichever you feel is suitable. You should check out the implementations of other Node classes to see which thickness is used most often, so that it is consistent.

addChild

In Piccolo, there is a root node of a tree of objects that are displayed on a canvas. PPath objects can be added as children to this root. The objects are drawn on the canvas so that parent nodes are drawn before child nodes.

The last two lines of the constructor written above are:

addChild( highlightSq ) ;
addChild( highlightCircle ) ;

This is added to the SquareNode object (recall SquareNode is a class derived from PPath, and therefore has an addChild() method).

By adding as children to SquareNode, these PPath objects are drawn after the SquareNode is drawn. That's good because we want the highlighting to be drawn on top of the square, so that it shows up (instead of underneath).

Currently, neither highlightSq nor highlightCircle are set to any shape, so they do not appear on the canvas. Later on, we set the shape, so they are visible on the canvas.

Creating Methods for Highlighting

Recall the division of labor. Editor classes handle the mouse and key events. Node classes handle the display of the shape on the canvas. An Editor object contains an instance of a Node object.

We expect two kinds of operations.

  • Dragging the entire shape (but not resizing it)
  • Resizing the square, by grabbing one of the corners

To indicate dragging the entire shape, the entire square is highlighted. (See bbox or obox). This occurs when the mouse cursor is inside the square.

To indicate resizing the corner, one of the corners is highlighted.

It's the Editor object's job to determine which operation has been performed. For example, we can create these three SquareNode methods:

// Sets the shape of the square
// ( x, y ) is upper left hand corner
public void setSquare( double x, double y, double size ) ;

// Picks one of the four corners to highlight
public void bold( CanvasDir dir ) ;

// Highlights entire square
public void boldAll() ;

The first method is used for both resizing and for moving the shape. It's up to the SquareCanvasEditor object to call this method correctly when updating the node.

The second method is for bolding one of the four corners. (Bolding is the same as highlighting). A SquareCanvasEditor object calls this method when to indicate to the user that they are resizing the square.

The third method is for highlighting the entire square. A SquareCanvasEditor object calls this method when to indicate to the user that they are moving the square.

Implementing the Node methods

Let's implement each method.:

Point2D [] squarePts = new Point2D[ 5 ] ;

public void setSquare( double x, double y, double size ) 
{
    squarePts[ 0 ] = new Point2D.Double( x, y ) ;
    squarePts[ 1 ] = new Point2D.Double( x + size, y ) ;
    squarePts[ 2 ] = new Point2D.Double( y + size ) ;
    squarePts[ 3 ] = new Point2D.Double( x + size, y + size ) ;
    squarePts[ 4 ] = squarePts[ 0 ] ;
    
    // Calls PPath method to set the path
    setPathToPolyline( squarePts ) ;

    // To highlight
    rehighlight() ;
}

SquareNode is a derived class from PPath. Calling the setPathToPolyline() method makes the PPath display itself as a polyline. A polyline is a sequence of connected line segments. You provide an array of Point2D objects (in this example, it's squarePts). It draws a line from the point at index i to index i + 1, provided i and i + 1 are in bounds.

Then, there is a call to rehighlight(). We'll explain the implementation of this method in a moment.

Case 1: Resizing the square

Now, we implement the first version of bold:

// Instance flag variable for highlighting a corner
CanvasDir highlightDir = CanvasDir.NONE ;

// Instance flag variable for highlighting the entire square
boolean boldAll = false ;

public void bold( CanvasDir dir ) 
{
   // Indicate which corner to highlight: TOP_LEFT, TOP_RIGHT,
   // BOTTOM_LEFT, BOTTOM_RIGHT
   highlightDir = dir ;

   // We are resizing, not moving, so turn off boldAll
   boldAll = false ;

   // Redraw the highlighting
   rehighlight() ;
}

We use two instance variables to keep track of what highlighting is being done:

highlightDir
boldAll

If we are resizing a square, then highlightDir can be one of the following four values:

-- CanvasDir.TOP_LEFT
-- CanvasDir.TOP_RIGHT
-- CanvasDir.BOTTOM_LEFT
-- CanvasDir.BOTTOM_RIGHT

This value is the corner that is being grabbed (the opposite corner is "fixed").

In that case, boldAll should be false, since the user can't simulataneously adjust a corner and move a shape.

Then, there's a call to rehighlight().

Case 2: Moving the square

If are moving the square, the entire square is highlighted. To indicate this, we set boldAll to true, and set highlightDir to CanvasDir.NONE indicating no direction is highlighted.:

public void bold()
{
   // We are moving
   boldAll = true ;

   // Turn off highlighting of corners
   highlightDir = CanvasDir.NONE ;

   // Redraw the highlighting
   rehighlight() ;
}

Again, this method only sets flags. The highlighting is redone in a method called rehighlight().

rehighlight()

Based on the information from boldAll and highlightDir, the method rehighlight is meant to redo the highlighting. This method should be called whenever the highlighting has changed, or whenever the square has been resized.

Here's an implementation:

void rehighlight()
{
    Point2D [] blank = new Point2D[ 1 ] ;
    blank[ 0 ] = new Point2D.Double() ;
    if ( boldAll }
    {
        highlightSq.setPathToPolyline( squarePts ) ;
    }
    else // set it to blank
    {
        highlightSq.setPathToPolyline( blank ) ;
    }

    int x = squarePts[ 0 ].getX() ;
    int y = squarePts[ 0 ].getY() ;
    int rad = getCornerRadius() ;
    // Length of one side of square
    int size = squarePts[ 1 ].getX() - x ;
    // Draw ellipse left and above the corner of the square
    int modX = x - rad ;
    int modY = y - rad ;
    
    // Highlight the corner
    if ( highlightDir == CanvasDir.TOP_LEFT )
    {
        highlightCircle.setPathToEllipse( modX, modY, 2 * rad, 2 * rad ) ;
    }
    else if  ( highlightDir == CanvasDir.TOP_RIGHT )
    {
        highlightCircle.setPathToEllipse( modX + size, modY, 
                                          2 * rad, 2 * rad ) ;
    }
    else if  ( highlightDir == CanvasDir.BOTTOM_LEFT )
    {
        highlightCircle.setPathToEllipse( modX, modY + size, 
                                          2 * rad, 2 * rad ) ;
    }
    else if  ( highlightDir == CanvasDir.BOTTOM_RIGHT )
    {
        highlightCircle.setPathToEllipse( modX + size, modY + size, 
                                          2 * rad, 2 * rad ) ;
    }
    else // set it to blank
    {
        highlightCircle.setPathToPolyline( blank ) ;
    }
    
}

This method checks if boldAll is true. If so, it highlights the entire square. If not, it doesn't highlight at all. Highlighting is done by making the highlighted square (whose color was set to magenta, and whose width was set wider than the width of the usual shape) set to the size of the original square (which is why we use squarePts array, since that holds points that make up the square).

Then, we highlight corners. To draw a circle whose center is at (x, y), we must draw its bounding box at (x - radius, y - radius) and make the width and height of the bounding box equal to 2 * radius.

In implementing this method, we assume that boldAll and highlightDir are never both set to true/non-null values, since we can only either be resizing or moving the square, but not both.