1 package com.billkuker.rocketry.motorsim.visual;
\r
3 import java.awt.BasicStroke;
\r
4 import java.awt.BorderLayout;
\r
5 import java.awt.Color;
\r
6 import java.awt.Font;
\r
7 import java.awt.Stroke;
\r
8 import java.awt.Toolkit;
\r
9 import java.awt.datatransfer.StringSelection;
\r
10 import java.awt.event.ActionEvent;
\r
11 import java.awt.event.ActionListener;
\r
12 import java.lang.reflect.InvocationTargetException;
\r
13 import java.lang.reflect.Method;
\r
14 import java.util.Collection;
\r
15 import java.util.Iterator;
\r
16 import java.util.concurrent.ExecutorService;
\r
17 import java.util.concurrent.Executors;
\r
18 import java.util.concurrent.ThreadFactory;
\r
20 import javax.measure.quantity.Area;
\r
21 import javax.measure.quantity.Length;
\r
22 import javax.measure.quantity.Quantity;
\r
23 import javax.measure.unit.SI;
\r
24 import javax.measure.unit.Unit;
\r
25 import javax.swing.JFrame;
\r
26 import javax.swing.JMenuItem;
\r
27 import javax.swing.JPanel;
\r
28 import javax.swing.SwingUtilities;
\r
30 import org.apache.log4j.Logger;
\r
31 import org.jfree.chart.ChartFactory;
\r
32 import org.jfree.chart.ChartPanel;
\r
33 import org.jfree.chart.JFreeChart;
\r
34 import org.jfree.chart.plot.Marker;
\r
35 import org.jfree.chart.plot.PlotOrientation;
\r
36 import org.jfree.chart.plot.ValueMarker;
\r
37 import org.jfree.data.xy.XYSeries;
\r
38 import org.jfree.data.xy.XYSeriesCollection;
\r
39 import org.jfree.ui.RectangleAnchor;
\r
40 import org.jfree.ui.RectangleInsets;
\r
41 import org.jfree.ui.TextAnchor;
\r
42 import org.jscience.physics.amount.Amount;
\r
44 import com.billkuker.rocketry.motorsim.RocketScience;
\r
45 import com.billkuker.rocketry.motorsim.grain.CoredCylindricalGrain;
\r
47 public class Chart<X extends Quantity, Y extends Quantity> extends JPanel implements RocketScience.UnitPreferenceListener {
\r
48 private static final long serialVersionUID = 1L;
\r
49 private static Logger log = Logger.getLogger(Chart.class);
\r
51 private final Stroke dashed = new BasicStroke(1, 1, 1, 1, new float[]{2,4}, 0);
\r
52 private final Font labelFont = new Font(Font.DIALOG, Font.BOLD, 10);
\r
54 private static ThreadFactory fastTF = new ThreadFactory() {
\r
55 public Thread newThread(Runnable r) {
\r
56 Thread t = new Thread(r);
\r
58 t.setName("Fast Chart Draw");
\r
62 private static ThreadFactory slowTF = new ThreadFactory() {
\r
63 public Thread newThread(Runnable r) {
\r
64 Thread t = new Thread(r);
\r
66 t.setName("Slow Chart Draw");
\r
70 private static ExecutorService fast = Executors.newFixedThreadPool(2, fastTF);
\r
71 private static ExecutorService slow = Executors.newFixedThreadPool(2, slowTF);
\r
74 public class IntervalDomain implements Iterable<Amount<X>> {
\r
76 Amount<X> low, high, delta;
\r
79 public IntervalDomain(Amount<X> low, Amount<X> high) {
\r
82 delta = high.minus(low).divide(steps);
\r
85 public IntervalDomain(Amount<X> low, Amount<X> high, int steps) {
\r
89 delta = high.minus(low).divide(steps);
\r
92 public Iterator<Amount<X>> iterator() {
\r
93 return new Iterator<Amount<X>>() {
\r
94 Amount<X> current = low;
\r
96 public boolean hasNext() {
\r
97 return current.isLessThan(high.plus(delta));
\r
100 public Amount<X> next() {
\r
101 Amount<X> ret = current;
\r
102 current = current.plus(delta);
\r
106 public final void remove() {
\r
107 throw new UnsupportedOperationException(
\r
108 "Chart domain iterators are not modifiable.");
\r
115 XYSeriesCollection dataset = new XYSeriesCollection();
\r
127 Iterable<Amount<X>> domain;
\r
129 public Chart(Unit<X> xUnit, Unit<Y> yUnit, Object source, String method, String xLabel, String yLabel)
\r
130 throws NoSuchMethodException {
\r
131 super(new BorderLayout());
\r
132 f = source.getClass().getMethod(method, Amount.class);
\r
134 this.source = source;
\r
136 this.xUnit = xUnit;
\r
137 this.yUnit = yUnit;
\r
139 this.xLabel = xLabel;
\r
140 this.yLabel = yLabel;
\r
142 RocketScience.addUnitPreferenceListener(this);
\r
147 private static String toTitle(Method f) {
\r
148 String ret = f.getName().substring(0, 1).toUpperCase()
\r
149 + f.getName().substring(1);
\r
150 ret = ret.replaceAll("(\\p{Ll})(\\p{Lu})", "$1 $2");
\r
154 private void setup(){
\r
156 this.xUnit = RocketScience.UnitPreference.getUnitPreference()
\r
157 .getPreferredUnit(xUnit);
\r
158 this.yUnit = RocketScience.UnitPreference.getUnitPreference()
\r
159 .getPreferredUnit(yUnit);
\r
161 chart = ChartFactory.createXYLineChart(
\r
162 toTitle(f), // Title
\r
163 xLabel + " (" + xUnit.toString() + ")", // x-axis Label
\r
164 yLabel + " (" + yUnit.toString() + ")", // y-axis Label
\r
165 dataset, PlotOrientation.VERTICAL, // Plot Orientation
\r
166 false, // Show Legend
\r
167 true, // Use tool tips
\r
168 false // Configure chart to generate URLs?
\r
170 ChartPanel cp = new ChartPanel(chart);
\r
171 cp.getPopupMenu().add(new JMenuItem("Copy CSV to Clipboard") {
\r
172 private static final long serialVersionUID = 1L;
\r
174 addActionListener(new ActionListener() {
\r
176 public void actionPerformed(ActionEvent ae) {
\r
177 XYSeries s = dataset.getSeries(0);
\r
178 StringBuilder sb = new StringBuilder();
\r
179 sb.append(f.getName().substring(0, 1).toUpperCase()
\r
180 + f.getName().substring(1));
\r
182 sb.append(Chart.this.chart.getXYPlot().getDomainAxis().getLabel());
\r
184 sb.append(Chart.this.chart.getXYPlot().getRangeAxis().getLabel());
\r
186 for (int i = 0; i < s.getItemCount(); i++) {
\r
187 sb.append(s.getX(i));
\r
189 sb.append(s.getY(i));
\r
192 Toolkit.getDefaultToolkit()
\r
193 .getSystemClipboard()
\r
195 new StringSelection(sb.toString()),
\r
206 public void preferredUnitsChanged() {
\r
213 public void addDomainMarker(Amount<X> x, String label, Color c){
\r
214 double xVal = x.doubleValue(xUnit);
\r
215 Marker marker = new ValueMarker(xVal);
\r
216 marker.setStroke(dashed);
\r
217 marker.setPaint(c);
\r
218 marker.setLabelPaint(c);
\r
219 marker.setLabelFont(labelFont);
\r
220 marker.setLabel(label + ": " + RocketScience.ammountToRoundedString(x));
\r
221 marker.setLabelTextAnchor(TextAnchor.TOP_LEFT);
\r
222 marker.setLabelOffset(new RectangleInsets(0, -5, 0, 0));
\r
223 chart.getXYPlot().addDomainMarker(marker);
\r
226 public void addRangeMarker(Amount<Y> y, String label, Color c){
\r
227 double yVal = y.doubleValue(yUnit);
\r
228 Marker marker = new ValueMarker(yVal);
\r
229 marker.setStroke(dashed);
\r
230 marker.setPaint(c);
\r
231 marker.setLabelPaint(c);
\r
232 marker.setLabelFont(labelFont);
\r
233 marker.setLabel(label + ": " + RocketScience.ammountToRoundedString(y));
\r
234 marker.setLabelTextAnchor(TextAnchor.TOP_LEFT);
\r
235 marker.setLabelOffset(new RectangleInsets(0, 5, 0, 0));
\r
236 chart.getXYPlot().addRangeMarker(marker);
\r
239 private Marker focusMarkerX, focusMarkerY;
\r
241 public void mark(Amount<X> m) {
\r
242 if (focusMarkerX != null)
\r
243 chart.getXYPlot().removeDomainMarker(focusMarkerX);
\r
244 if (focusMarkerY != null)
\r
245 chart.getXYPlot().removeRangeMarker(focusMarkerY);
\r
248 focusMarkerX = new ValueMarker(m.doubleValue(xUnit));
\r
249 focusMarkerX.setPaint(Color.blue);
\r
250 focusMarkerX.setAlpha(0.8f);
\r
252 chart.getXYPlot().addDomainMarker(focusMarkerX);
\r
254 Amount<Y> val = getNear(m);
\r
255 if ( val != null ){
\r
256 focusMarkerY = new ValueMarker(val.doubleValue(yUnit));
\r
257 focusMarkerY.setPaint(Color.BLUE);
\r
258 focusMarkerY.setLabelAnchor(RectangleAnchor.TOP_RIGHT);
\r
259 focusMarkerY.setLabelTextAnchor(TextAnchor.TOP_RIGHT);
\r
260 focusMarkerY.setLabelPaint(Color.BLUE);
\r
261 focusMarkerY.setLabelFont(labelFont);
\r
262 focusMarkerY.setLabelOffset(new RectangleInsets(0,5,0,0));
\r
263 chart.getXYPlot().addRangeMarker(focusMarkerY);
\r
264 focusMarkerY.setLabel(RocketScience.ammountToRoundedString(val));
\r
270 * Get the Y value at or near a given X
\r
271 * For display use only!
\r
276 private Amount<Y> getNear(final Amount<X> ax){
\r
277 if ( dataset.getSeriesCount() != 1 )
\r
279 final XYSeries s = dataset.getSeries(0);
\r
280 final double x = ax.doubleValue(xUnit);
\r
281 int idx = s.getItemCount() / 2;
\r
282 int delta = s.getItemCount() / 4;
\r
284 if ( s.getX(idx).doubleValue() < x ){
\r
293 final double lowerX = s.getX(idxL).doubleValue();
\r
294 final double higherX = s.getX(idxH).doubleValue();
\r
295 final double sampleXDiff = higherX - lowerX;
\r
296 final double xDiff = x - lowerX;
\r
297 final double dist = xDiff / sampleXDiff;
\r
298 final double lowerY = s.getY(idxL).doubleValue();
\r
299 final double higherY = s.getY(idxH).doubleValue();
\r
300 final double y = lowerY + dist * (higherY - lowerY);
\r
302 return Amount.valueOf( y, yUnit);
\r
307 private void drawDone(){
\r
311 private volatile boolean stop = false;
\r
312 private volatile int lastSkipStepShown;
\r
313 public void setDomain(final Iterable<Amount<X>> d) {
\r
314 chart.getXYPlot().clearDomainMarkers();
\r
315 chart.getXYPlot().clearRangeMarkers();
\r
316 lastSkipStepShown = Integer.MAX_VALUE;
\r
319 fast.submit(new Thread() {
\r
320 public void run() {
\r
323 slow.submit(new Thread() {
\r
324 public void run() {
\r
334 @SuppressWarnings("unchecked")
\r
335 private synchronized void fill(Iterable<Amount<X>> d, final int requestedSkip) {
\r
338 log.debug(f.getName() + " " + requestedSkip + " Start");
\r
341 int calculatedSkip = requestedSkip;
\r
342 if (d instanceof Collection) {
\r
343 sz = ((Collection<Amount<X>>) d).size();
\r
344 int sk2 = sz / 200;
\r
345 if (calculatedSkip < sk2)
\r
346 calculatedSkip = sk2;
\r
351 final XYSeries newSeries = new XYSeries(f.getName());
\r
353 Amount<X> last = null;
\r
354 for (Amount<X> ax : d) {
\r
356 log.debug(f.getName() + " " + calculatedSkip + " Abort");
\r
360 if (cnt % calculatedSkip == 0) {
\r
361 Amount<Y> y = (Amount<Y>) f.invoke(source, ax);
\r
362 newSeries.add(ax.doubleValue(xUnit), y.doubleValue(yUnit));
\r
366 Amount<Y> y = (Amount<Y>) f.invoke(source, last);
\r
367 newSeries.add(last.doubleValue(xUnit), y.doubleValue(yUnit));
\r
368 SwingUtilities.invokeLater(new Thread() {
\r
370 public void run() {
\r
371 if ( requestedSkip < lastSkipStepShown ){
\r
372 lastSkipStepShown = requestedSkip;
\r
373 dataset.removeAllSeries();
\r
374 dataset.addSeries(newSeries);
\r
375 log.debug(f.getName() + " Replaced with " + requestedSkip);
\r
377 if ( requestedSkip == 1 ){
\r
382 } catch (IllegalArgumentException e) {
\r
384 } catch (IllegalAccessException e) {
\r
386 } catch (InvocationTargetException e) {
\r
389 log.debug(f.getName() + " " + calculatedSkip + " Done");
\r
392 public void show() {
\r
394 private static final long serialVersionUID = 1L;
\r
396 setContentPane(Chart.this);
\r
398 setDefaultCloseOperation(EXIT_ON_CLOSE);
\r
400 }.setVisible(true);
\r
403 public static void main(String args[]) throws Exception {
\r
404 CoredCylindricalGrain g = new CoredCylindricalGrain();
\r
405 g.setLength(Amount.valueOf(70, SI.MILLIMETER));
\r
406 g.setOD(Amount.valueOf(30, SI.MILLIMETER));
\r
407 g.setID(Amount.valueOf(10, SI.MILLIMETER));
\r
409 Chart<Length, Area> c = new Chart<Length, Area>(SI.MILLIMETER,
\r
410 SI.MILLIMETER.pow(2).asType(Area.class), g, "surfaceArea", "Regression", "Area");
\r
412 c.setDomain(c.new IntervalDomain(Amount.valueOf(0, SI.CENTIMETER), g
\r
418 Chart<Length, Volume> v = new Chart<Length, Volume>(SI.MILLIMETER,
\r
419 SI.MILLIMETER.pow(3).asType(Volume.class), g, "volume");
\r
421 v.setDomain(c.new IntervalDomain(Amount.valueOf(0, SI.CENTIMETER), g
\r