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 Graph1D represents the graph of a function of one variable, to be
032: * displayed in a given CoordinateRect. A Graph1D is a Computable.
033: * The data for the graph is recomputed when its compute() method is
034: * called. It will also be recomputed, before it is drawn, if the
035: * coordinate rect has changed in some way.
036: */
037:
038: public class Graph1D extends Drawable implements Computable {
039:
040: private Function func; //The function that is 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:
048: //for points where function is undefined.
049:
050: /**
051: * Create a Graph1D with no function to graph. One can be set
052: * later with setFunction();
053: */
054: public Graph1D() {
055: }
056:
057: /**
058: * Create a graph of the specified function.
059: *
060: * @param func The function to be graphed. If func is null, nothing is drawn.
061: * If func is non-null, it must be a function of one variable.
062: */
063: public Graph1D(Function func) {
064: setFunction(func);
065: }
066:
067: /**
068: * Set the color to be used for drawing the graph. The default color is magenta.
069: */
070: public void setColor(Color c) {
071: if (c != null & !c.equals(graphColor)) {
072: graphColor = c;
073: needsRedraw();
074: }
075: }
076:
077: /**
078: * Get the color that is used to draw the graph.
079: */
080: public Color getColor() {
081: return graphColor;
082: }
083:
084: /**
085: * Set the function to be graphed. If it is null, nothing is drawn.
086: * If it is non-null, it must be a function of one variable, or an error will occur.
087: *
088: */
089: synchronized public void setFunction(Function f) {
090: if (f != null && f.getArity() != 1)
091: throw new IllegalArgumentException(
092: "Internal Error: Graph1D can only graph a function of one variable.");
093: if (f != func) {
094: func = f;
095: changed = true;
096: needsRedraw();
097: }
098: }
099:
100: /**
101: * Get the (possibly null) function whose graph is drawn.
102: */
103: public Function getFunction() {
104: return func;
105: }
106:
107: //------------------ Implementation details -----------------------------
108:
109: /**
110: * Recompute data for the graph and make sure that the area of the display canvas
111: * that shows the graph is redrawn. This method is ordinarily called by a
112: * Controller.
113: */
114: synchronized public void compute() {
115: setup(coords);
116: needsRedraw();
117: }
118:
119: /**
120: * Draw the graph (possibly recomputing the data if the CoordinateRect has changed).
121: * This is not usually called directly.
122: *
123: */
124: synchronized public void draw(Graphics g, boolean coordsChanged) {
125: if (changed || coordsChanged || xcoord == null
126: || ycoord == null) {
127: setup(coords);
128: changed = false;
129: }
130: if (xcoord.length == 0)
131: return;
132: g.setColor(graphColor);
133: int x = xcoord[0];
134: int y = ycoord[0];
135: for (int i = 1; i < xcoord.length; i++) {
136: if (xcoord[i] == Integer.MIN_VALUE) {
137: do {
138: i++;
139: } while (i < xcoord.length
140: && xcoord[i] == Integer.MIN_VALUE);
141: if (i < xcoord.length) {
142: x = xcoord[i];
143: y = ycoord[i];
144: }
145: } else {
146: int x2 = xcoord[i];
147: int y2 = ycoord[i];
148: g.drawLine(x, y, x2, y2);
149: x = x2;
150: y = y2;
151: }
152: }
153: }
154:
155: // ------------------------- Computing the points on the graph -----------------------
156:
157: private double absoluteYmax, onscreenymax, absoluteYmin,
158: onscreenymin;
159: private final static int UNDEFINED = 0, ABOVE = 1, BELOW = 2,
160: ONSCREEN = 3;
161: private double[] v = new double[1];
162: private Cases case1 = new Cases();
163: private Cases case2 = new Cases();
164:
165: private double eval(double x, Cases c) {
166: v[0] = x;
167: if (c != null)
168: c.clear();
169: double y = func.getValueWithCases(v, c);
170: if (Double.isInfinite(y) || Double.isNaN(y))
171: return Double.NaN;
172: else if (y > absoluteYmax)
173: return absoluteYmax;
174: else if (y < absoluteYmin)
175: return absoluteYmin;
176: else
177: return y;
178: }
179:
180: private int getStatus(double y) {
181: if (Double.isNaN(y))
182: return UNDEFINED;
183: else if (y > onscreenymax)
184: return ABOVE;
185: else if (y < onscreenymin)
186: return BELOW;
187: else
188: return ONSCREEN;
189: }
190:
191: private void setup(CoordinateRect c) {
192: if (func == null || c == null) {
193: xcoord = ycoord = new int[0];
194: return;
195: }
196: Vector points = new Vector();
197: double pixelWidth = (c.getXmax() - c.getXmin())
198: / (c.getWidth() - 2 * c.getGap() - 1);
199: onscreenymax = c.getYmax() + (100 + c.getGap()) * pixelWidth;
200: onscreenymin = c.getYmin() - (100 + c.getGap()) * pixelWidth;
201: absoluteYmax = c.getYmax() + 5000 * pixelWidth;
202: absoluteYmin = c.getYmin() - 5000 * pixelWidth;
203:
204: double prevx, prevy, x, y, lastx;
205: int status, prevstatus;
206:
207: int pixelx = c.getLeft();
208: int pixely;
209:
210: int xHoldOffscreen = Integer.MIN_VALUE;
211: int yHoldOffscreen = 0;
212: int statusHoldOffscreen = 0;
213:
214: x = c.pixelToX(pixelx);
215: y = eval(x, case1);
216: status = getStatus(y);
217: if (status == ONSCREEN) {
218: points.addElement(new Point(pixelx, c.yToPixel(y)));
219: } else if (status != UNDEFINED) {
220: xHoldOffscreen = pixelx;
221: yHoldOffscreen = c.yToPixel(y);
222: statusHoldOffscreen = status;
223: }
224:
225: int limitx = c.getLeft() + c.getWidth() - 1;
226: while (pixelx < limitx) {
227: prevx = x;
228: prevy = y;
229: prevstatus = status;
230: pixelx += 3;
231: if (pixelx > limitx)
232: pixelx = limitx;
233: x = c.pixelToX(pixelx);
234: y = eval(x, case2);
235: status = getStatus(y);
236: if (status == UNDEFINED) {
237: if (prevstatus != UNDEFINED) {
238: if (prevstatus == ONSCREEN)
239: domainEndpoint(c, points, prevx, x, prevy, y,
240: prevstatus, status, 1);
241: else if (xHoldOffscreen != Integer.MIN_VALUE)
242: points.addElement(new Point(xHoldOffscreen,
243: yHoldOffscreen));
244: xHoldOffscreen = Integer.MIN_VALUE;
245: points.addElement(new Point(Integer.MIN_VALUE, 0));
246: }
247: } else if (prevstatus == UNDEFINED) {
248: if (status == ONSCREEN) {
249: domainEndpoint(c, points, prevx, x, prevy, y,
250: prevstatus, status, 1);
251: points.addElement(new Point(pixelx, c.yToPixel(y)));
252: xHoldOffscreen = Integer.MIN_VALUE;
253: } else {// note: status != UNDEFINED
254: xHoldOffscreen = pixelx;
255: yHoldOffscreen = c.yToPixel(y);
256: statusHoldOffscreen = status;
257: }
258: // xHoldOffscreen is already Integer.MIN_VALUE
259: } else if (case1.equals(case2)) {
260: if (status == ONSCREEN) {
261: if (xHoldOffscreen != Integer.MIN_VALUE) {
262: points.addElement(new Point(xHoldOffscreen,
263: yHoldOffscreen));
264: xHoldOffscreen = Integer.MIN_VALUE;
265: }
266: points.addElement(new Point(pixelx, c.yToPixel(y)));
267: } else {
268: pixely = c.yToPixel(y);
269: if (xHoldOffscreen != Integer.MIN_VALUE) {
270: if (status != statusHoldOffscreen) { // one ABOVE, one BELOW
271: points.addElement(new Point(xHoldOffscreen,
272: yHoldOffscreen));
273: points
274: .addElement(new Point(pixelx,
275: pixely));
276: points.addElement(new Point(
277: Integer.MIN_VALUE, 0));
278: }
279: } else
280: points.addElement(new Point(pixelx, pixely)); // first jump to offscreen
281: xHoldOffscreen = pixelx;
282: yHoldOffscreen = pixely;
283: statusHoldOffscreen = status;
284: }
285: } else { // discontinuity
286: if (prevstatus == ABOVE || prevstatus == BELOW) {
287: if (status == prevstatus) {
288: if (xHoldOffscreen != Integer.MIN_VALUE) { // should be false
289: points.addElement(new Point(xHoldOffscreen,
290: yHoldOffscreen));
291: points.addElement(new Point(
292: Integer.MIN_VALUE, 0));
293: }
294: xHoldOffscreen = pixelx; // don't worry about offscreen discontinuity
295: yHoldOffscreen = c.yToPixel(y);
296: statusHoldOffscreen = status;
297: } else if (status == ONSCREEN) { // possible visible discontinuity
298: if (xHoldOffscreen != Integer.MIN_VALUE) {
299: points.addElement(new Point(xHoldOffscreen,
300: yHoldOffscreen));
301: xHoldOffscreen = Integer.MIN_VALUE;
302: }
303: discontinuity(c, points, prevx, x, prevy, y,
304: prevstatus, status, 1);
305: y = eval(x, case2); // reset cases, for next check
306: points.addElement(new Point(pixelx, c
307: .yToPixel(y)));
308: } else { // status == ABOVE or BELOW, opposit to prevstatus; just do a jump
309: if (xHoldOffscreen != Integer.MIN_VALUE)
310: points.addElement(new Point(xHoldOffscreen,
311: yHoldOffscreen));
312: points.addElement(new Point(Integer.MIN_VALUE,
313: 0));
314: xHoldOffscreen = pixelx;
315: yHoldOffscreen = c.yToPixel(y);
316: statusHoldOffscreen = status;
317: }
318: } else { // prevstatus is ONSCREEN; possible visible discontinuity
319: discontinuity(c, points, prevx, x, prevy, y,
320: prevstatus, status, 1);
321: y = eval(x, case2); // reset cases, for next check
322: if (status == ONSCREEN) {
323: points.addElement(new Point(pixelx, c
324: .yToPixel(y)));
325: xHoldOffscreen = Integer.MIN_VALUE;
326: } else {
327: xHoldOffscreen = pixelx;
328: yHoldOffscreen = c.yToPixel(y);
329: statusHoldOffscreen = status;
330: }
331: }
332: }
333: Cases temp = case2;
334: case2 = case1;
335: case1 = temp;
336: } // end while (pixel < limitx)
337: xcoord = new int[points.size()];
338: ycoord = new int[points.size()];
339: for (int i = 0; i < ycoord.length; i++) {
340: Point p = (Point) points.elementAt(i);
341: xcoord[i] = p.x;
342: ycoord[i] = p.y;
343: }
344: }
345:
346: private static final int MAX_DEPTH = 10;
347:
348: // Status of one endpoints is ONSCREEN; other is ABOVE,BELOW, or ONSCREEN
349: private void discontinuity(CoordinateRect c, Vector points,
350: double x1, double x2, double y1, double y2, int status1,
351: int status2, int depth) {
352:
353: //System.out.println("In discontinuity, depth = " + depth);
354: if (depth == MAX_DEPTH) {
355: points
356: .addElement(new Point(c.xToPixel(x1), c
357: .yToPixel(y1)));
358: points.addElement(new Point(Integer.MIN_VALUE, 0));
359: points
360: .addElement(new Point(c.xToPixel(x2), c
361: .yToPixel(y2)));
362: } else {
363: double xmid = (x1 + x2) / 2.0;
364: y1 = eval(x1, case1);
365: double ymid = eval(xmid, case2);
366: boolean cases1 = case1.equals(case2);
367: y2 = eval(x2, case1);
368: boolean cases2 = case1.equals(case2);
369: int statusmid = getStatus(ymid);
370: if (statusmid == UNDEFINED) { // hope it doesn't happen
371: if (status1 == ONSCREEN)
372: domainEndpoint(c, points, x1, xmid, y1, ymid,
373: status1, statusmid, 1);
374: points.addElement(new Point(Integer.MIN_VALUE, 0));
375: if (status2 == ONSCREEN)
376: domainEndpoint(c, points, xmid, x2, ymid, y2,
377: statusmid, status2, 1);
378:
379: } else if (cases1 == false) {
380: discontinuity(c, points, x1, xmid, y1, ymid, status1,
381: statusmid, depth + 1);
382: if (cases2 == false) // double discontinuity
383: discontinuity(c, points, xmid, x2, ymid, y2,
384: statusmid, status2, depth + 1);
385: } else if (cases2 == false)
386: discontinuity(c, points, xmid, x2, ymid, y2, statusmid,
387: status2, depth + 1);
388: else
389: System.out
390: .println("Impossible error? no discontinuity found in discontinuity for "
391: + x1 + ',' + x2);
392: }
393: }
394:
395: //One of status1 and status2 is UNDEFINED, one is ONSCREEN.
396: //This always adds a point to points.
397: private void domainEndpoint(CoordinateRect c, Vector points,
398: double x1, double x2, double y1, double y2, int status1,
399: int status2, int depth) {
400: //System.out.println("IN domainEndpoint, ......... depth = " + depth);
401: if (depth == MAX_DEPTH * 2) {
402: if (status1 == ONSCREEN)
403: points.addElement(new Point(c.xToPixel(x1), c
404: .yToPixel(y1)));
405: else
406: // status2 == ONSCREEN
407: points.addElement(new Point(c.xToPixel(x2), c
408: .yToPixel(y2)));
409: } else {
410: double xmid = (x1 + x2) / 2.0;
411: double ymid = eval(xmid, null);
412: int statusmid = getStatus(ymid);
413: if (statusmid == ABOVE || statusmid == BELOW)
414: points.addElement(new Point(c.xToPixel(xmid), c
415: .yToPixel(ymid)));
416: else if (statusmid == status1) // statusmid is ONSCREEN or UNDEFINED
417: domainEndpoint(c, points, xmid, x2, ymid, y2,
418: statusmid, status2, depth + 1);
419: else
420: domainEndpoint(c, points, x1, xmid, y1, ymid, status1,
421: statusmid, depth + 1);
422: }
423: }
424:
425: } // end class Graph1D
|