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 edu.hws.jcm.data.*;
026: import edu.hws.jcm.awt.*;
027: import java.awt.*;
028: import java.util.Vector;
029:
030: /**
031: * A ParametricCurve is defined by two functions, x(t) and y(t) of a variable, t,
032: * for t in a specified interval. The curve is simply defined as a sequence of line
033: * segments connecting points of the form (x(t),y(t)), except where one of the functions
034: * is undefined. Also, in some cases a
035: * discontinuity will be detected and no line will be drawn between two of the points.
036: */
037:
038: public class ParametricCurve extends Drawable implements Computable {
039:
040: private Function xFunc, yFunc; //The functions of t that are graphed.
041:
042: private Color graphColor = Color.magenta; //Color of the graph.
043:
044: private boolean changed; // Used internally to indicate that data has to be recomputed.
045:
046: private transient int[] xcoord, ycoord; //points on graph; xcoord[i] == Integer.MIN_VALUE
047: //for points where a gap occurs.
048:
049: private Value tmin, tmax; // Value objects giving the minimum and maximum value of t.
050:
051: private Value intervals; // Value object giving the number of intervals into which the
052: // interval (tmin,tmax) is to be divided.
053:
054: private double tmin_val, tmax_val; // The values of tmin and tmax.
055: // (tmin_val is set to Double.NaN if any of the values are bad, and nothing is drawn.)
056: private int intervals_val; // The value of intervals.
057:
058: /**
059: * Create a ParametricCurve with nothing to graph. The functions and other values
060: * can be set later.
061: */
062: public ParametricCurve() {
063: this (null, null, null, null, null);
064: }
065:
066: /**
067: * Create a parametric curve with x and y coordinates given by the specified functions
068: * of the parameter t. Defaults values are used for tmin, tmax, and the number of intervals.
069: * If either function is null, nothing is drawn.
070: */
071: public ParametricCurve(Function xFunc, Function yFunc) {
072: this (xFunc, yFunc, null, null, null);
073: }
074:
075: /**
076: * Create a parametric curve with the specified values.
077: *
078: * @param xFunc A Function of one variable giving the x-coordinate of points on the curve. If this
079: * is null, then nothing will be drawn.
080: * @param yFunc A Function of one variable giving the y-coordinate of points on the curve. If this
081: * is null, then nothing will be drawn.
082: * @param tmin A Value object giving one endpoint of the domain of the parameter. If this is null,
083: * the default value -5 is used.
084: * @param tmax A Value object giving the second endpoint of the domain of the parameter. If this is null,
085: * the default value 5 is used. Note that it is not required that tmax be greater than tmin.
086: * @param intervals A Value object giving the number of intervals into which the domain is subdivided.
087: * If this is null, the default value 200 is used. The number of points on the curve will be
088: * the number of intervals plus one (unless a function is undefined at some value of the parameter
089: * or if a discontinuity is detected). The number of intervals is clamped to the range 1 to 10000.
090: * Values outside this range would certainly be unreasonable.
091: */
092: public ParametricCurve(Function xFunc, Function yFunc, Value tmin,
093: Value tmax, Value intevals) {
094: if ((xFunc != null && xFunc.getArity() != 1)
095: || (yFunc != null && yFunc.getArity() != 1))
096: throw new IllegalArgumentException(
097: "Internal Error: The functions that define a parametric curve must be functions of one variable.");
098: this .xFunc = xFunc;
099: this .yFunc = yFunc;
100: this .tmin = tmin;
101: this .tmax = tmax;
102: this .intervals = intervals;
103: changed = true;
104: }
105:
106: /**
107: * Set the color to be used for drawing the graph.
108: */
109: public void setColor(Color c) {
110: if (c != null & !c.equals(graphColor)) {
111: graphColor = c;
112: needsRedraw();
113: }
114: }
115:
116: /**
117: * Get the color that is used to draw the graph.
118: */
119: public Color getColor() {
120: return graphColor;
121: }
122:
123: /**
124: * Sets the functions that gives the coordinates of the curve to be graphed. If either function is
125: * null, then nothing is drawn. If non-null, each function must be a function of one variable.
126: */
127: synchronized public void setFunctions(Function x, Function y) {
128: setXFunction(x);
129: setYFunction(y);
130: }
131:
132: /**
133: * Set the function that gives the x-coordinate of the curve to be graphed. If this is
134: * null, then nothing is drawn. If non-null, it must be a function of one variable.
135: */
136: synchronized public void setXFunction(Function x) {
137: if (x != null && x.getArity() != 1)
138: throw new IllegalArgumentException(
139: "Internal Error: ParametricCurve can only graph functions of one variable.");
140: if (x != xFunc) {
141: xFunc = x;
142: changed = true;
143: needsRedraw();
144: }
145: }
146:
147: /**
148: * Set the function that gives the y-coordinate of the curve to be graphed. If this is
149: * null, then nothing is drawn. If non-null, it must be a function of one variable.
150: */
151: synchronized public void setYFunction(Function y) {
152: if (y != null && y.getArity() != 1)
153: throw new IllegalArgumentException(
154: "Internal Error: ParametricCurve can only graph functions of one variable.");
155: if (y != yFunc) {
156: yFunc = y;
157: changed = true;
158: needsRedraw();
159: }
160: }
161:
162: /**
163: * Get the (possibly null) function that gives the x-coordinate of the curve.
164: */
165: public Function getXFunction() {
166: return xFunc;
167: }
168:
169: /**
170: * Get the (possibly null) function that gives the y-coordinate of the curve.
171: */
172: public Function getYFunction() {
173: return yFunc;
174: }
175:
176: /**
177: * Specify the number of subintervals into which the domain of the parametric curve is divided.
178: * The interval (tmin,tmax) is divided into subintervals. X and y coordinates of the parametric curve
179: * are computed at each endpoint of these subintervals, and then the points are connected by lines.
180: * If the parameter of this function is null, or if no interval count is ever specified, then a
181: * default value of 200 is used.
182: */
183: public void setIntervals(Value intervalCount) {
184: intervals = intervalCount;
185: changed = true;
186: }
187:
188: /**
189: * Get the value object, possibly null, that determines the number of points on the curve.
190: */
191: public Value getIntervals() {
192: return intervals;
193: }
194:
195: /**
196: * Set the Value objects that specify the domain of the paratmeter.
197: */
198: public void setLimits(Value tmin, Value tmax) {
199: setTMin(tmin);
200: setTMax(tmax);
201: }
202:
203: /**
204: * Get the Value object, possibly null, that gives the left endpoint of the domain of the parameter.
205: */
206: public Value getTMin() {
207: return tmin;
208: }
209:
210: /**
211: * Get the Value object, possibly null, that gives the right endpoint of the domain of the parameter.
212: */
213: public Value getTMax() {
214: return tmax;
215: }
216:
217: /**
218: * Set the Value object that gives the left endpoint of the domain of the parameter. If this is null,
219: * then a default value of -5 is used for the left endpoint. (Note: actually, it's not required that
220: * tmin be less than tmax, so this might really be the "right" endpoint.)
221: */
222: public void setTMin(Value tmin) {
223: this .tmin = tmin;
224: changed = true;
225: }
226:
227: /**
228: * Set the Value object that gives the right endpoint of the domain of the parameter. If this is null,
229: * then a default value of 5 is used for the right endpoint. (Note: actually, it's not required that
230: * tmin be less than tmax, so this might really be the "left" endpoint.)
231: */
232: public void setTMax(Value tmax) {
233: this .tmax = tmax;
234: changed = false;
235: }
236:
237: //------------------ Implementation details -----------------------------
238:
239: /**
240: * Recompute data for the graph and make sure that the area of the display canvas
241: * that shows the graph is redrawn. This method is ordinarily called by a
242: * Controller.
243: */
244: synchronized public void compute() {
245: setup();
246: needsRedraw();
247: changed = false;
248: }
249:
250: /**
251: * Draw the graph (possibly recomputing the data if the CoordinateRect has changed).
252: * This is not usually called directly.
253: *
254: */
255: synchronized public void draw(Graphics g, boolean coordsChanged) {
256: if (changed || coordsChanged || xcoord == null
257: || ycoord == null) {
258: setup();
259: changed = false;
260: }
261: if (xcoord == null || xcoord.length == 0)
262: return;
263: g.setColor(graphColor);
264: int x = xcoord[0];
265: int y = ycoord[0];
266: for (int i = 1; i < xcoord.length; i++) {
267: if (xcoord[i] == Integer.MIN_VALUE) {
268: do {
269: i++;
270: } while (i < xcoord.length
271: && xcoord[i] == Integer.MIN_VALUE);
272: if (i < xcoord.length) {
273: x = xcoord[i];
274: y = ycoord[i];
275: }
276: } else {
277: int x2 = xcoord[i];
278: int y2 = ycoord[i];
279: g.drawLine(x, y, x2, y2);
280: x = x2;
281: y = y2;
282: }
283: }
284: }
285:
286: // ------------------------- Computing the points on the graph -----------------------
287:
288: private double[] v = new double[1];
289: private Cases case1x = new Cases();
290: private Cases case2x = new Cases();
291: private Cases case1y = new Cases();
292: private Cases case2y = new Cases();
293: private Cases case3x = new Cases();
294: private Cases case3y = new Cases();
295:
296: private Vector points = new Vector(250);
297:
298: private Point eval(double t, Cases xcases, Cases ycases) {
299: v[0] = t;
300: if (xcases != null)
301: xcases.clear();
302: if (ycases != null)
303: ycases.clear();
304: double x = xFunc.getValueWithCases(v, xcases);
305: double y = yFunc.getValueWithCases(v, ycases);
306: if (Double.isNaN(x) || Double.isNaN(y))
307: return null;
308: int xInt = coords.xToPixel(x);
309: int yInt = coords.yToPixel(y);
310: if (Math.abs(xInt) > 10000 || Math.abs(yInt) > 10000)
311: return null;
312: return new Point(xInt, yInt);
313: }
314:
315: private void setup() {
316: if (xFunc == null || yFunc == null || coords == null) {
317: xcoord = ycoord = new int[0]; // Nothing will be drawn
318: return;
319: }
320: double intervals_val_d;
321: if (tmin == null)
322: tmin_val = -5;
323: else
324: tmin_val = tmin.getVal();
325: if (tmax == null)
326: tmax_val = 5;
327: else
328: tmax_val = tmax.getVal();
329: if (intervals == null)
330: intervals_val_d = 200;
331: else
332: intervals_val_d = intervals.getVal();
333: if (Double.isInfinite(tmin_val) || Double.isInfinite(tmax_val)
334: || Double.isInfinite(intervals_val_d)
335: || Double.isNaN(tmax_val)
336: || Double.isNaN(intervals_val_d))
337: tmin_val = Double.NaN; // Signal that data is bad, so nothing will be drawn.
338: if (intervals_val_d < 1)
339: intervals_val = 1;
340: else if (intervals_val > 10000)
341: intervals_val = 10000;
342: else
343: intervals_val = (int) Math.round(intervals_val_d);
344: if (Double.isNaN(tmin_val)) { // data is bad, don't draw
345: xcoord = ycoord = new int[0];
346: return;
347: }
348:
349: points.setSize(0);
350:
351: double delta = (tmax_val - tmin_val) / intervals_val;
352: double prevx, prevy, x, y, lastT;
353: Point point, prevpoint;
354:
355: double t = tmin_val;
356: prevpoint = eval(t, case1x, case1y);
357: if (prevpoint != null)
358: points.addElement(prevpoint);
359:
360: for (int i = 1; i <= intervals_val; i++) {
361: t = tmin_val + i * delta;
362: point = eval(t, case2x, case2y);
363: if (point != null && prevpoint != null) {
364: if (!case1x.equals(case2x) || !case1y.equals(case2y))
365: // A discontinuity between two "onscreen" points.
366: discontinuity(prevpoint,
367: tmin_val + (i - 1) * delta, point, t, 0);
368: else
369: points.addElement(point);
370: } else if (prevpoint == null && point != null) {
371: becomesDefined(prevpoint, tmin_val + (i - 1) * delta,
372: point, t, 0);
373: } else if (prevpoint != null && point == null) {
374: becomesUndefined(prevpoint, tmin_val + (i - 1) * delta,
375: point, t, 0);
376: }
377:
378: prevpoint = point;
379: Cases temp = case1x;
380: case1x = case2x;
381: case2x = temp;
382: temp = case1y;
383: case1y = case2y;
384: case2y = temp;
385:
386: } // end for
387:
388: xcoord = new int[points.size()];
389: ycoord = new int[points.size()];
390: for (int i = 0; i < ycoord.length; i++) {
391: Point p = (Point) points.elementAt(i);
392: xcoord[i] = p.x;
393: ycoord[i] = p.y;
394: }
395:
396: } // end setup();
397:
398: private static int MAXDEPTH = 10; // maximum depth of recursion in the next three methods.
399:
400: void discontinuity(Point p1, double t1, Point p2, double t2,
401: int depth) {
402: // Both p1 and p2 are non-null; "cases" data at these two points does not agree;
403: // Original point p1 (from case depth=1) is in points vector. Case data for p1 and p2
404: // is contained in case1x,case1y and case2x,case2y respectively.
405: if (depth >= MAXDEPTH
406: || (Math.abs(p1.x - p2.x) < 2 && Math.abs(p1.y - p2.y) < 2)) {
407: if (points.elementAt(points.size() - 1) != p1)
408: points.addElement(p1);
409: if (depth >= MAXDEPTH)
410: points.addElement(new Point(Integer.MIN_VALUE, 0));
411: points.addElement(p2);
412: return;
413: }
414: double t = (t1 + t2) / 2;
415: Point p = eval(t, case3x, case3y);
416: if (p == null) {
417: becomesUndefined(p1, t1, p, t, depth + 1);
418: becomesDefined(p, t, p2, t2, depth + 1);
419: } else if (case3x.equals(case1x) && case3y.equals(case1y)) {
420: discontinuity(p, t, p2, t2, depth + 1);
421: } else if (case3x.equals(case2x) && case3y.equals(case2y)) {
422: discontinuity(p1, t1, p, t, depth + 1);
423: } else {
424: discontinuity(p1, t1, p, t, depth + 2);
425: discontinuity(p, t, p2, t2, depth + 2);
426: }
427: }
428:
429: void becomesUndefined(Point p1, double t1, Point p2, double t2,
430: int depth) {
431: // p1 is non-null; p2 is null. Original point p1 is in points vector.
432: if (depth >= MAXDEPTH) {
433: if (points.elementAt(points.size() - 1) != p1)
434: points.addElement(p1);
435: points.addElement(new Point(Integer.MIN_VALUE, 0));
436: return;
437: }
438: double t = (t1 + t2) / 2;
439: Point p = eval(t, null, null);
440: if (p == null)
441: becomesUndefined(p1, t1, p, t, depth + 1);
442: else
443: becomesUndefined(p, t, p2, t2, depth + 1);
444: }
445:
446: void becomesDefined(Point p1, double t1, Point p2, double t2,
447: int depth) {
448: // p1 is null; p2 is non-null
449: if (depth >= MAXDEPTH) {
450: if (points.size() > 0)
451: points.addElement(new Point(Integer.MIN_VALUE, 0));
452: points.addElement(p2);
453: return;
454: }
455: double t = (t1 + t2) / 2;
456: Point p = eval(t, null, null);
457: if (p != null)
458: becomesDefined(p1, t1, p, t, depth + 1);
459: else
460: becomesDefined(p, t, p2, t2, depth + 1);
461: }
462:
463: } // end class ParametricCurve
|