001: /*************************************************************************
002: * *
003: * 1) This source code file, in unmodified form, and compiled classes *
004: * derived from it can be used and distributed without restriction, *
005: * including for commercial use. (Attribution is not required *
006: * but is appreciated.) *
007: * *
008: * 2) Modified versions of this file can be made and distributed *
009: * provided: the modified versions are put into a Java package *
010: * different from the original package, edu.hws; modified *
011: * versions are distributed under the same terms as the original; *
012: * and the modifications are documented in comments. (Modification *
013: * here does not include simply making subclasses that belong to *
014: * a package other than edu.hws, which can be done without any *
015: * restriction.) *
016: * *
017: * David J. Eck *
018: * Department of Mathematics and Computer Science *
019: * Hobart and William Smith Colleges *
020: * Geneva, New York 14456, USA *
021: * Email: eck@hws.edu WWW: http://math.hws.edu/eck/ *
022: * *
023: *************************************************************************/package edu.hws.jcm.draw;
024:
025: import java.awt.*;
026: import java.awt.event.*;
027: import edu.hws.jcm.data.*;
028: import edu.hws.jcm.awt.*;
029:
030: /**
031: * A DraggablePoint can be added to a DisplayCanvas, where it appears as a small disk, square, or
032: * cross. (The visual style is a settable property.) This object can be dragged with the mouse,
033: * within the limits of the CoordinateRect that contains the DraggablePoint. Either the x- or
034: * y-value of the point can be clamped to a specified Value. Typically, the y-value might
035: * be given by some function of the x-value. In that case, the point is constrained to move
036: * along the graph of the function. Or the x- or y-value can be clamped to a constant to make
037: * the point move along a vertical or horizontal line. Two Variables are associated with
038: * the DraggablePoint. These Variables represent the x- and y- values of the point. Each Variable
039: * implements the Tieable interface, so it can be synchronized with other Tieable values such as
040: * a VariableIput or VariableSlider.
041: */
042:
043: public class DraggablePoint extends Drawable implements InputObject,
044: Draggable {
045:
046: /**
047: * A style constant that specifies the visual appearance of a DraggablePoint to be a disk.
048: */
049: public static final int DISK = 0;
050:
051: /**
052: * A style constant that specifies the visual appearance of a DraggablePoint to be a square.
053: */
054: public static final int SQUARE = 1;
055:
056: /**
057: * A style constant that specifies the visual appearance of a DraggablePoint to be a cross.
058: */
059: public static final int CROSS = 2;
060:
061: private int radius; // Radius of the point.
062: private Color color; // Color of the point.
063: private Color ghostColor; // Color used for point when it is undefined or outside the CoordinateRect.
064: private int style; // One of the above style constants, DISK by default.
065: private double xLoc, yLoc; // The current x- and y-values of the point.
066: private int xPosition, yPosition; // The pixel position of the point.
067: private boolean useGhost; // This is true if the point is a "ghost" (undefined or outside the CoordinateRect).
068: private DPV xVar, yVar; // The Variables that represent the x- and y-values; DPV is a private nested class, defined below.
069: private Controller onUserAction; // A Controller whose compute method is called when the user drags the point.
070: private Value clampX, clampY; // Values used to clamp the x- and y-values. Only one can be non-null.
071:
072: /**
073: * Create a DraggablePoint with default values for style, radius, color. The point appears as a dark gray disk of radius 4.
074: */
075: public DraggablePoint() {
076: this (DISK);
077: }
078:
079: /**
080: * Create a DraggablePoint with specified visual style. Radius is 4, color is darkGray, and
081: * ghostColor is lightGray.
082: *
083: * @param style One of the style constants DraggablePoint.DISK, DraggablePoint.SQUARE, or DraggablePoint.CROSS.
084: */
085: public DraggablePoint(int style) {
086: if (style >= 0 && style <= 2)
087: this .style = style;
088: setColor(Color.darkGray);
089: setGhostColor(Color.lightGray);
090: radius = 4;
091: xPosition = -10000;
092: xLoc = Double.NaN;
093: yLoc = Double.NaN;
094: xVar = new DPV(true);
095: yVar = new DPV(false);
096: }
097:
098: /**
099: * Clamp the x-value of the point to v. That is, if v is not null, then whenever the location of the point
100: * changes, its x-value is modified to v.getVal(). Note that if v is non-null then any clamp Value
101: * specified for y will be cleared since x and y cannot both be clamped.
102: */
103: public void clampX(Value v) {
104: clampX = v;
105: if (v != null)
106: clampY = null;
107: checkClamp();
108: needsRedraw();
109: }
110:
111: /**
112: * Clamp the y-value of the point to v. That is, if v is not null, then whenever the location of the point
113: * changes, its y-value is modified to v.getVal(). Note that if v is non-null then any clamp Value
114: * specified for x will be cleared since x and y cannot both be clamped.
115: */
116: public void clampY(Value v) {
117: clampY = v;
118: if (v != null)
119: clampX = null;
120: checkClamp();
121: needsRedraw();
122: }
123:
124: /**
125: * Clamp the x-value of the point to the constant x, so that the point is constrained to a vertical line.
126: */
127: public void clampX(double x) {
128: clampX(new Constant(x));
129: }
130:
131: /**
132: * Clamp the y-value of the point to the constant y, so that the point is constrained to a horizontal line.
133: */
134: public void clampY(double y) {
135: clampY(new Constant(y));
136: }
137:
138: /**
139: * Clamp the x-value of the point to the function f, so that the point is constrained to move along the graph of x = f(y).
140: * f must be a function of one variable.
141: */
142: public void clampX(Function f) {
143: if (f != null)
144: clampX(new ValueMath(f, xVar));
145: }
146:
147: /**
148: * Clamp the y-value of the point to the function f, so that the point is constrained to move along the graph of y = f(x).
149: * f must be a function of one variable.
150: */
151: public void clampY(Function f) {
152: if (f != null)
153: clampY(new ValueMath(f, xVar));
154: }
155:
156: /**
157: * Get the radius used for drawing the point. The point's height and width are given by two times the radius.
158: */
159: public int getRadius() {
160: return radius;
161: }
162:
163: /**
164: * Set the radius that determines the size of the point when it is drawn.
165: * The point's height and width are given by two times the radius.
166: */
167: public void setRadius(int r) {
168: if (r > 0) {
169: radius = r;
170: needsRedraw();
171: }
172: }
173:
174: /**
175: * Set the visual style of the point. The style should be one of the constants
176: * DraggablePoint.DISK, DraggablePoint.SQUARE, or DraggablePoint.CROSS. If it is not,
177: * then nothing is done.
178: */
179: public void setStyle(int style) {
180: if (style >= 0 && style <= 2) {
181: this .style = style;
182: needsRedraw();
183: }
184: }
185:
186: /**
187: * Get the visual style of the point, which must be one of the constants
188: * DraggablePoint.DISK, DraggablePoint.SQUARE, or DraggablePoint.CROSS.
189: */
190: public int getStyle() {
191: return style;
192: }
193:
194: /**
195: * Get the variable that represents the current x-value of the point. (Note that this
196: * variable can be type-cast to type Tieable.)
197: */
198: public Variable getXVar() {
199: return xVar;
200: }
201:
202: /**
203: * Get the variable that represents the current y-value of the point. (Note that this
204: * variable can be type-cast to type Tieable.)
205: */
206: public Variable getYVar() {
207: return yVar;
208: }
209:
210: /**
211: * Get the color used for drawing the point.
212: */
213: public Color getColor() {
214: return color;
215: }
216:
217: /**
218: * Set the color to be used for drawing the point. If the specified Color value is
219: * null, then nothing is done.
220: */
221: public void setColor(Color c) {
222: if (c != null) {
223: color = c;
224: needsRedraw();
225: }
226: }
227:
228: /**
229: * Get the "ghostColor" of the point. This color is used for drawing the point when its x-value
230: * or y-value is undefined or outside the range of values on the CoordinateRect that contains
231: * the point. (This can happen because of clamping of values. It can also happen if the limits
232: * on the CoordinateRect are changed.)
233: */
234: public Color getGhostColor() {
235: return ghostColor;
236: }
237:
238: /**
239: * Set the ghoseColor to be used for drawing the point when it location is undefined or is outside the
240: * proper limits. If the specified Color value is null, then nothing is done.
241: */
242: public void setGhostColor(Color c) {
243: if (c != null) {
244: ghostColor = c;
245: needsRedraw();
246: }
247: }
248:
249: /**
250: * Set the Controller that is to be notified when the user drags the point. (The compute() method
251: * of the Controller is called.) If the Controller value is null, then no notification is done.
252: */
253: public void setOnUserAction(Controller c) {
254: onUserAction = c;
255: }
256:
257: /**
258: * Method required by InputObject interface; in this class, it simply calls
259: * setOnUserAction(c). This is meant to be called by JCMPanel.gatherInputs().
260: */
261: public void notifyControllerOnChange(Controller c) {
262: setOnUserAction(c);
263: }
264:
265: /**
266: * Get the Controller that is notified when the user drags the point. A null value means that
267: * no notification is done.
268: */
269: public Controller getOnUserAction(Controller c) {
270: return onUserAction;
271: }
272:
273: /**
274: * Move the point to (x,y), then "clamp" the value of x or y, if a clamp Value has been set.
275: */
276: public void setLocation(double x, double y) {
277: xLoc = x;
278: yLoc = y;
279: xVar.setVariableValue(x);
280: yVar.setVariableValue(y);
281: xVar.serialNumber++;
282: yVar.serialNumber++;
283: checkClamp();
284: needsRedraw();
285: }
286:
287: private void checkClamp() {
288: // Apply the clamping values.
289: if (clampX != null) {
290: xLoc = clampX.getVal();
291: xVar.setVariableValue(xLoc);
292: } else if (clampY != null) {
293: yLoc = clampY.getVal();
294: yVar.setVariableValue(yLoc);
295: }
296: }
297:
298: /**
299: * This method is required by the InputObject interface. In this case, it just applies the
300: * clamping Values if any are specified.
301: */
302: public void checkInput() {
303: xVar.needsClamp = true;
304: yVar.needsClamp = true;
305: }
306:
307: /**
308: * This method, from the Drawable interface, draws the point. It is not usually called directly.
309: */
310: public void draw(Graphics g, boolean coordsChanged) {
311: if (coords == null)
312: return;
313: checkPosition();
314: if (useGhost)
315: g.setColor(getGhostColor());
316: else
317: g.setColor(color);
318: switch (style) {
319: case DISK:
320: g.fillOval(xPosition - radius, yPosition - radius,
321: 2 * radius + 1, 2 * radius + 1);
322: break;
323: case SQUARE:
324: g.fillRect(xPosition - radius, yPosition - radius,
325: 2 * radius + 1, 2 * radius + 1);
326: break;
327: case CROSS:
328: g.drawLine(xPosition - radius, yPosition, xPosition
329: + radius, yPosition);
330: g.drawLine(xPosition, yPosition - radius, xPosition,
331: yPosition + radius);
332: break;
333: }
334: }
335:
336: private void checkPosition() {
337: // compute (xPosition, yPosition), the position where point is actually drawn
338: useGhost = false;
339: xVar.getVal(); // Forces recompute, if needsClamp
340: yVar.getVal();
341: if (Double.isNaN(xLoc) || Double.isNaN(yLoc)) {
342: if (xPosition == -10000) { // otherwise, use previous position
343: xPosition = coords.getLeft() + coords.getWidth() / 2;
344: yPosition = coords.getTop() + coords.getHeight() / 2;
345: }
346: useGhost = true;
347: } else {
348: xPosition = coords.xToPixel(xLoc);
349: yPosition = coords.yToPixel(yLoc);
350: }
351: if (xPosition <= coords.getLeft()) {
352: useGhost = true;
353: xPosition = coords.getLeft() + 1;
354: } else if (xPosition >= coords.getLeft() + coords.getWidth()) {
355: useGhost = true;
356: xPosition = coords.getLeft() + coords.getWidth() - 1;
357: }
358: if (yPosition <= coords.getTop()) {
359: useGhost = true;
360: yPosition = coords.getTop() + 1;
361: } else if (yPosition >= coords.getTop() + coords.getHeight()) {
362: useGhost = true;
363: yPosition = coords.getTop() + coords.getHeight() - 1;
364: }
365: }
366:
367: //------------------ Dragging the point ---------------------------
368:
369: private boolean dragging; // True if the point is being dragged.
370:
371: /**
372: * Check whether a mouse click (as specified in the MouseEvent parameter) is a
373: * click on this DraggablePoint. If so, return true, and start a drag operation.
374: * It is expected that the continueDrag() and finishDrag() will be called to
375: * complete the drag operation. This is only meant to be called from
376: * the checkDraggables() method in class CoordinateRect.
377: */
378: public boolean startDrag(MouseEvent evt) {
379: dragging = false;
380: if (evt.isConsumed() || !getVisible() || coords == null)
381: return false;
382: checkPosition();
383: if (evt.getX() < xPosition - radius
384: || evt.getX() >= xPosition + radius
385: || evt.getY() < yPosition - radius
386: || evt.getY() >= yPosition + radius)
387: return false;
388: dragging = true;
389: evt.consume();
390: return true;
391: }
392:
393: /**
394: * Continue a drag operation begun in startDrag(). This is not meant to be called directly.
395: */
396: public void continueDrag(MouseEvent evt) {
397: if (!dragging)
398: return;
399: int xInt = evt.getX();
400: int yInt = evt.getY();
401: double x = coords.pixelToX(evt.getX());
402: double y = coords.pixelToY(evt.getY());
403: if (x < coords.getXmin())
404: x = coords.getXmin();
405: else if (x > coords.getXmax())
406: x = coords.getXmax();
407: if (y < coords.getYmin())
408: y = coords.getYmin();
409: else if (y > coords.getYmax())
410: y = coords.getYmax();
411: setLocation(x, y);
412: if (Double.isNaN(xLoc) || Double.isNaN(yLoc)) {
413: xPosition = xInt;
414: yPosition = yInt;
415: }
416: if (onUserAction != null)
417: onUserAction.compute();
418: }
419:
420: /**
421: * Finish a drag operation begun in startDrag(). This is not meant to be called directly.
422: */
423: public void finishDrag(MouseEvent evt) {
424: dragging = false;
425: }
426:
427: private class DPV extends Variable implements Tieable {
428:
429: private boolean isXVar; // True for xVar; false for yVar.
430:
431: long serialNumber; // This object's serial number.
432: boolean needsClamp; // Set to true by DraggablePoint().checkInput().
433:
434: DPV(boolean isXVar) {
435: // Create the variable.
436: super (isXVar ? "xDrag" : "yDrag");
437: this .isXVar = isXVar;
438: super .setVal(Double.NaN);
439: }
440:
441: public double getVal() {
442: // Return the value, after applying clamping, if necessary.
443: // (It's done this way because checkInput() can't use values of
444: // other objects, but after it's called, any call to getVal()
445: // should return the new correct value.)
446: if (needsClamp) {
447: if (isXVar) {
448: if (clampX != null) {
449: xLoc = clampX.getVal();
450: setVariableValue(xLoc);
451: }
452: } else {
453: if (clampY != null) {
454: yLoc = clampY.getVal();
455: setVariableValue(yLoc);
456: }
457: }
458: needsClamp = false;
459: }
460: return super .getVal();
461: }
462:
463: public void setVal(double val) {
464: // Set the value of the variable, and set the point's
465: // location to reflect new value. (setLocation ups serial number
466: // and calls setVariableValue() to set the actual variable value.)
467: if (isXVar)
468: setLocation(val, yVar.getVal());
469: else
470: setLocation(xVar.getVal(), val);
471: }
472:
473: void setVariableValue(double val) {
474: // Call the setVal() routine from the superclass.
475: super .setVal(val);
476: needsClamp = false;
477: }
478:
479: public long getSerialNumber() {
480: // Return this Tieable object's serial number.
481: return serialNumber;
482: }
483:
484: public void sync(Tie tie, Tieable newest) {
485: // Synchronize values and serial numbers with newest.
486: if (!(newest instanceof Value))
487: throw new IllegalArgumentException(
488: "Internal Error: A MouseTracker variable can only be tied to a Value object.");
489: if (newest != this ) {
490: setVal(((Value) newest).getVal());
491: serialNumber = newest.getSerialNumber();
492: }
493: }
494:
495: }
496:
497: } // end class DraggablePoint
|