Add axis labels to charts
[sw/motorsim] / gui / com / billkuker / rocketry / motorsim / visual / Chart.java
1 package com.billkuker.rocketry.motorsim.visual;\r
2 \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
19 \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
29 \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
43 \r
44 import com.billkuker.rocketry.motorsim.RocketScience;\r
45 import com.billkuker.rocketry.motorsim.grain.CoredCylindricalGrain;\r
46 \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
50         \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
53 \r
54         private static ThreadFactory fastTF = new ThreadFactory() {\r
55                 public Thread newThread(Runnable r) {\r
56                         Thread t = new Thread(r);\r
57                         t.setDaemon(true);\r
58                         t.setName("Fast Chart Draw");\r
59                         return t;\r
60                 }\r
61         };\r
62         private static ThreadFactory slowTF = new ThreadFactory() {\r
63                 public Thread newThread(Runnable r) {\r
64                         Thread t = new Thread(r);\r
65                         t.setDaemon(true);\r
66                         t.setName("Slow Chart Draw");\r
67                         return t;\r
68                 }\r
69         };\r
70         private static ExecutorService fast = Executors.newFixedThreadPool(2, fastTF);\r
71         private static ExecutorService slow = Executors.newFixedThreadPool(2, slowTF);\r
72 \r
73 \r
74         public class IntervalDomain implements Iterable<Amount<X>> {\r
75 \r
76                 Amount<X> low, high, delta;\r
77                 int steps = 100;\r
78 \r
79                 public IntervalDomain(Amount<X> low, Amount<X> high) {\r
80                         this.low = low;\r
81                         this.high = high;\r
82                         delta = high.minus(low).divide(steps);\r
83                 }\r
84 \r
85                 public IntervalDomain(Amount<X> low, Amount<X> high, int steps) {\r
86                         this.steps = steps;\r
87                         this.low = low;\r
88                         this.high = high;\r
89                         delta = high.minus(low).divide(steps);\r
90                 }\r
91 \r
92                 public Iterator<Amount<X>> iterator() {\r
93                         return new Iterator<Amount<X>>() {\r
94                                 Amount<X> current = low;\r
95 \r
96                                 public boolean hasNext() {\r
97                                         return current.isLessThan(high.plus(delta));\r
98                                 }\r
99 \r
100                                 public Amount<X> next() {\r
101                                         Amount<X> ret = current;\r
102                                         current = current.plus(delta);\r
103                                         return ret;\r
104                                 }\r
105 \r
106                                 public final void remove() {\r
107                                         throw new UnsupportedOperationException(\r
108                                                         "Chart domain iterators are not modifiable.");\r
109                                 }\r
110                         };\r
111                 }\r
112 \r
113         }\r
114 \r
115         XYSeriesCollection dataset = new XYSeriesCollection();\r
116         JFreeChart chart;\r
117 \r
118         Unit<X> xUnit;\r
119         Unit<Y> yUnit;\r
120 \r
121         String xLabel;\r
122         String yLabel;\r
123         \r
124         Object source;\r
125         Method f;\r
126         \r
127         Iterable<Amount<X>> domain;\r
128         \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
133 \r
134                 this.source = source;\r
135                 \r
136                 this.xUnit = xUnit;\r
137                 this.yUnit = yUnit;\r
138                 \r
139                 this.xLabel = xLabel;\r
140                 this.yLabel = yLabel;\r
141 \r
142                 RocketScience.addUnitPreferenceListener(this);\r
143                 \r
144                 setup();\r
145         }\r
146         \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
151                 return ret;\r
152         }\r
153         \r
154         private void setup(){\r
155                 removeAll();\r
156                 this.xUnit = RocketScience.UnitPreference.getUnitPreference()\r
157                                 .getPreferredUnit(xUnit);\r
158                 this.yUnit = RocketScience.UnitPreference.getUnitPreference()\r
159                                 .getPreferredUnit(yUnit);\r
160 \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
169                                 );\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
173                         {\r
174                                 addActionListener(new ActionListener() {\r
175                                         @Override\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
181                                                 sb.append("\n");\r
182                                                 sb.append(Chart.this.chart.getXYPlot().getDomainAxis().getLabel());\r
183                                                 sb.append(",");\r
184                                                 sb.append(Chart.this.chart.getXYPlot().getRangeAxis().getLabel());\r
185                                                 sb.append("\n");\r
186                                                 for (int i = 0; i < s.getItemCount(); i++) {\r
187                                                         sb.append(s.getX(i));\r
188                                                         sb.append(",");\r
189                                                         sb.append(s.getY(i));\r
190                                                         sb.append("\n");\r
191                                                 }\r
192                                                 Toolkit.getDefaultToolkit()\r
193                                                                 .getSystemClipboard()\r
194                                                                 .setContents(\r
195                                                                                 new StringSelection(sb.toString()),\r
196                                                                                 null);\r
197                                         }\r
198                                 });\r
199                         }\r
200                 }, 3);\r
201                 add(cp);\r
202         }\r
203         \r
204 \r
205         @Override\r
206         public void preferredUnitsChanged() {\r
207                 setup();\r
208                 setDomain(domain);\r
209         }\r
210         \r
211         \r
212         \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
224         }\r
225         \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
237         }\r
238 \r
239         private Marker focusMarkerX, focusMarkerY;\r
240 \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
246                 \r
247                 if (m != null) {\r
248                         focusMarkerX = new ValueMarker(m.doubleValue(xUnit));\r
249                         focusMarkerX.setPaint(Color.blue);\r
250                         focusMarkerX.setAlpha(0.8f);\r
251                         \r
252                         chart.getXYPlot().addDomainMarker(focusMarkerX);\r
253                         \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
265                         }\r
266                 }\r
267         }\r
268         \r
269         /**\r
270          * Get the Y value at or near a given X\r
271          * For display use only!\r
272          * \r
273          * @param ax\r
274          * @return\r
275          */\r
276         private Amount<Y> getNear(final Amount<X> ax){\r
277                 if ( dataset.getSeriesCount() != 1 )\r
278                         return null;\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
283                 while(true){\r
284                         if ( s.getX(idx).doubleValue() < x ){\r
285                                 idx += delta;\r
286                         } else {\r
287                                 idx -= delta;\r
288                         }\r
289                         delta = delta / 2;\r
290                         if ( delta < 1 ){\r
291                                 int idxL = idx-1;\r
292                                 int idxH = idx;\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
301                                 \r
302                                 return Amount.valueOf( y, yUnit);\r
303                         }\r
304                 }\r
305         }\r
306         \r
307         private void drawDone(){\r
308         \r
309         }\r
310 \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
317                 stop = true;\r
318                 fill(d, 100);\r
319                 fast.submit(new Thread() {\r
320                         public void run() {\r
321                                 if (!stop)\r
322                                         fill(d, 10);\r
323                                 slow.submit(new Thread() {\r
324                                         public void run() {\r
325                                                 if (!stop){\r
326                                                         fill(d, 1);\r
327                                                 }\r
328                                         }\r
329                                 });\r
330                         }\r
331                 });\r
332         }\r
333 \r
334         @SuppressWarnings("unchecked")\r
335         private synchronized void fill(Iterable<Amount<X>> d, final int requestedSkip) {\r
336                 this.domain = d;\r
337                 \r
338                 log.debug(f.getName() + " " + requestedSkip + " Start");\r
339                 stop = false;\r
340                 int sz = 0;\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
347                 }\r
348                 // series.clear();\r
349                 int cnt = 0;\r
350 \r
351                 final XYSeries newSeries = new XYSeries(f.getName());\r
352                 try {\r
353                         Amount<X> last = null;\r
354                         for (Amount<X> ax : d) {\r
355                                 if (stop) {\r
356                                         log.debug(f.getName() + " " + calculatedSkip + " Abort");\r
357                                         return;\r
358                                 }\r
359                                 last = ax;\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
363                                 }\r
364                                 cnt++;\r
365                         }\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
369                                 @Override\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
376                                         }\r
377                                         if ( requestedSkip == 1 ){\r
378                                                 drawDone();\r
379                                         }\r
380                                 }\r
381                         });\r
382                 } catch (IllegalArgumentException e) {\r
383                         log.error(e);\r
384                 } catch (IllegalAccessException e) {\r
385                         log.error(e);\r
386                 } catch (InvocationTargetException e) {\r
387                         log.error(e);\r
388                 }\r
389                 log.debug(f.getName() + " " + calculatedSkip + " Done");\r
390         }\r
391 \r
392         public void show() {\r
393                 new JFrame() {\r
394                         private static final long serialVersionUID = 1L;\r
395                         {\r
396                                 setContentPane(Chart.this);\r
397                                 setSize(640, 480);\r
398                                 setDefaultCloseOperation(EXIT_ON_CLOSE);\r
399                         }\r
400                 }.setVisible(true);\r
401         }\r
402 \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
408 \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
411 \r
412                 c.setDomain(c.new IntervalDomain(Amount.valueOf(0, SI.CENTIMETER), g\r
413                                 .webThickness()));\r
414 \r
415                 c.show();\r
416 \r
417                 /*\r
418                 Chart<Length, Volume> v = new Chart<Length, Volume>(SI.MILLIMETER,\r
419                                 SI.MILLIMETER.pow(3).asType(Volume.class), g, "volume");\r
420 \r
421                 v.setDomain(c.new IntervalDomain(Amount.valueOf(0, SI.CENTIMETER), g\r
422                                 .webThickness()));\r
423 \r
424                 v.show();*/\r
425         }\r
426 \r
427 \r
428 }\r