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.functions;
024:
025: import java.awt.*;
026: import java.awt.event.*;
027: import edu.hws.jcm.data.*;
028: import edu.hws.jcm.awt.*;
029: import edu.hws.jcm.draw.*;
030:
031: /**
032: * A TableInputFunction is a Panel that can be used to define a TableFunction
033: * or to edit an existing TableFunction. To fetch the function currently
034: * displayed in the panel, call copyOfCurrentFunction(). To edit a function,
035: * call startEdit() to install the function. Then call cancelEdit() or
036: * finishEdit() to finish the editing. The panel displays both the graph
037: * of the function and a list of points that define the function. The user
038: * can drag the points on the graph up and down to change the y-values.
039: * Points can be added or modified by typing in their x- and y-coordinates.
040: * A point can be deleted by clicking it in the list of points and then
041: * clicking on the button labeled "Delete Point".
042: */
043: public class TableFunctionInput extends Panel implements ItemListener,
044: ActionListener, MouseListener, MouseMotionListener {
045:
046: private VariableInput xInput, yInput; // Components of the GUI.
047: private DisplayCanvas canvas;
048: private List pointList;
049: private Button clearButton, deleteButton, addButton;
050: private Checkbox[] styleCheckbox = new Checkbox[5];
051: private CheckboxGroup styleGroup;
052:
053: private Controller onChange; // If non-null, the compute() method of the controller
054: // is called when the user edits a point.
055:
056: private TableFunction function; // The function displayed.
057: private TableFunction editFunction; // If non-null, the function passed to startEdit.
058:
059: /**
060: * Create a TableFunctionInput panel. Initially, the function in the
061: * panel has no points and so is undefined everywhere. The panel needs
062: * to be fairly large!
063: */
064: public TableFunctionInput() {
065:
066: xInput = new VariableInput();
067: xInput.addActionListener(this );
068: yInput = new VariableInput();
069: yInput.addActionListener(this );
070:
071: pointList = new List();
072: pointList.setBackground(Color.white);
073: pointList.setFont(new Font("Monospaced", Font.PLAIN, 12));
074: pointList.addItemListener(this );
075:
076: clearButton = new Button("Remove All Points");
077: clearButton.addActionListener(this );
078: deleteButton = new Button("Delete Point");
079: deleteButton.setEnabled(false);
080: deleteButton.addActionListener(this );
081: addButton = new Button("Add/Modify Point");
082: addButton.addActionListener(this );
083:
084: styleGroup = new CheckboxGroup();
085: styleCheckbox[0] = new Checkbox("Smooth", true, styleGroup);
086: styleCheckbox[1] = new Checkbox("Piecewise Linear", false,
087: styleGroup);
088: styleCheckbox[2] = new Checkbox("Step (nearest value)", false,
089: styleGroup);
090: styleCheckbox[3] = new Checkbox("Step (value from left)",
091: false, styleGroup);
092: styleCheckbox[4] = new Checkbox("Step (value from right)",
093: false, styleGroup);
094: for (int i = 0; i < 5; i++)
095: styleCheckbox[i].addItemListener(this );
096:
097: canvas = new DisplayCanvas(new CoordinateRect(-1, 1, -1, 1));
098: canvas.add(new Axes());
099: canvas.addMouseListener(this );
100: canvas.addMouseMotionListener(this );
101:
102: function = new TableFunction();
103: canvas.add(new Draw());
104:
105: Font labelFont = new Font("Serif", Font.BOLD, 12);
106: Label lab1 = new Label("Input Area");
107: lab1.setForeground(Color.red);
108: lab1.setFont(labelFont);
109: Label lab2 = new Label("Type of Function", Label.CENTER);
110: lab2.setForeground(Color.red);
111: lab2.setFont(labelFont);
112: Label lab3 = new Label("Table of Values", Label.CENTER);
113: lab3.setForeground(Color.red);
114: lab3.setFont(labelFont);
115:
116: Panel topLeft = new Panel();
117: topLeft.setLayout(new FlowLayout(FlowLayout.CENTER, 10000, 3));
118: Panel topRight = new Panel();
119: topRight.setLayout(new GridLayout(6, 1, 3, 3));
120: Panel bottomLeft = new Panel();
121: bottomLeft.setLayout(new BorderLayout());
122: Panel top = new Panel();
123: top.setLayout(new BorderLayout(3, 3));
124: top.add(topLeft, BorderLayout.CENTER);
125: top.add(topRight, BorderLayout.EAST);
126: setLayout(new BorderLayout(3, 3));
127: add(top, BorderLayout.NORTH);
128: add(bottomLeft, BorderLayout.WEST);
129: add(canvas, BorderLayout.CENTER);
130: setBackground(Color.darkGray);
131: topLeft.setBackground(Color.lightGray);
132: topRight.setBackground(Color.lightGray);
133: bottomLeft.setBackground(Color.lightGray);
134:
135: Panel inputBar = new Panel();
136: inputBar.add(new Label("x = "));
137: inputBar.add(xInput);
138: inputBar.add(new Label(" y = "));
139: inputBar.add(yInput);
140:
141: Panel buttonBar = new Panel();
142: buttonBar.setLayout(new GridLayout(1, 2, 3, 3));
143: buttonBar.add(deleteButton);
144: buttonBar.add(clearButton);
145:
146: topLeft.add(lab1);
147: topLeft.add(inputBar);
148: topLeft.add(addButton);
149: topLeft.add(new Label("(Press RETURN in X to move to Y)"));
150: topLeft
151: .add(new Label(
152: "(Press RETURN in Y to add/modify point)"));
153:
154: topRight.add(lab2);
155: for (int i = 0; i < 5; i++)
156: topRight.add(styleCheckbox[i]);
157:
158: bottomLeft.add(pointList, BorderLayout.CENTER);
159: bottomLeft.add(lab3, BorderLayout.NORTH);
160: bottomLeft.add(buttonBar, BorderLayout.SOUTH);
161:
162: } // end constructor
163:
164: //------------------- Methods for modifying an existing function --------------
165:
166: /**
167: * Install a function to be edited. The data from the function is
168: * copied into the panel, and a pointer to the function is retained.
169: * Note that the function itself is not changed by any editing that
170: * the user does. To commit the changes made by the user to the
171: * actual function, you must call finishEdit(). If f is null,
172: * the effect is to start with a new, empty function.
173: */
174: public void startEdit(TableFunction f) {
175: editFunction = f;
176: revertEditFunction(); // Installs data from function.
177: }
178:
179: /**
180: * If a function has been specified using startEdit(), and neither
181: * finishEdit() nor cancelEdit have been called, then this method
182: * will discard the current data in and replace it with data from
183: * the edit function. If there is no edit function, then the data
184: * is simply discarded. (That is, the data reverts to an empty point list.)
185: */
186: public void revertEditFunction() {
187: if (editFunction == null) {
188: clearAllPoints();
189: return;
190: }
191: function.copyDataFrom(editFunction);
192: pointList.removeAll();
193: int pointCt = function.getPointCount();
194: for (int i = 0; i < pointCt; i++)
195: pointList.add(makePointString(function.getX(i), function
196: .getY(i)));
197: styleGroup.setSelectedCheckbox(styleCheckbox[function
198: .getStyle()]);
199: checkCanvas();
200: if (onChange != null)
201: onChange.compute();
202: }
203:
204: /**
205: * If an edit function has been specified (by startEdit()), this function copies the
206: * data form the TableFunctionInput into that function, and returns a
207: * pointer to that function. This ends the edit session, and the internally stored
208: * pointer to the edit function is discarded. If no edit function has been specified
209: * then a new TableFunction is created with the data from the panel, and a
210: * pointer to the new function is returned. This does not clear the data
211: * in the TableFunctionInput panel.
212: */
213: public TableFunction finishEdit() {
214: TableFunction func;
215: if (editFunction == null)
216: func = copyOfCurrentFunction();
217: else {
218: editFunction.copyDataFrom(function);
219: func = editFunction;
220: editFunction = null;
221: }
222: return func;
223: }
224:
225: /**
226: * Discards the internal pointer to the edit function (specified by startEdit()),
227: * if any. This does not clear the data in the TableFunctionInput panel.
228: */
229: public void cancelEdit() {
230: editFunction = null;
231: }
232:
233: /**
234: * Create a new TableFunction containing the data that is currently
235: * in the TableFunctionInput panel, and return a pointer to that new function.
236: */
237: public TableFunction copyOfCurrentFunction() {
238: TableFunction copy = new TableFunction();
239: copy.copyDataFrom(function);
240: copy.setName(function.getName());
241: return copy;
242: }
243:
244: /**
245: * Specify a controller whose compute() method will be called whenever
246: * the user edits the data in this TableFunctionInput panel. (Note that
247: * when the user edits the function by dragging a point, the Controller
248: * is only called once at the end of the drag.) If the specified
249: * Controller is null, then no notification takes place.
250: */
251: public void setOnChange(Controller c) {
252: onChange = c;
253: }
254:
255: /**
256: * Get the Controller that is notified when the user edits the data
257: * in this panel. The return value can be null (the default), indicating
258: * that no notification takes place.
259: */
260: public Controller getOnChange() {
261: return onChange;
262: }
263:
264: private void deletePoint() {
265: // delete point selected in the point list, if any
266: int index = pointList.getSelectedIndex();
267: if (index >= 0) {
268: pointList.remove(index);
269: function.removePointAt(index);
270: checkCanvas();
271: if (onChange != null)
272: onChange.compute();
273: }
274: deleteButton.setEnabled(false);
275: }
276:
277: private void clearAllPoints() {
278: // remove all points, leaving an empty point list
279: function.removeAllPoints();
280: pointList.removeAll();
281: deleteButton.setEnabled(false);
282: if (onChange != null)
283: onChange.compute();
284: checkCanvas();
285: }
286:
287: private void addPoint() {
288: // add the point whose coords are specified in the x and y input boxes;
289: // if the x-value already exits, then the corresponding y-value is changed.
290: double x, y;
291: try {
292: xInput.checkInput();
293: x = xInput.getVal();
294: } catch (JCMError e) {
295: canvas.setErrorMessage(null,
296: "The input for x does is not a legal real number.");
297: xInput.requestFocus();
298: xInput.selectAll();
299: return;
300: }
301: try {
302: yInput.checkInput();
303: y = yInput.getVal();
304: } catch (JCMError e) {
305: canvas.setErrorMessage(null,
306: "The input for y does is not a legal real number.");
307: yInput.requestFocus();
308: yInput.selectAll();
309: return;
310: }
311: String str = makePointString(x, y);
312: int index = function.findPoint(x);
313: if (index >= 0 && y == function.getY(index)) {
314: xInput.requestFocus();
315: xInput.selectAll();
316: return;
317: }
318: int newindex = function.addPoint(x, y);
319: if (index >= 0)
320: pointList.replaceItem(str, index);
321: else
322: pointList.addItem(str, newindex);
323: deleteButton.setEnabled(pointList.getSelectedIndex() != -1);
324: checkCanvas();
325: if (onChange != null)
326: onChange.compute();
327: xInput.requestFocus();
328: xInput.selectAll();
329: }
330:
331: private String makePointString(double x, double y) {
332: // Make a string representing (x,y), using exactly 11 spaces for each number.
333: String X = NumUtils.realToString(x);
334: String Y = NumUtils.realToString(y);
335: if (X.length() < 11)
336: X = " ".substring(0, 11 - X.length()) + X;
337: if (Y.length() < 11)
338: Y = " ".substring(0, 11 - Y.length()) + Y;
339: return X + " " + Y;
340: }
341:
342: private void selectPoint() {
343: // React when user selects a point in the list of points.
344: int index = pointList.getSelectedIndex();
345: if (index >= 0) {
346: xInput.setVal(function.getX(index));
347: yInput.setVal(function.getY(index));
348: yInput.requestFocus();
349: yInput.selectAll();
350: }
351: deleteButton.setEnabled(index >= 0);
352: }
353:
354: private void changeStyle() {
355: // React when user changes style of function.
356: int newstyle = 0;
357: Checkbox selected = styleGroup.getSelectedCheckbox();
358: for (int i = 1; i < 5; i++)
359: if (selected == styleCheckbox[i])
360: newstyle = i;
361: if (function.getStyle() == newstyle)
362: return;
363: function.setStyle(newstyle);
364: canvas.doRedraw();
365: if (onChange != null)
366: onChange.compute();
367: }
368:
369: private void checkCanvas() {
370: // Adjust limits on canvas, if necessary, and redraw it.
371: int ct = function.getPointCount();
372: double newXmin = -1, newXmax = 1, newYmin = -1, newYmax = 1;
373: if (ct > 0) {
374: if (ct == 1) {
375: newXmin = function.getX(0);
376: if (Math.abs(newXmin) < 10000) {
377: newXmax = newXmin + 1;
378: newXmin -= 1;
379: } else {
380: newXmax = newXmin - Math.abs(newXmin) / 10;
381: newXmin -= Math.abs(newXmin) / 10;
382: }
383: } else {
384: newXmin = function.getX(0);
385: newXmax = function.getX(ct - 1);
386: }
387: newYmin = function.getY(0);
388: newYmax = newYmin;
389: for (int i = 1; i < ct; i++) {
390: double y = function.getY(i);
391: if (y < newYmin)
392: newYmin = y;
393: else if (y > newYmax)
394: newYmax = y;
395: }
396: double size = Math.abs(newYmin - newYmax);
397: if (size < 1e-10 && Math.abs(newYmin) < 10000
398: && Math.abs(newYmax) < 10000) {
399: newYmax += 1;
400: newYmin -= 1;
401: } else {
402: newYmax += size * 0.15;
403: newYmin -= size * 0.15;
404: }
405: }
406: CoordinateRect coords = canvas.getCoordinateRect(0);
407: double curSize = Math.abs(coords.getYmin() - coords.getYmax());
408: double newSize = Math.abs(newYmax - newYmin);
409: if (newXmax != coords.getXmax() || newXmin != coords.getXmin()
410: || newSize > 1.3 * curSize || newSize < 0.5 * curSize
411: || newYmax > coords.getYmax() - 0.1 * curSize
412: || newYmin < coords.getYmin() + 0.1 * curSize)
413: coords.setLimits(newXmin, newXmax, newYmin, newYmax);
414: canvas.doRedraw();
415: }
416:
417: /**
418: * Leave a 3-pixel gap around the edges of the panel. Not meant to be called directly.
419: */
420: public Insets getInsets() {
421: return new Insets(3, 3, 3, 3);
422: }
423:
424: /**
425: * React when user clicks one of the buttons or presses return in one
426: * of the input boxes. Not meant to be called directly.
427: */
428: public void actionPerformed(ActionEvent evt) {
429: Object source = evt.getSource();
430: if (source == deleteButton)
431: deletePoint();
432: else if (source == clearButton)
433: clearAllPoints();
434: else if (source == xInput) {
435: yInput.requestFocus();
436: yInput.selectAll();
437: } else
438: addPoint();
439: }
440:
441: /**
442: * React when user clicks on a point in the list of points or clicks one
443: * of the radio buttons for specifying the style of the function.
444: * Not meant to be called directly.
445: */
446: public void itemStateChanged(ItemEvent evt) {
447: if (evt.getSource() == pointList)
448: selectPoint();
449: else
450: changeStyle();
451: }
452:
453: //-------------------- Dragging --------------------------
454:
455: private int dragPoint = -1; // -1 if no point is being dragged;
456: // Otherwise, the index of the point being dragged.
457:
458: private int startX, startY; // Point where mouse was clicked at start of drag.
459:
460: private int prevY; // Previous position of mouse during dragging.
461:
462: private boolean moved; // Becomes true once the clicked point has actually
463:
464: // been dragged a bit. If the mouse is released before
465: // the point is moved at least 3 pixels, then the associated
466: // y-value is not changed.
467:
468: /**
469: * Method required by the MouseListener interface. Defined here to
470: * support dragging of points on the function's graph. Not meant to be called directly.
471: */
472: public void mousePressed(MouseEvent evt) {
473: dragPoint = -1;
474: moved = false;
475: int ct = function.getPointCount();
476: CoordinateRect coords = canvas.getCoordinateRect(0);
477: for (int i = 0; i < ct; i++) {
478: int x = coords.xToPixel(function.getX(i));
479: int y = coords.yToPixel(function.getY(i));
480: if (evt.getX() >= x - 3 && evt.getX() <= x + 3
481: && evt.getY() >= y - 3 && evt.getY() <= y + 3) {
482: startX = evt.getX();
483: prevY = startY = evt.getY();
484: pointList.deselect(pointList.getSelectedIndex());
485: pointList.select(i);
486: selectPoint();
487: dragPoint = i;
488: return;
489: }
490: }
491: }
492:
493: /**
494: * Method required by the MouseListener interface. Defined here to
495: * support dragging of points on the function's graph. Not meant to be called directly.
496: */
497: public void mouseReleased(MouseEvent evt) {
498: if (dragPoint == -1)
499: return;
500: if (!moved) {
501: dragPoint = -1;
502: return;
503: }
504: mouseDragged(evt);
505: pointList.replaceItem(makePointString(function.getX(dragPoint),
506: function.getY(dragPoint)), dragPoint);
507: pointList.select(dragPoint);
508: dragPoint = -1;
509: if (onChange != null)
510: onChange.compute();
511: }
512:
513: /**
514: * Method required by the MouseListener interface. Defined here to
515: * support dragging of points on the function's graph. Not meant to be called directly.
516: */
517: public void mouseDragged(MouseEvent evt) {
518: if (dragPoint == -1 || prevY == evt.getY())
519: return;
520: if (!moved && Math.abs(evt.getY() - startY) < 3)
521: return;
522: moved = true;
523: int y = evt.getY();
524: CoordinateRect coords = canvas.getCoordinateRect(0);
525: if (y < coords.getTop() + 4)
526: y = coords.getTop() + 4;
527: else if (y > coords.getTop() + coords.getHeight() - 4)
528: y = coords.getTop() + coords.getHeight() - 4;
529: if (Math.abs(evt.getX() - startX) > 72)
530: y = startY;
531: if (y == prevY)
532: return;
533: prevY = y;
534: function.setY(dragPoint, coords.pixelToY(prevY));
535: yInput.setVal(function.getY(dragPoint));
536: canvas.doRedraw();
537: }
538:
539: /**
540: * Empty method, required by the MouseListener interface.
541: */
542: public void mouseClicked(MouseEvent evt) {
543: }
544:
545: /**
546: * Empty method, required by the MouseMotionListener interface.
547: */
548: public void mouseEntered(MouseEvent evt) {
549: }
550:
551: /**
552: * Empty method, required by the MouseMotionListener interface.
553: */
554: public void mouseExited(MouseEvent evt) {
555: }
556:
557: /**
558: * Empty method, required by the MouseMotionListener interface.
559: */
560: public void mouseMoved(MouseEvent evt) {
561: }
562:
563: //--------------------------------------------------------
564:
565: private class Draw extends Drawable {
566: // An object of this nested class is added to the canvas
567: // of the TableFunctionInput panel. It is responsible for
568: // drawing the function.
569:
570: public void draw(Graphics g, boolean coordsChanged) {
571: int ct = function.getPointCount();
572: if (ct == 0)
573: return;
574: g.setColor(Color.magenta);
575: int xInt, yInt, aInt, bInt;
576: double x, y, a, b;
577: switch (function.getStyle()) {
578: case TableFunction.SMOOTH: {
579: if (ct > 1) {
580: try {
581: x = function.getX(0);
582: y = function.getVal(x);
583: xInt = coords.xToPixel(x);
584: yInt = coords.yToPixel(y);
585: int limit = coords.xToPixel(function
586: .getX(ct - 1));
587: aInt = xInt;
588: while (aInt < limit) {
589: aInt += 3;
590: if (aInt > limit)
591: aInt = limit;
592: a = coords.pixelToX(aInt);
593: b = function.getVal(a);
594: bInt = coords.yToPixel(b);
595: g.drawLine(xInt, yInt, aInt, bInt);
596: xInt = aInt;
597: yInt = bInt;
598: }
599: } catch (Exception e) {
600: e.printStackTrace();
601: }
602: }
603: break;
604: }
605: case TableFunction.PIECEWISE_LINEAR: {
606: x = function.getX(0);
607: xInt = coords.xToPixel(x);
608: y = function.getY(0);
609: yInt = coords.yToPixel(y);
610: for (int i = 1; i < ct; i++) {
611: a = function.getX(i);
612: aInt = coords.xToPixel(a);
613: b = function.getY(i);
614: bInt = coords.yToPixel(b);
615: g.drawLine(xInt, yInt, aInt, bInt);
616: xInt = aInt;
617: yInt = bInt;
618: }
619: break;
620: }
621: case TableFunction.STEP: {
622: x = function.getX(0);
623: xInt = coords.xToPixel(x);
624: for (int i = 0; i < ct; i++) {
625: if (i < ct - 1) {
626: double nextX = function.getX(i + 1);
627: a = (x + nextX) / 2;
628: x = nextX;
629: } else
630: a = x;
631: aInt = coords.xToPixel(a);
632: y = function.getY(i);
633: yInt = coords.yToPixel(y);
634: g.drawLine(xInt, yInt, aInt, yInt);
635: xInt = aInt;
636: }
637: break;
638: }
639: case TableFunction.STEP_LEFT: {
640: x = function.getX(0);
641: xInt = coords.xToPixel(x);
642: for (int i = 1; i < ct; i++) {
643: a = function.getX(i);
644: aInt = coords.xToPixel(a);
645: y = function.getY(i - 1);
646: yInt = coords.yToPixel(y);
647: g.drawLine(xInt, yInt, aInt, yInt);
648: xInt = aInt;
649: }
650: break;
651: }
652: case TableFunction.STEP_RIGHT: {
653: x = function.getX(0);
654: xInt = coords.xToPixel(x);
655: for (int i = 1; i < ct; i++) {
656: a = function.getX(i);
657: aInt = coords.xToPixel(a);
658: y = function.getY(i);
659: yInt = coords.yToPixel(y);
660: g.drawLine(xInt, yInt, aInt, yInt);
661: xInt = aInt;
662: }
663: break;
664: }
665: }
666: for (int i = 0; i < ct; i++) {
667: x = function.getX(i);
668: y = function.getY(i);
669: xInt = coords.xToPixel(x);
670: yInt = coords.yToPixel(y);
671: g.fillOval(xInt - 2, yInt - 2, 5, 5);
672: }
673: }
674:
675: }
676:
677: }
|