1ff57721ea39baa8c52603dfdf4fba2fd85effde
[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.lang.reflect.InvocationTargetException;\r
9 import java.lang.reflect.Method;\r
10 import java.util.Collection;\r
11 import java.util.Iterator;\r
12 import java.util.concurrent.ExecutorService;\r
13 import java.util.concurrent.Executors;\r
14 import java.util.concurrent.ThreadFactory;\r
15 \r
16 import javax.measure.quantity.Area;\r
17 import javax.measure.quantity.Length;\r
18 import javax.measure.quantity.Quantity;\r
19 import javax.measure.quantity.Volume;\r
20 import javax.measure.unit.SI;\r
21 import javax.measure.unit.Unit;\r
22 import javax.swing.JFrame;\r
23 import javax.swing.JPanel;\r
24 import javax.swing.SwingUtilities;\r
25 \r
26 import org.apache.log4j.Logger;\r
27 import org.jfree.chart.ChartFactory;\r
28 import org.jfree.chart.ChartPanel;\r
29 import org.jfree.chart.JFreeChart;\r
30 import org.jfree.chart.plot.Marker;\r
31 import org.jfree.chart.plot.PlotOrientation;\r
32 import org.jfree.chart.plot.ValueMarker;\r
33 import org.jfree.data.xy.XYSeries;\r
34 import org.jfree.data.xy.XYSeriesCollection;\r
35 import org.jfree.ui.RectangleAnchor;\r
36 import org.jfree.ui.RectangleInsets;\r
37 import org.jfree.ui.TextAnchor;\r
38 import org.jscience.physics.amount.Amount;\r
39 \r
40 import com.billkuker.rocketry.motorsim.RocketScience;\r
41 import com.billkuker.rocketry.motorsim.grain.CoredCylindricalGrain;\r
42 \r
43 public class Chart<X extends Quantity, Y extends Quantity> extends JPanel implements RocketScience.UnitPreferenceListener {\r
44         private static final long serialVersionUID = 1L;\r
45         private static Logger log = Logger.getLogger(Chart.class);\r
46         \r
47         private final Stroke dashed = new BasicStroke(1, 1, 1, 1, new float[]{2,4}, 0);\r
48         private final Font labelFont = new Font(Font.DIALOG, Font.BOLD, 10);\r
49 \r
50         private static ThreadFactory fastTF = new ThreadFactory() {\r
51                 public Thread newThread(Runnable r) {\r
52                         Thread t = new Thread(r);\r
53                         t.setDaemon(true);\r
54                         t.setName("Fast Chart Draw");\r
55                         return t;\r
56                 }\r
57         };\r
58         private static ThreadFactory slowTF = new ThreadFactory() {\r
59                 public Thread newThread(Runnable r) {\r
60                         Thread t = new Thread(r);\r
61                         t.setDaemon(true);\r
62                         t.setName("Slow Chart Draw");\r
63                         return t;\r
64                 }\r
65         };\r
66         private static ExecutorService fast = Executors.newFixedThreadPool(2, fastTF);\r
67         private static ExecutorService slow = Executors.newFixedThreadPool(2, slowTF);\r
68 \r
69 \r
70         public class IntervalDomain implements Iterable<Amount<X>> {\r
71 \r
72                 Amount<X> low, high, delta;\r
73                 int steps = 100;\r
74 \r
75                 public IntervalDomain(Amount<X> low, Amount<X> high) {\r
76                         this.low = low;\r
77                         this.high = high;\r
78                         delta = high.minus(low).divide(steps);\r
79                 }\r
80 \r
81                 public IntervalDomain(Amount<X> low, Amount<X> high, int steps) {\r
82                         this.steps = steps;\r
83                         this.low = low;\r
84                         this.high = high;\r
85                         delta = high.minus(low).divide(steps);\r
86                 }\r
87 \r
88                 public Iterator<Amount<X>> iterator() {\r
89                         return new Iterator<Amount<X>>() {\r
90                                 Amount<X> current = low;\r
91 \r
92                                 public boolean hasNext() {\r
93                                         return current.isLessThan(high.plus(delta));\r
94                                 }\r
95 \r
96                                 public Amount<X> next() {\r
97                                         Amount<X> ret = current;\r
98                                         current = current.plus(delta);\r
99                                         return ret;\r
100                                 }\r
101 \r
102                                 public final void remove() {\r
103                                         throw new UnsupportedOperationException(\r
104                                                         "Chart domain iterators are not modifiable.");\r
105                                 }\r
106                         };\r
107                 }\r
108 \r
109         }\r
110 \r
111         XYSeriesCollection dataset = new XYSeriesCollection();\r
112         JFreeChart chart;\r
113 \r
114         Unit<X> xUnit;\r
115         Unit<Y> yUnit;\r
116 \r
117         Object source;\r
118         Method f;\r
119         \r
120         Iterable<Amount<X>> domain;\r
121 \r
122         public Chart(Unit<X> xUnit, Unit<Y> yUnit, Object source, String method)\r
123                         throws NoSuchMethodException {\r
124                 super(new BorderLayout());\r
125                 f = source.getClass().getMethod(method, Amount.class);\r
126 \r
127                 this.source = source;\r
128                 \r
129                 this.xUnit = xUnit;\r
130                 this.yUnit = yUnit;\r
131 \r
132                 RocketScience.addUnitPreferenceListener(this);\r
133                 \r
134                 setup();\r
135         }\r
136         \r
137         private void setup(){\r
138                 removeAll();\r
139                 this.xUnit = RocketScience.UnitPreference.getUnitPreference()\r
140                                 .getPreferredUnit(xUnit);\r
141                 this.yUnit = RocketScience.UnitPreference.getUnitPreference()\r
142                                 .getPreferredUnit(yUnit);\r
143 \r
144                 chart = ChartFactory.createXYLineChart(f.getName().substring(0, 1)\r
145                                 .toUpperCase()\r
146                                 + f.getName().substring(1), // Title\r
147                                 this.xUnit.toString(), // x-axis Label\r
148                                 this.yUnit.toString(), // y-axis Label\r
149                                 dataset, PlotOrientation.VERTICAL, // Plot Orientation\r
150                                 false, // Show Legend\r
151                                 true, // Use tool tips\r
152                                 false // Configure chart to generate URLs?\r
153                                 );\r
154                 add(new ChartPanel(chart));\r
155         }\r
156         \r
157 \r
158         @Override\r
159         public void preferredUnitsChanged() {\r
160                 setup();\r
161                 setDomain(domain);\r
162         }\r
163         \r
164         \r
165         \r
166         public void addDomainMarker(Amount<X> x, String label, Color c){\r
167                 double xVal = x.doubleValue(xUnit);\r
168                 Marker marker = new ValueMarker(xVal);\r
169                 marker.setStroke(dashed);\r
170                 marker.setPaint(c);\r
171                 marker.setLabelPaint(c);\r
172                 marker.setLabelFont(labelFont);\r
173                 marker.setLabel(label + ": " + RocketScience.ammountToRoundedString(x));\r
174                 marker.setLabelTextAnchor(TextAnchor.TOP_LEFT);\r
175                 marker.setLabelOffset(new RectangleInsets(0, -5, 0, 0));\r
176                 chart.getXYPlot().addDomainMarker(marker);\r
177         }\r
178         \r
179         public void addRangeMarker(Amount<Y> y, String label, Color c){\r
180                 double yVal = y.doubleValue(yUnit);\r
181                 Marker marker = new ValueMarker(yVal);\r
182                 marker.setStroke(dashed);\r
183                 marker.setPaint(c);\r
184                 marker.setLabelPaint(c);\r
185                 marker.setLabelFont(labelFont);\r
186                 marker.setLabel(label + ": " + RocketScience.ammountToRoundedString(y));\r
187                 marker.setLabelTextAnchor(TextAnchor.TOP_LEFT);\r
188                 marker.setLabelOffset(new RectangleInsets(0, 5, 0, 0));\r
189                 chart.getXYPlot().addRangeMarker(marker);\r
190         }\r
191 \r
192         private Marker focusMarkerX, focusMarkerY;\r
193 \r
194         public void mark(Amount<X> m) {\r
195                 if (focusMarkerX != null)\r
196                         chart.getXYPlot().removeDomainMarker(focusMarkerX);\r
197                 if (focusMarkerY != null)\r
198                         chart.getXYPlot().removeRangeMarker(focusMarkerY);\r
199                 \r
200                 if (m != null) {\r
201                         focusMarkerX = new ValueMarker(m.doubleValue(xUnit));\r
202                         focusMarkerX.setPaint(Color.blue);\r
203                         focusMarkerX.setAlpha(0.8f);\r
204                         \r
205                         chart.getXYPlot().addDomainMarker(focusMarkerX);\r
206                         \r
207                         Amount<Y> val = getNear(m);\r
208                         if ( val != null ){\r
209                                 focusMarkerY = new ValueMarker(val.doubleValue(yUnit));\r
210                                 focusMarkerY.setPaint(Color.BLUE);\r
211                                 focusMarkerY.setLabelAnchor(RectangleAnchor.TOP_RIGHT);\r
212                                 focusMarkerY.setLabelTextAnchor(TextAnchor.TOP_RIGHT);\r
213                                 focusMarkerY.setLabelPaint(Color.BLUE);\r
214                                 focusMarkerY.setLabelFont(labelFont);\r
215                                 focusMarkerY.setLabelOffset(new RectangleInsets(0,5,0,0));\r
216                                 chart.getXYPlot().addRangeMarker(focusMarkerY);\r
217                                 focusMarkerY.setLabel(RocketScience.ammountToRoundedString(val));\r
218                         }\r
219                 }\r
220         }\r
221         \r
222         /**\r
223          * Get the Y value at or near a given X\r
224          * For display use only!\r
225          * \r
226          * @param ax\r
227          * @return\r
228          */\r
229         private Amount<Y> getNear(final Amount<X> ax){\r
230                 if ( dataset.getSeriesCount() != 1 )\r
231                         return null;\r
232                 final XYSeries s = dataset.getSeries(0);\r
233                 final double x = ax.doubleValue(xUnit);\r
234                 int idx = s.getItemCount() / 2;\r
235                 int delta = s.getItemCount() / 4;\r
236                 while(true){\r
237                         if ( s.getX(idx).doubleValue() < x ){\r
238                                 idx += delta;\r
239                         } else {\r
240                                 idx -= delta;\r
241                         }\r
242                         delta = delta / 2;\r
243                         if ( delta < 1 ){\r
244                                 int idxL = idx-1;\r
245                                 int idxH = idx;\r
246                                 final double lowerX = s.getX(idxL).doubleValue();\r
247                                 final double higherX = s.getX(idxH).doubleValue();\r
248                                 final double sampleXDiff = higherX - lowerX;\r
249                                 final double xDiff = x - lowerX;\r
250                                 final double dist = xDiff / sampleXDiff;\r
251                                 final double lowerY = s.getY(idxL).doubleValue();\r
252                                 final double higherY = s.getY(idxH).doubleValue();\r
253                                 final double y = lowerY + dist * (higherY - lowerY);\r
254                                 \r
255                                 return Amount.valueOf( y, yUnit);\r
256                         }\r
257                 }\r
258         }\r
259         \r
260         private void drawDone(){\r
261         \r
262         }\r
263 \r
264         private volatile boolean stop = false;\r
265         private volatile int lastSkipStepShown;\r
266         public void setDomain(final Iterable<Amount<X>> d) {\r
267                 chart.getXYPlot().clearDomainMarkers();\r
268                 chart.getXYPlot().clearRangeMarkers();\r
269                 lastSkipStepShown = Integer.MAX_VALUE;\r
270                 stop = true;\r
271                 fill(d, 100);\r
272                 fast.submit(new Thread() {\r
273                         public void run() {\r
274                                 if (!stop)\r
275                                         fill(d, 10);\r
276                                 slow.submit(new Thread() {\r
277                                         public void run() {\r
278                                                 if (!stop){\r
279                                                         fill(d, 1);\r
280                                                 }\r
281                                         }\r
282                                 });\r
283                         }\r
284                 });\r
285         }\r
286 \r
287         @SuppressWarnings("unchecked")\r
288         private synchronized void fill(Iterable<Amount<X>> d, final int requestedSkip) {\r
289                 this.domain = d;\r
290                 \r
291                 log.debug(f.getName() + " " + requestedSkip + " Start");\r
292                 stop = false;\r
293                 int sz = 0;\r
294                 int calculatedSkip = requestedSkip;\r
295                 if (d instanceof Collection) {\r
296                         sz = ((Collection<Amount<X>>) d).size();\r
297                         int sk2 = sz / 200;\r
298                         if (calculatedSkip < sk2)\r
299                                 calculatedSkip = sk2;\r
300                 }\r
301                 // series.clear();\r
302                 int cnt = 0;\r
303 \r
304                 final XYSeries newSeries = new XYSeries(f.getName());\r
305                 try {\r
306                         Amount<X> last = null;\r
307                         for (Amount<X> ax : d) {\r
308                                 if (stop) {\r
309                                         log.debug(f.getName() + " " + calculatedSkip + " Abort");\r
310                                         return;\r
311                                 }\r
312                                 last = ax;\r
313                                 if (cnt % calculatedSkip == 0) {\r
314                                         Amount<Y> y = (Amount<Y>) f.invoke(source, ax);\r
315                                         newSeries.add(ax.doubleValue(xUnit), y.doubleValue(yUnit));\r
316                                 }\r
317                                 cnt++;\r
318                         }\r
319                         Amount<Y> y = (Amount<Y>) f.invoke(source, last);\r
320                         newSeries.add(last.doubleValue(xUnit), y.doubleValue(yUnit));\r
321                         SwingUtilities.invokeLater(new Thread() {\r
322                                 @Override\r
323                                 public void run() {\r
324                                         if ( requestedSkip < lastSkipStepShown ){\r
325                                                 lastSkipStepShown = requestedSkip;\r
326                                                 dataset.removeAllSeries();\r
327                                                 dataset.addSeries(newSeries);\r
328                                                 log.debug(f.getName() + " Replaced with " + requestedSkip);\r
329                                         }\r
330                                         if ( requestedSkip == 1 ){\r
331                                                 drawDone();\r
332                                         }\r
333                                 }\r
334                         });\r
335                 } catch (IllegalArgumentException e) {\r
336                         log.error(e);\r
337                 } catch (IllegalAccessException e) {\r
338                         log.error(e);\r
339                 } catch (InvocationTargetException e) {\r
340                         log.error(e);\r
341                 }\r
342                 log.debug(f.getName() + " " + calculatedSkip + " Done");\r
343         }\r
344 \r
345         public void show() {\r
346                 new JFrame() {\r
347                         private static final long serialVersionUID = 1L;\r
348                         {\r
349                                 setContentPane(Chart.this);\r
350                                 setSize(640, 480);\r
351                                 setDefaultCloseOperation(DISPOSE_ON_CLOSE);\r
352                         }\r
353                 }.setVisible(true);\r
354         }\r
355 \r
356         public static void main(String args[]) throws Exception {\r
357                 CoredCylindricalGrain g = new CoredCylindricalGrain();\r
358                 g.setLength(Amount.valueOf(70, SI.MILLIMETER));\r
359                 g.setOD(Amount.valueOf(30, SI.MILLIMETER));\r
360                 g.setID(Amount.valueOf(10, SI.MILLIMETER));\r
361 \r
362                 Chart<Length, Area> c = new Chart<Length, Area>(SI.MILLIMETER,\r
363                                 SI.MILLIMETER.pow(2).asType(Area.class), g, "surfaceArea");\r
364 \r
365                 c.setDomain(c.new IntervalDomain(Amount.valueOf(0, SI.CENTIMETER), g\r
366                                 .webThickness()));\r
367 \r
368                 c.show();\r
369 \r
370                 Chart<Length, Volume> v = new Chart<Length, Volume>(SI.MILLIMETER,\r
371                                 SI.MILLIMETER.pow(3).asType(Volume.class), g, "volume");\r
372 \r
373                 v.setDomain(c.new IntervalDomain(Amount.valueOf(0, SI.CENTIMETER), g\r
374                                 .webThickness()));\r
375 \r
376                 v.show();\r
377         }\r
378 \r
379 \r
380 }\r