001: /*
002: * The contents of this file are subject to the Sapient Public License
003: * Version 1.0 (the "License"); you may not use this file except in compliance
004: * with the License. You may obtain a copy of the License at
005: * http://carbon.sf.net/License.html.
006: *
007: * Software distributed under the License is distributed on an "AS IS" basis,
008: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
009: * the specific language governing rights and limitations under the License.
010: *
011: * The Original Code is The Carbon Component Framework.
012: *
013: * The Initial Developer of the Original Code is Sapient Corporation
014: *
015: * Copyright (C) 2003 Sapient Corporation. All Rights Reserved.
016: */
017:
018: package org.sape.carbon.services.swing.graph;
019:
020: import java.awt.BasicStroke;
021: import java.awt.Color;
022: import java.awt.Graphics;
023: import java.awt.Graphics2D;
024: import java.awt.Rectangle;
025: import java.awt.RenderingHints;
026: import java.awt.geom.AffineTransform;
027: import java.awt.geom.GeneralPath;
028: import java.awt.geom.Line2D;
029: import java.awt.geom.PathIterator;
030: import java.awt.geom.Point2D;
031: import java.awt.geom.Rectangle2D;
032: import java.text.NumberFormat;
033: import java.util.HashMap;
034: import java.util.Iterator;
035: import java.util.Map;
036:
037: import javax.swing.JFrame;
038: import javax.swing.JPanel;
039:
040: /** <p>This class provides a consistent space within to graph real
041: * numbers. It provides features such as auto-centering and
042: * real-time scaling. A user of this graph provides data by creating
043: * one or more tracks and then adding real points to those. Calling
044: * translate periodically allows you to create a scrolling graph as
045: * well.</p>
046: *
047: * <p>This graph will also maintain tick marks that resize and
048: * can be stuck to the sides of the screen so they are always
049: * visible even if the origin is off-screen.</p>
050: *
051: * Copyright 2001 Sapient
052: * @author Greg Hinkle
053: * @version $Revision: 1.4 $ ($Author: dvoet $ / $Date: 2003/05/05 21:21:26 $)
054: */
055: public class GraphCanvas extends JPanel {
056:
057: /** A list of Track's that are a part of this graph */
058: private Map tracks = new HashMap(11);
059:
060: /** The current graph bounds that are visible */
061: protected Rectangle2D graphBounds;
062:
063: /** The portion of the entire height that should be researved as
064: * a border, above and below the highest and lowest track points */
065: private static final double BORDER_PERCENT = 0.1d;
066:
067: /** The background color for this graph */
068: protected Color backgroundColor = new Color(204, 204, 204);
069:
070: protected static NumberFormat labelFormat = null;
071: protected static NumberFormat bigNumberLabelFormat = null;
072:
073: /**
074: * Instantiates a graph canvas
075: */
076: public GraphCanvas() {
077: super ();
078:
079: setBackground(Color.blue);
080:
081: this .graphBounds = new Rectangle2D.Double(-5, 0, 150, 2);
082:
083: this .labelFormat = NumberFormat.getNumberInstance();
084: this .labelFormat.setMaximumFractionDigits(2);
085:
086: this .bigNumberLabelFormat = NumberFormat.getNumberInstance();
087: this .bigNumberLabelFormat.setMaximumFractionDigits(0);
088:
089: System.out
090: .println("GraphCanvas::<init> - New GraphCanvas created.");
091: }
092:
093: /**
094: * <p>Sets the background color of this graph
095: *
096: * @param color the Color to set the background to
097: */
098: public void setBackgroundColor(Color color) {
099: this .backgroundColor = color;
100: }
101:
102: /** Gets the bounds of the graphing space that are currently showing
103: * on the screen.
104: * @return Rectangle2D The bounds of the currently visible graph
105: */
106: public Rectangle2D getGraphBounds() {
107: return this .graphBounds;
108: }
109:
110: /**
111: * Sets the bounds that this graph is displaying
112: *
113: * @param rect the Rectangle2D of the desired graph points
114: */
115: public void setGraphBounds(Rectangle2D rect) {
116: this .graphBounds = rect;
117: }
118:
119: public AffineTransform getTransform() {
120:
121: AffineTransform affineT = new AffineTransform(1d, 0d, 0d, -1d,
122: 0d, super .getParent().getHeight());
123:
124: // scale to current scale
125: affineT.concatenate(AffineTransform.getScaleInstance(this
126: .getBounds().getWidth()
127: / this .graphBounds.getWidth(), this .getBounds()
128: .getHeight()
129: / this .graphBounds.getHeight()));
130:
131: // translate to the current origin
132: affineT.concatenate(AffineTransform.getTranslateInstance(
133: -this .graphBounds.getX(), -this .graphBounds.getY()));
134:
135: return affineT;
136: }
137:
138: // CLEAR ALL CURVES FROM PLOT
139: public void clear() {
140:
141: }
142:
143: public void addTrack(String trackName) {
144: this .tracks.put(trackName, new Track(trackName));
145: }
146:
147: public void addTrack(String trackName, Color color) {
148: this .tracks.put(trackName, new Track(trackName, color));
149: }
150:
151: // ADD CURVE TO STORAGE (DOESN'T GRAPH UNTIL REPAINT()).
152: public void addPoint(String track, Point2D point) {
153: ((Track) this .tracks.get(track)).addPoint(point);
154: }
155:
156: public Track getTrack(String trackName) {
157: return (Track) this .tracks.get(trackName);
158: }
159:
160: public void clearAll() {
161: this .getGraphics().clearRect((int) getBounds().getX(),
162: (int) getBounds().getY(), (int) getBounds().getWidth(),
163: (int) getBounds().getHeight());
164: }
165:
166: public void paint(Graphics gg) {
167: Graphics2D g = (Graphics2D) gg;
168: g.setBackground(this .backgroundColor);
169: // What is the current graph to panel transform
170: AffineTransform newTrans = getTransform();
171:
172: g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
173: RenderingHints.VALUE_ANTIALIAS_OFF);
174:
175: // Erase this entire graph so that we can redraw it
176: g.clearRect((int) getBounds().getX(), (int) getBounds().getY(),
177: (int) getBounds().getWidth(), (int) getBounds()
178: .getHeight());
179:
180: // This draws the tick marks and the tick values
181: drawLines(g, newTrans);
182:
183: // This drawes the axeses
184: drawAxes(g, newTrans);
185:
186: // This draws each of tracks in this graph
187: drawTracks(g, newTrans);
188:
189: // This draws the keys for each graph
190: drawKey(g, newTrans);
191: }
192:
193: /**
194: * <p>Draw the key to the tracks by calling thier toString</p>
195: *
196: * @param graphics2D the Graphics2D to draw to
197: * @param transform the Affine Transform to use to determine how to draw
198: */
199: protected void drawKey(Graphics2D g, AffineTransform transform) {
200:
201: int start = 20;
202:
203: Iterator trackIterator = this .tracks.values().iterator();
204: while (trackIterator.hasNext()) {
205:
206: Track track = (Track) trackIterator.next();
207:
208: String info = track.toString();
209: // Will draw the key in the same color as it's line
210: g.setColor(track.getColor());
211: g.drawString(info, 50, start += 25);
212: }
213: }
214:
215: protected void drawTracks(Graphics2D g, AffineTransform transform) {
216:
217: // Store original transform to restore it later
218: // until I figure out how to track differences only
219: AffineTransform origTrans = g.getTransform();
220:
221: // Transform for local drawing
222: g.transform(transform);
223:
224: g.setColor(Color.orange);
225:
226: // Using a small stroke will minimize the width to a single output
227: // level pixel up to a reasonable scaling size
228: g.setStroke(new BasicStroke(0.001f));
229:
230: // Draw the tracks
231: Iterator trackIterator = this .tracks.values().iterator();
232: while (trackIterator.hasNext()) {
233: Track track = (Track) trackIterator.next();
234: g.setColor(track.getColor());
235: GeneralPath path = track.getPath();
236:
237: g.draw(path);
238: }
239:
240: // Reset transformation
241: g.setTransform(origTrans);
242: }
243:
244: /**
245: * This draws the axes
246: */
247: protected void drawAxes(Graphics2D g, AffineTransform transform) {
248: g.setColor(Color.white);
249:
250: Point2D origin = transform.transform(new Point2D.Double(0, 0),
251: null);
252:
253: // If you want to have rubber banding axes (always visible)
254: Rectangle2D axesRect = new Rectangle2D.Double(5, 5, this
255: .bounds().getWidth() - 10, this .bounds().getHeight());
256: origin = floorPoint(origin, axesRect);
257:
258: Line2D x = new Line2D.Double(getBounds().getMinX(), origin
259: .getY(), getBounds().getMaxX(), origin.getY());
260:
261: Line2D y = new Line2D.Double(origin.getX(), getBounds()
262: .getMinY(), origin.getX(), getBounds().getMaxY());
263:
264: g.draw(x);
265: g.draw(y);
266: }
267:
268: /**
269: * <p>This finds the closest point on a rectangle's edge to a point outside
270: * the rectangle or if that point is within the rectangle it is returned.
271: * </p>
272: *
273: * @param point The point to rectangularly floor
274: * @param rect The rectangle to floor within
275: */
276: public static Point2D floorPoint(Point2D point, Rectangle2D rect) {
277: double x = point.getX();
278: double y = point.getY();
279:
280: if (x < rect.getMinX())
281: x = rect.getMinX();
282: if (x > rect.getMaxX())
283: x = rect.getMaxX();
284: if (y < rect.getMinY())
285: y = rect.getMinY();
286: if (y > rect.getMaxY())
287: y = rect.getMaxY();
288:
289: return new Point2D.Double(x, y);
290: }
291:
292: /**
293: * <p>This draws the tick marks in the graph
294: *
295: */
296: protected void drawLines(Graphics2D g, AffineTransform transform) {
297:
298: g.setColor(Color.white);
299: int REAL_TICK_SPACE = 40;
300: int REAL_TICK_HEIGHT = 10;
301:
302: double graphTickSpaceX = (REAL_TICK_SPACE / transform
303: .getScaleX());
304: double graphTickSpaceY = (REAL_TICK_SPACE / Math.abs(transform
305: .getScaleY()));
306:
307: Point2D origin = transform.transform(new Point2D.Float(0, 0),
308: null);
309:
310: // If you want to have rubber banding axes (always visible)
311: Rectangle2D axesRect = new Rectangle2D.Double(5, 5, this
312: .bounds().getWidth() - 10, this .bounds().getHeight());
313: Point2D falseOrigin = floorPoint(origin, axesRect);
314:
315: double firstX = this .graphBounds.getMinX();
316:
317: Point2D pt = new Point2D.Float();
318: for (double x = firstX; x <= (this .graphBounds.getMaxX() + graphTickSpaceX); x += graphTickSpaceX) {
319: double tx = (Math.floor(x / graphTickSpaceX))
320: * graphTickSpaceX;
321: pt.setLocation(tx, 0);
322: transform.transform(pt, pt);
323: g.drawLine((int) pt.getX(), (int) falseOrigin.getY() - 5,
324: (int) pt.getX(), (int) falseOrigin.getY() + 5);
325:
326: String label;
327: if (tx > 10)
328: label = this .bigNumberLabelFormat.format(tx);
329: else
330: label = this .labelFormat.format(tx);
331:
332: g.drawString(label, (float) pt.getX(), (float) falseOrigin
333: .getY() - 9);
334: }
335:
336: double firstY = this .graphBounds.getMinY();
337: for (double y = firstY; y <= (this .graphBounds.getMaxY() + graphTickSpaceY); y += graphTickSpaceY) {
338: double ty = (Math.floor(y / graphTickSpaceY))
339: * graphTickSpaceY;
340: pt.setLocation(0, ty);
341: transform.transform(pt, pt);
342: g.drawLine((int) falseOrigin.getX() - 5, (int) pt.getY(),
343: (int) falseOrigin.getX() + 5, (int) pt.getY());
344:
345: String label;
346: if (ty > 10)
347: label = this .bigNumberLabelFormat.format(ty);
348: else
349: label = this .labelFormat.format(ty);
350:
351: g.drawString(label, (float) falseOrigin.getX() + 7,
352: (float) pt.getY());
353:
354: }
355: }
356:
357: public static class Track {
358: protected String name;
359: protected Color color = Color.black; //Default to black
360: protected GeneralPath path = new GeneralPath();
361: protected boolean started = false;
362: protected NumberFormat keyFormat;
363:
364: public Track(String name) {
365: super ();
366: this .name = name;
367:
368: this .keyFormat = NumberFormat.getNumberInstance();
369: this .keyFormat.setMaximumFractionDigits(2);
370: }
371:
372: public Track(String name, Color color) {
373: this (name);
374: this .color = color;
375: }
376:
377: public void setPath(GeneralPath path) {
378: this .path = path;
379: }
380:
381: public GeneralPath getPath() {
382: return this .path;
383: }
384:
385: public void addPoint(Point2D point) {
386: if (path.getCurrentPoint() == null) {
387: this .path.moveTo((float) point.getX(), (float) point
388: .getY());
389: this .started = true;
390: } else {
391: this .path.lineTo((float) point.getX(), (float) point
392: .getY());
393: }
394:
395: }
396:
397: public Color getColor() {
398: return this .color;
399: }
400:
401: public void setColor(Color color) {
402: this .color = color;
403: }
404:
405: public String toString() {
406: String value = null;
407: if (this .path.getCurrentPoint() != null) {
408: double val = this .path.getCurrentPoint().getY();
409:
410: //NumberFormat nf = NumberFormat.getNumberInstance();
411: value = this .keyFormat.format(val);
412: }
413: return this .name + ": " + value;
414: }
415: }
416:
417: /**
418: * <p>Bounds the graph to the limits of the tracks verticaly providing a
419: * useful scaling. A more intelligent implementation could have minimum
420: * bounds to limit the bouncyness to startup.</p>
421: */
422: public void verticalBound() {
423: Rectangle2D rect = null;
424: Rectangle2D orig = getGraphBounds();
425:
426: Iterator trackIterator = this .tracks.values().iterator();
427: while (trackIterator.hasNext()) {
428: Track track = (Track) trackIterator.next();
429:
430: GeneralPath path = track.getPath();
431:
432: if (rect == null)
433: rect = path.getBounds2D();
434: else
435: Rectangle.union(rect, path.getBounds2D(), rect);
436: }
437: Rectangle.union(rect, new Rectangle2D.Double(orig.getX(), 0, 1,
438: 1), rect);
439:
440: double border = rect.getHeight() * BORDER_PERCENT;
441:
442: setGraphBounds(new Rectangle2D.Double(orig.getMinX(), rect
443: .getMinY()
444: - border, orig.getWidth(), rect.getHeight()
445: + (2d * border)));
446: }
447:
448: public void clipOld() {
449: Rectangle2D rect = getGraphBounds();
450:
451: //Rectangle2D orig = AffineTransform.getScaleInstance(1.5,1.5).createTransformedShape(getGraphBounds()).getBounds();
452:
453: Iterator trackIterator = this .tracks.values().iterator();
454: double[] cs = new double[6];
455: while (trackIterator.hasNext()) {
456: Track track = (Track) trackIterator.next();
457:
458: GeneralPath path = track.getPath();
459:
460: GeneralPath newPath = new GeneralPath();
461:
462: PathIterator pIter = path
463: .getPathIterator(new AffineTransform());
464: while (!pIter.isDone()) {
465:
466: pIter.currentSegment(cs);
467:
468: //Point2D pt = new Point2D.Double(cs[0],cs[1]);
469: if (cs[0] > rect.getMinX()) {
470: if (newPath.getCurrentPoint() == null)
471: newPath.moveTo((float) cs[0], (float) cs[1]);
472: else
473: newPath.lineTo((float) cs[0], (float) cs[1]);
474: }
475:
476: /*
477: System.out.println("Current Segment: " +
478: cs[0] + ", " +
479: cs[1] + ", " +
480: cs[2] + ", " +
481: cs[3] + ", " +
482: cs[4] + ", " +
483: cs[5]);
484: **/
485: pIter.next();
486:
487: }
488: track.setPath(newPath);
489: }
490:
491: }
492:
493: /**
494: * <p>Translates the main graph rect by x and y, horizontally and vertically
495: * respectively.</p>
496: */
497: public void translate(double x, double y) {
498:
499: Rectangle2D rect = getGraphBounds();
500: setGraphBounds(new Rectangle2D.Double(rect.getMinX() + x, rect
501: .getMinY()
502: + y, rect.getWidth(), rect.getHeight()));
503: }
504:
505: public static void main(String[] args) throws Exception {
506:
507: GraphCanvas gc = new GraphCanvas();
508: gc.show();
509:
510: JFrame frame = new JFrame("Memory Graph");
511: frame.getContentPane().add(gc);
512: frame.setSize(600, 200);
513:
514: // TODO: Add window exit listener
515:
516: frame.show();
517: gc.repaint();
518: gc.paint((Graphics2D) gc.getGraphics());
519:
520: long start = System.currentTimeMillis();
521:
522: gc.addTrack("test", Color.cyan);
523: gc.addTrack("test2", Color.blue);
524: gc.addTrack("test3", Color.red);
525: gc.addTrack("test4", Color.yellow);
526: gc.addTrack("test5", Color.green);
527: gc.addTrack("test6", Color.orange);
528: gc.addTrack("test7", Color.pink);
529:
530: int i = 0;
531: while (true) {
532: i++;
533:
534: Point2D pt = new Point2D.Float(i, ((float) Math
535: .cos(i / 20f) + (float) Math.sin(i / 40f)) * 3f);
536: gc.addPoint("test", pt);
537:
538: Point2D pt2 = new Point2D.Float(i, (float) Math
539: .cos(i / 25.0f) * 10f);
540: gc.addPoint("test2", pt2);
541:
542: Point2D pt3 = new Point2D.Float(i, Math.min((float) Math
543: .cos(Math.sin(i / 4f))
544: * 13f - (float) Math.cos(i / 80f) * 20f, 400f));
545: gc.addPoint("test3", pt3);
546:
547: Point2D pt4 = new Point2D.Float(i, (float) Math
548: .sin(.31 * i)
549: * 2f
550: + ((float) 2f * (float) Math.cos(.07f * i))
551: * 8f);
552: gc.addPoint("test4", pt4);
553:
554: Point2D pt5 = new Point2D.Float(i, (float) Math
555: .cos(.66 * i)
556: * 1f
557: + ((float) 2f * (float) Math.cos(.07f * i))
558: * 3f);
559: gc.addPoint("test5", pt5);
560:
561: Point2D pt6 = new Point2D.Float(i, (float) Math
562: .sin(.31 * i)
563: * 2f
564: + ((float) 2f * (float) Math
565: .cos(.07f * Math.tan(i))) * 5f);
566: gc.addPoint("test6", pt6);
567:
568: Point2D pt7 = new Point2D.Float(i, (float) Math.sin(i) * 2f
569: + ((float) 2f * (float) Math.sin(.25f * i)) * 0.5f);
570: gc.addPoint("test7", pt7);
571:
572: if (i > 150)
573: gc.translate(1, 0);
574:
575: gc.verticalBound();
576:
577: //if(i%100 == 0) {
578: gc.clipOld();
579: //}
580: gc.repaint();
581:
582: Thread.sleep(10);
583: if (i % 100 == 0) {
584: System.out
585: .println("Framewrate: "
586: + (100d / ((System.currentTimeMillis() - start) / 1000d)));
587: start = System.currentTimeMillis();
588: }
589: }
590:
591: }
592: }
|