001 package org.LiveGraph.plot;
002
003 import java.awt.Color;
004 import java.awt.Dimension;
005 import java.awt.Font;
006 import java.awt.FontMetrics;
007 import java.awt.Graphics;
008 import java.awt.Point;
009 import java.awt.Rectangle;
010 import java.awt.geom.AffineTransform;
011 import java.awt.geom.NoninvertibleTransformException;
012 import java.awt.geom.Point2D;
013 import java.awt.geom.Rectangle2D;
014 import java.io.FileNotFoundException;
015 import java.util.ArrayList;
016 import java.util.Arrays;
017 import java.util.Collections;
018 import java.util.Comparator;
019 import java.util.List;
020
021 import org.LiveGraph.LiveGraph;
022 import org.LiveGraph.dataCache.CacheEvent;
023 import org.LiveGraph.dataCache.DataCache;
024 import org.LiveGraph.dataCache.DataSeries;
025 import org.LiveGraph.dataCache.DataSet;
026 import org.LiveGraph.events.Event;
027 import org.LiveGraph.events.EventListener;
028 import org.LiveGraph.events.EventManager;
029 import org.LiveGraph.events.EventType;
030 import org.LiveGraph.settings.DataSeriesSettings;
031 import org.LiveGraph.settings.GraphSettings;
032 import org.LiveGraph.settings.ObservableSettings;
033 import org.LiveGraph.settings.SettingsEvent;
034 import org.LiveGraph.settings.DataSeriesSettings.TransformMode;
035 import org.LiveGraph.settings.GraphSettings.HGridType;
036 import org.LiveGraph.settings.GraphSettings.VGridType;
037 import org.LiveGraph.settings.GraphSettings.XAxisType;
038
039 import com.softnetConsult.utils.collections.Pair;
040 import com.softnetConsult.utils.exceptions.UnexpectedSwitchCase;
041 import com.softnetConsult.utils.math.MathTools;
042 import com.softnetConsult.utils.mutableWrappers.MutableInt;
043 import com.softnetConsult.utils.sys.SystemTools;
044
045
046 /**
047 * This class handles the conversion of the cached data to a screen image and the
048 * drawing of the image on a {@code Graphics} object.
049 * <br />
050 * This class uses an {@code AffineTransform} object to convert the data held in the
051 * cache to a data plot in screen coordinates. In order to keep the {@code AffineTransform}
052 * object appropriate for the current display at all times a plotter listens to
053 * various {@link DataCache} and {@link ObservableSettings} events; in addition it offers
054 * a {@link #setScreenSize(int, int)}-method which must be called each time when the
055 * canvas-panel that uses the plotter changes its size.
056 * <br />
057 * Whenever the {@link #dataCache} changes, a plotter uses the current {@link #datScrTransform}
058 * object to convert the data from the cache into a plot in screen coordinates according to
059 * the current global graph- and series-settings. The screen data obtained this way is locally
060 * cached in the {@link #screenDataBuffer} array. This way the data does not need to be
061 * re-computed each time the plot must be drawn on the screen.
062 * <br />
063 * In this version the plotter handles data values transformations required by the display
064 * options (if any) on the fly. If new options should be added to the interface, this mechanism
065 * should be replaces by a more flexible solution.
066 *
067 * <p style="font-size:smaller;">This product includes software developed by the
068 * <strong>LiveGraph</strong> project and its contributors.<br />
069 * (<a href="http://www.live-graph.org" target="_blank">http://www.live-graph.org</a>)<br />
070 * Copyright (c) 2007-2008 G. Paperin.<br />
071 * All rights reserved.
072 * </p>
073 * <p style="font-size:smaller;">File: Plotter.java</p>
074 * <p style="font-size:smaller;">Redistribution and use in source and binary forms, with or
075 * without modification, are permitted provided that the following terms and conditions are met:
076 * </p>
077 * <p style="font-size:smaller;">1. Redistributions of source code must retain the above
078 * acknowledgement of the LiveGraph project and its web-site, the above copyright notice,
079 * this list of conditions and the following disclaimer.<br />
080 * 2. Redistributions in binary form must reproduce the above acknowledgement of the
081 * LiveGraph project and its web-site, the above copyright notice, this list of conditions
082 * and the following disclaimer in the documentation and/or other materials provided with
083 * the distribution.<br />
084 * 3. All advertising materials mentioning features or use of this software or any derived
085 * software must display the following acknowledgement:<br />
086 * <em>This product includes software developed by the LiveGraph project and its
087 * contributors.<br />(http://www.live-graph.org)</em><br />
088 * 4. All advertising materials distributed in form of HTML pages or any other technology
089 * permitting active hyper-links that mention features or use of this software or any
090 * derived software must display the acknowledgment specified in condition 3 of this
091 * agreement, and in addition, include a visible and working hyper-link to the LiveGraph
092 * homepage (http://www.live-graph.org).
093 * </p>
094 * <p style="font-size:smaller;">THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY
095 * OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
096 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
097 * THE AUTHORS, CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
098 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
099 * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
100 * </p>
101 *
102 * @author Greg Paperin (<a href="http://www.paperin.org" target="_blank">http://www.paperin.org</a>)
103 * @version {@value org.LiveGraph.LiveGraph#version}
104 */
105 public class Plotter implements EventListener {
106
107 /**
108 * Vertical margin.
109 */
110 private static final int VMARGIN = 20;
111
112 /**
113 * Horisiontal margin.
114 */
115 private static final int HMARGIN = 20;
116
117 /*
118 * Minimum plot size.
119 */
120 private static final Dimension minScreenSize = new Dimension(150, 100);
121
122 /**
123 * Y axis colour.
124 */
125 private static final Color VAXIS_COL = Color.BLACK;
126
127 /**
128 * X axis colour.
129 */
130 private static final Color HAXIS_COL = Color.BLACK;
131
132 /**
133 * Label font size.
134 */
135 private static final int FONT_SIZE = 9;
136
137 /**
138 * Gap between axes labels.
139 */
140 private static final int AXES_LBL_GAP = 100;
141
142 /**
143 * Size of the scale marks on the axes.
144 */
145 private static final int AXES_MARKS_SIZE = 4;
146
147 /**
148 * Radius for datapoints marks on small graphs.
149 */
150 private static final int DATAPOINT_RAD = 3;
151
152 /**
153 * The minimum distance between grid lines (in pixels).
154 */
155 private static final int MIN_GRIDLINE_DIST = 3;
156
157
158 /**
159 * The data cache.
160 */
161 private DataCache dataCache = null;
162
163 /**
164 * Data series settings.
165 */
166 private DataSeriesSettings seriesSetts = null;
167
168 /**
169 * Graph settings.
170 */
171 private GraphSettings graphSetts = null;
172
173
174 /**
175 * Whether anythig at all is to be displayed.
176 */
177 private boolean showAtLeastOneSeries = false;
178
179 /**
180 * Buffers the screen coordinates of the graphs.
181 */
182 private SeriesScreenData[] screenDataBuffer = null;
183
184 /**
185 * Used to synchronise of the screen data buffer.
186 */
187 private Object screenDataBufferLock = new Object();
188
189 /**
190 * Buffers the x coordinates.
191 */
192 private double[] xCoordinates = null;
193
194 /**
195 * Used for sorting points by x values.
196 */
197 private PointsByIndexComparator pointsByIndexComparator = null;
198
199
200 /**
201 * Viewable area in data coordinates.
202 */
203 private Rectangle2D.Double dataViewport = null;
204
205 /**
206 * Screen size in pixels.
207 */
208 private Dimension screenSize = null;
209
210 /**
211 * Data space to screen space transformation.
212 */
213 private AffineTransform datScrTransform = null;
214
215
216 /**
217 * Whether screen data computation is in progress.
218 */
219 private boolean dataComputationRunning = false;
220
221 /**
222 * Whether screen data computation is in progress.
223 */
224 private boolean pointHighlightComputationRunning = false;
225
226 /**
227 * Whether the next change of h-grid settings was initiated by this plotter and should
228 * therefore be ignored by the plotter's handler.
229 */
230 private boolean selfSettingHGridSize = false;
231
232 /**
233 * The h-grid size get by a settings change that was not initiated by this
234 * plotter itself.
235 */
236 private double userHGridStep = Double.NaN;
237
238 /**
239 * The actual h-grid step after the consideration of plot size.
240 */
241 private double hGridStep = Double.NaN;
242
243 /**
244 * Whether the next change of v-grid settings was initiated by this plotter and should
245 * therefore be ignored by the plotter's handler.
246 */
247 private boolean selfSettingVGridSize = false;
248
249 /**
250 * The v-grid size get by a settings change that was not initiated by this
251 * plotter itself.
252 */
253 private double userVGridStep = Double.NaN;
254
255 /**
256 * The actual v-grid step after the consideration of plot size.
257 */
258 private double vGridStep = Double.NaN;
259
260
261 /**
262 * Whether dara points close to the mouse position should be highlighted.
263 */
264 private boolean highlightPoints = true;
265
266
267 /**
268 * Creates a plotter for the data held in the specified cache.
269 * @param dataCache Cache holding the data to plot.
270 */
271 public Plotter(DataCache dataCache) {
272
273 if (null == dataCache)
274 throw new NullPointerException("Plotter cannot act on a null cache");
275
276 this.dataCache = dataCache;
277
278 this.seriesSetts = LiveGraph.application().getDataSeriesSettings();
279 this.graphSetts = LiveGraph.application().getGraphSettings();
280
281 this.resetScreenDataBuffer();
282
283 this.xCoordinates = new double[DataCache.CACHE_SIZE];
284 this.pointsByIndexComparator = new PointsByIndexComparator();
285
286 this.dataViewport = new Rectangle2D.Double();
287 this.screenSize = new Dimension(minScreenSize);
288 this.datScrTransform = new AffineTransform();
289
290 this.dataComputationRunning = false;
291 this.pointHighlightComputationRunning = false;
292
293 this.selfSettingHGridSize = false;
294 this.userHGridStep = graphSetts.getHGridSize();
295 this.hGridStep = graphSetts.getHGridSize();
296
297 this.selfSettingVGridSize = false;
298 this.userVGridStep = graphSetts.getVGridSize();
299 this.vGridStep = graphSetts.getVGridSize();
300
301 this.computeGridSteps();
302
303 this.highlightPoints = true;
304
305 this.updateScreenData();
306 }
307
308 /**
309 * Gets whether the screen area is large enough to paint the graph.
310 *
311 * @return {@code true} iff the screen area is large enough to paint the graph.
312 */
313 public boolean screenTooSmall() {
314 return (minScreenSize.height > screenSize.height || minScreenSize.width > screenSize.width);
315 }
316
317 /**
318 * Gets whether at least one series is to be plotted.
319 *
320 * @return {@code true} if at seast one data series should be plotted, {@code false} otherwise.
321 */
322 public boolean getShowAtLeastOneSeries() {
323 return this.showAtLeastOneSeries;
324 }
325
326 /**
327 * Paints the previously computed graphs along with the axes, labels, grids and so on to the
328 * specified graphics context.
329 * @param g Paint context.
330 */
331 public void paint(Graphics g) {
332
333 // If screen is to small, just paint a message:
334 if (screenTooSmall()) {
335 g.setColor(Color.BLACK);
336 g.setFont(new Font(g.getFont().getName(), g.getFont().getStyle(), FONT_SIZE));
337 FontMetrics fMetrics = g.getFontMetrics();
338 g.drawString("LiveGraph " + LiveGraph.version, 2, fMetrics.getHeight() + 2);
339 g.drawString("Enlarge this window to see the plot.", 2, 2 * fMetrics.getHeight() + 4);
340 return;
341 }
342
343
344 // If there is nothing to show, just print a message:
345 if (!showAtLeastOneSeries) {
346 g.setColor(Color.BLACK);
347 g.setFont(new Font(g.getFont().getName(), g.getFont().getStyle(), FONT_SIZE));
348 FontMetrics fMetrics = g.getFontMetrics();
349 g.drawString("LiveGraph " + LiveGraph.version, 2, fMetrics.getHeight() + 2);
350 g.drawString("No data to display.", 2, 2 * fMetrics.getHeight() + 4);
351 return;
352 }
353
354
355 // If data computation is running, do not do anything:
356 if (dataComputationRunning)
357 return;
358
359 // Now do the actual painting:
360 synchronized(screenDataBufferLock) {
361 try {
362 paintGrids(g);
363 paintAxes(g);
364 paintData(g);
365 }catch (NullPointerException e) {
366 ; // Discart this weird exception.. Synch problem?
367 // Next redraw- better luck.
368 }
369 }
370
371 } // public void paint(Graphics g)
372
373
374 /**
375 * Paints the grid.
376 * @param g The graphics context.
377 */
378 private void paintGrids(Graphics g) {
379
380 // Plot horizontal grid:
381
382 if (HGridType.HGrid_Simple == graphSetts.getHGridType()) {
383
384 g.setColor(graphSetts.getHGridColour());
385
386 double dataViewportBottom = dataViewport.y - dataViewport.height;
387
388 double gy = dataViewportBottom + -(dataViewportBottom % hGridStep);
389 if (0 <= dataViewportBottom)
390 gy += hGridStep;
391
392 Point2D.Double p1 = new Point2D.Double();
393 Point2D.Double p2 = new Point2D.Double();
394
395 while (gy <= dataViewport.y) {
396
397 p1.setLocation(dataViewport.x, gy);
398 p2.setLocation(dataViewport.x + dataViewport.width, gy);
399 datScrTransform.transform(p1, p1);
400 datScrTransform.transform(p2, p2);
401 g.drawLine((int) p1.x, (int) p1.y, (int) p2.x, (int) p2.y);
402 gy += hGridStep;
403 }
404 }
405
406
407 // Plot vertical grid if it is alligned at x-axis units:
408
409 if (VGridType.VGrid_XAUnitAligned == graphSetts.getVGridType()) {
410
411 g.setColor(graphSetts.getVGridColour());
412
413 double gx = dataViewport.x + -(dataViewport.x % vGridStep);
414 if (0 <= dataViewport.x)
415 gx += vGridStep;
416
417 Point2D.Double p1 = new Point2D.Double();
418 Point2D.Double p2 = new Point2D.Double();
419
420 while (gx <= dataViewport.x + dataViewport.width) {
421
422 p1.setLocation(gx, dataViewport.y);
423 p2.setLocation(gx, dataViewport.y - dataViewport.height);
424 datScrTransform.transform(p1, p1);
425 datScrTransform.transform(p2, p2);
426 g.drawLine((int) p1.x, (int) p1.y, (int) p2.x, (int) p2.y);
427 gx += vGridStep;
428 }
429 }
430
431
432 // Plot vertical grid if it is alligned at dataset file indices:
433
434 // Get any (e.g. the first) data series which will be drawn:
435 SeriesScreenData firstSeriesVisible = null;
436 for (int s = 0; s < screenDataBuffer.length; s++) {
437 if (screenDataBuffer[s].doShow) {
438 firstSeriesVisible = screenDataBuffer[s];
439 break;
440 }
441 }
442
443 if (VGridType.VGrid_DSNumAligned == graphSetts.getVGridType()) {
444
445 g.setColor(graphSetts.getVGridColour());
446
447 int gy1 = VMARGIN;
448 int gy2 = VMARGIN + screenSize.height;
449
450 int curDSInd, gx;
451 int[] fsvDSIndices = firstSeriesVisible.dsIndices;
452 int lastDSInd = fsvDSIndices[0];
453 boolean forcePlot = dataViewport.x < (double) lastDSInd;
454
455 for (int p = 0; p < firstSeriesVisible.plotPoints; p++) {
456
457 curDSInd = firstSeriesVisible.dsIndices[p];
458 if (curDSInd - lastDSInd >= vGridStep || forcePlot) {
459 gx = (int) firstSeriesVisible.points[p].x;
460 g.drawLine(gx, gy1, gx, gy2);
461 lastDSInd = curDSInd;
462 forcePlot = false;
463 }
464
465 }
466 }
467 } // private void paintGrids
468
469
470 /**
471 * Paints the coordinate axes.
472 * @param g The graphics context.
473 */
474 private void paintAxes(Graphics g) {
475
476 // Setup font:
477
478 Font font = g.getFont();
479 g.setFont(new Font(font.getName(), font.getStyle(), FONT_SIZE));
480 FontMetrics fMetrics = g.getFontMetrics();
481
482 // Plot horizontal axis:
483
484 int xAxisY = VMARGIN + screenSize.height;
485
486 g.setColor(HAXIS_COL);
487 g.drawLine(HMARGIN / 2, xAxisY, HMARGIN * 3 / 2 + screenSize.width, xAxisY);
488
489 // Find propper rounding:
490 Point h1s = new Point(HMARGIN, xAxisY);
491 Point h2s = new Point(HMARGIN * 3 / 2 + screenSize.width, xAxisY);
492 Point2D.Double h1d = screenToDataPoint(h1s);
493 Point2D.Double h2d = screenToDataPoint(h2s);
494 double hDiff = Math.abs(h1d.x - h2d.x);
495 int roundExpH = -1 + (int) Math.rint(MathTools.log(10.0, hDiff));
496 String formatH = "%.0f";
497 if (roundExpH < 0)
498 formatH = "%." + (-roundExpH) + "f";
499
500 boolean xAxisTime = (graphSetts.getXAxisType() == XAxisType.XAxis_DataValSecsToSetPower);
501
502 // Find location for the first label (after the one that is directly on the axes cross):
503 h1s.setLocation(HMARGIN, xAxisY);
504 h1d = screenToDataPoint(h1s);
505
506 h2s.setLocation(HMARGIN + AXES_LBL_GAP * (xAxisTime ? 2 : 1), xAxisY);
507 h2d = screenToDataPoint(h2s);
508
509 h2d.setLocation(MathTools.round(h2d.x, roundExpH), h2d.y);
510 datScrTransform.transform(h2d, h2s);
511 int firstHLabelXS = h2s.x;
512 double firstHLabelXD = h2d.x;
513
514 // Find label gap for all subsequent labels:
515 h1s.setLocation(h2s);
516 h2s.setLocation(h2s.x + AXES_LBL_GAP * (xAxisTime ? 2 : 1), xAxisY);
517 h2d = screenToDataPoint(h2s);
518 h2d.setLocation(MathTools.round(h2d.x, roundExpH), h2d.y);
519 datScrTransform.transform(h2d, h2s);
520 int xAxisLabelGap = h2s.x - h1s.x;
521 if (0 >= xAxisLabelGap)
522 xAxisLabelGap = AXES_LBL_GAP * (xAxisTime ? 2 : 1);
523
524 // Paint labels:
525 int lh = 0;
526 while(lh < 50) { // Something keeps going wrong, so provide an exit condition
527
528 String lbl;
529 if (0 == lh) {
530 h1s.setLocation(HMARGIN, xAxisY);
531 h1d = screenToDataPoint(h1s);
532
533 if (xAxisTime) {
534 long d = Math.round(h1d.x * 1000.0);
535 if (d > 185542587187199999L)
536 lbl = "more than " + (h1d.x < 0. ? "-" : "") + Integer.MAX_VALUE + "days";
537 else
538 lbl = (h1d.x < 0. ? "-" : "") + SystemTools.formatTimePeriod(0L, d);
539
540 } else {
541 StringBuffer b = new StringBuffer(String.format("%f", h1d.x));
542 int bl = b.length();
543 while(0 < bl && '0' == b.charAt(bl - 1))
544 b.setLength(--bl);
545
546 if (0 == bl || !Character.isDigit(b.charAt(bl - 1)))
547 b.append('0');
548
549 lbl = b.toString();
550 }
551
552 } else if (1 == lh) {
553 h1s.setLocation(firstHLabelXS, xAxisY);
554 h1d.setLocation(firstHLabelXD, h1d.y);
555 if (xAxisTime) {
556 long d = Math.round(h1d.x * 1000.0);
557 if (d > 185542587187199999L)
558 lbl = "more than " + (h1d.x < 0. ? "-" : "") + Integer.MAX_VALUE + "days";
559 else
560 lbl = (h1d.x < 0. ? "-" : "") + SystemTools.formatTimePeriod(0L, d);
561 } else {
562 lbl = String.format(formatH, h1d.x);
563 }
564
565 } else {
566 h2s.setLocation(h1s);
567 h1s.setLocation(h1s.x + xAxisLabelGap, xAxisY);
568 h1d = screenToDataPoint(h1s);
569 h1d.setLocation(MathTools.round(h1d.x, roundExpH), h1d.y);
570 datScrTransform.transform(h1d, h1s);
571
572 int abort = 50; // Something keeps going wrong, so provide an exit condition
573 while(h1s.x == h2s.x && abort > 0) {
574 h1d.setLocation(MathTools.round(h1d.x + Math.pow(10.0, roundExpH), roundExpH), h1d.y);
575 datScrTransform.transform(h1d, h1s);
576 abort--;
577 }
578
579 if (xAxisTime) {
580 long d = Math.round(h1d.x * 1000.0);
581 if (d > 185542587187199999L)
582 lbl = "more than " + (h1d.x < 0. ? "-" : "") + Integer.MAX_VALUE + "days";
583 else
584 lbl = (h1d.x < 0. ? "-" : "") + SystemTools.formatTimePeriod(0L, d);
585 } else {
586 lbl = String.format(formatH, h1d.x);
587 }
588 }
589
590 if (h1s.x + 2 + fMetrics.stringWidth(lbl) + 2 > HMARGIN * 2 + screenSize.width)
591 break;
592 if (HMARGIN < h1s.x)
593 g.drawLine(h1s.x, xAxisY - AXES_MARKS_SIZE / 2, h1s.x, xAxisY + AXES_MARKS_SIZE / 2);
594 g.drawString(lbl, h1s.x + 2, xAxisY + AXES_MARKS_SIZE / 2 + fMetrics.getHeight());
595 lh++;
596 } // while(lh < 1000)
597
598
599 // Plot vertical axis:
600
601 int yAxisX = HMARGIN;
602
603 g.setColor(VAXIS_COL);
604 g.drawLine(yAxisX, VMARGIN * 3 / 2 + screenSize.height, yAxisX, VMARGIN / 2);
605
606 // Find propper rounding:
607 Point v1s = new Point(yAxisX, VMARGIN + screenSize.height);
608 Point v2s = new Point(yAxisX, VMARGIN / 2);
609 Point2D.Double v1d = screenToDataPoint(v1s);
610 Point2D.Double v2d = screenToDataPoint(v2s);
611 double vDiff = Math.abs(v1d.y - v2d.y);
612 int roundExpV = -1 + (int) Math.rint(MathTools.log(10.0, vDiff));
613 String formatV = "%.0f";
614 if (roundExpV < 0)
615 formatV = "%." + (-roundExpV) + "f";
616
617
618 // Find location for the first label (after the one that is directly on the axes cross):
619 v1s.setLocation(yAxisX, VMARGIN + screenSize.height);
620 v1d = screenToDataPoint(v1s);
621
622 v2s.setLocation(yAxisX, VMARGIN + screenSize.height - AXES_LBL_GAP);
623 v2d = screenToDataPoint(v2s);
624
625 v2d.setLocation(v2d.x, MathTools.round(v2d.y, roundExpV));
626 datScrTransform.transform(v2d, v2s);
627 int firstVLabelYS = v2s.y;
628 double firstVLabelYD = v2d.y;
629
630 // Find label gap for all subsequent labels:
631 v1s.setLocation(v2s);
632 v2s.setLocation(yAxisX, v2s.y - AXES_LBL_GAP);
633 v2d = screenToDataPoint(v2s);
634 v2d.setLocation(v2d.x, MathTools.round(v2d.y, roundExpV));
635 datScrTransform.transform(v2d, v2s);
636 int yAxisLabelGap = v2s.y - v1s.y;
637 if (0 >= yAxisLabelGap)
638 yAxisLabelGap = AXES_LBL_GAP;
639
640 // Paint labels:
641 int lv = 0;
642 while(lv < 50) { // Something keeps going wrong, so provide an exit condition
643
644 String lbl;
645 if (0 == lv) {
646 v1s.setLocation(yAxisX, VMARGIN + screenSize.height);
647 v1d = screenToDataPoint(v1s);
648
649 StringBuffer b = new StringBuffer(String.format("%f", v1d.y));
650 int bl = b.length();
651 while(0 < bl && '0' == b.charAt(bl - 1))
652 b.setLength(--bl);
653
654 if (0 == bl || !Character.isDigit(b.charAt(bl - 1)))
655 b.append('0');
656
657 lbl = b.toString();
658
659 } else if (1 == lv) {
660 v1s.setLocation(yAxisX, firstVLabelYS);
661 v1d.setLocation(v1d.x, firstVLabelYD);
662 lbl = String.format(formatV, v1d.y);
663
664 } else {
665 v2s.setLocation(v1s);
666 v1s.setLocation(yAxisX, v1s.y - yAxisLabelGap);
667 v1d = screenToDataPoint(v1s);
668 v1d.setLocation(v1d.x, MathTools.round(v1d.y, roundExpV));
669 datScrTransform.transform(v1d, v1s);
670
671 int abort = 50; // Something keeps going wrong, so provide an exit condition
672 while(v1s.y == v2s.y && abort > 0) {
673 v1d.setLocation(v1d.x, MathTools.round(v1d.y + Math.pow(10.0, roundExpV), roundExpV));
674 datScrTransform.transform(v1d, v1s);
675 abort--;
676 }
677
678 lbl = String.format(formatV, v1d.y);
679 }
680
681 if (v1s.y - 2 - fMetrics.getHeight() - 2 < 0)
682 break;
683
684 if (VMARGIN + screenSize.height > v1s.y)
685 g.drawLine(yAxisX - AXES_MARKS_SIZE / 2, v1s.y, yAxisX + AXES_MARKS_SIZE / 2, v1s.y);
686
687 g.drawString(lbl, yAxisX + AXES_MARKS_SIZE / 2 + 2, v1s.y - 2);
688
689 lv++;
690 } // while(lv < 1000)
691
692
693
694 } // private void paintAxes
695
696
697 /**
698 * Paints the data series.
699 * @param g The graphics context.
700 */
701 private void paintData(Graphics g) {
702
703 // Get any (e.g. the first) data series which will be drawn:
704 if (null == screenDataBuffer)
705 return;
706 SeriesScreenData firstSeriesVisible = null;
707 for (int s = 0; s < screenDataBuffer.length; s++) {
708 if (screenDataBuffer[s].doShow) {
709 firstSeriesVisible = screenDataBuffer[s];
710 break;
711 }
712 }
713
714 // Plot data:
715 boolean drawPoints = true;
716 if (0 < firstSeriesVisible.plotPoints)
717 drawPoints = (screenSize.width / firstSeriesVisible.plotPoints > 4 * DATAPOINT_RAD);
718
719 SeriesScreenData series = null;
720 for (int s = 0; s < screenDataBuffer.length; s++) {
721
722 series = screenDataBuffer[s];
723 if (!series.doShow)
724 continue;
725
726 g.setColor(series.colour);
727
728 Point2D.Double[] points = series.points;
729 int x1 = (int) points[0].x;
730 int y1 = (int) points[0].y;
731 int x2, y2;
732 boolean connect = true;
733 for (int p = 0; p < series.plotPoints; p++) {
734 if (Double.isNaN(points[p].x) || Double.isNaN(points[p].y)
735 || Double.isInfinite(points[p].x) || Double.isInfinite(points[p].y)) {
736 connect = false;
737 continue;
738 }
739 x2 = (int) points[p].x;
740 y2 = (int) points[p].y;
741 if (!connect) {
742 x1 = x2;
743 y1 = y2;
744 connect = true;
745 }
746 g.drawLine(x1, y1, x2, y2);
747 if (drawPoints) {
748 g.drawLine(x2 - DATAPOINT_RAD, y2 - DATAPOINT_RAD, x2 + DATAPOINT_RAD, y2 + DATAPOINT_RAD);
749 g.drawLine(x2 - DATAPOINT_RAD, y2 + DATAPOINT_RAD, x2 + DATAPOINT_RAD, y2 - DATAPOINT_RAD);
750 g.drawLine(x2, y2 + DATAPOINT_RAD, x2, y2 - DATAPOINT_RAD);
751 g.drawLine(x2 - DATAPOINT_RAD, y2, x2 + DATAPOINT_RAD, y2);
752 }
753 if (series.hlPoints[p]) {
754 g.drawOval(x2 - DATAPOINT_RAD - 1, y2 - DATAPOINT_RAD - 1, 2 + 2 * DATAPOINT_RAD, 2 + 2 * DATAPOINT_RAD);
755 }
756 x1 = x2;
757 y1 = y2;
758 }
759 }
760 } // private void paintData
761
762
763 /**
764 * Computes the screen coordinates for the visible data series.
765 */
766 private synchronized void computeScreenData() {
767
768 if (dataComputationRunning)
769 return;
770
771 if (screenTooSmall())
772 return;
773
774 if (null == screenDataBuffer)
775 return;
776
777 dataComputationRunning = true;
778
779 computeXCoordinates();
780
781 showAtLeastOneSeries = false;
782 for (int s = 0; s < screenDataBuffer.length; s++) {
783
784 if (!seriesSetts.getShow(s)) {
785 screenDataBuffer[s].doShow = false;
786 continue;
787 }
788
789 screenDataBuffer[s].doShow = true;
790 showAtLeastOneSeries = true;
791 computeScreenDataForSeries(s);
792 }
793
794 dataComputationRunning = false;
795 }
796
797 /**
798 * Compute the x coordinates in data space according to the current settings.
799 */
800 private void computeXCoordinates() {
801
802 // If the option is to use a data series, but the index is invalid, we default to dataset numbers:
803
804 synchronized(dataCache) {
805
806 XAxisType xAxisType = graphSetts.getXAxisType();
807 int xSerInd = -1;
808 if (XAxisType.XAxis_DSNum != xAxisType) {
809 xSerInd = graphSetts.getXAxisSeriesIndex();
810 if (0 > xSerInd || dataCache.countDataSeries() <= xSerInd)
811 xAxisType = XAxisType.XAxis_DSNum;
812 }
813
814 // Now we can follow the secure option:
815
816 int dataLen = dataCache.countDataSets();
817
818 switch(xAxisType) {
819
820 case XAxis_DSNum:
821 for (int i = 0; i < dataLen; i++)
822 xCoordinates[i] = dataCache.getDataSet(i).getDataFileIndex();
823 break;
824
825 case XAxis_DataValSimple:
826 for (int i = 0; i < dataLen; i++)
827 xCoordinates[i] = dataCache.getDataSet(i).getValue(xSerInd);
828 break;
829
830 case XAxis_DataValScaleBySetVal:
831 double scaleFactor = graphSetts.getXAxisParamValue();
832 for (int i = 0; i < dataLen; i++)
833 xCoordinates[i] = dataCache.getDataSet(i).getValue(xSerInd) * scaleFactor;
834 break;
835
836 case XAxis_DataValTrans0to1:
837 DataSeries xSer = dataCache.getDataSeries(xSerInd);
838 double transfShift = xSer.getMinValue();
839 double transfScale = xSer.getMaxValue() - transfShift;
840 transfScale = (0 == transfScale ? 0. : 1. / transfScale);
841 for (int i = 0; i < dataLen; i++)
842 xCoordinates[i] = (dataCache.getDataSet(i).getValue(xSerInd) - transfShift) * transfScale;
843 break;
844
845 case XAxis_DataValLogToSetBase:
846 double base = graphSetts.getXAxisParamValue();
847 for (int i = 0; i < dataLen; i++)
848 xCoordinates[i] = MathTools.log(base, dataCache.getDataSet(i).getValue(xSerInd));
849 break;
850
851 case XAxis_DataValSecsToSetPower:
852 double power = graphSetts.getXAxisParamValue();
853 double secondsFactor = Math.pow(10.0, power);
854 for (int i = 0; i < dataLen; i++)
855 xCoordinates[i] = dataCache.getDataSet(i).getValue(xSerInd) * secondsFactor;
856 break;
857
858 default:
859 throw new UnexpectedSwitchCase(xAxisType);
860 } // switch(xAxisType)
861 } // synchronized(dataCache)
862 }
863
864 /**
865 * Compute the screen coordinates for the specified series.
866 *
867 * @param seriesIndex The cache index of the series to be computed.
868 */
869 private void computeScreenDataForSeries(int seriesIndex) {
870
871 boolean dataComputationWasRunning = dataComputationRunning;
872 dataComputationRunning = true;
873
874 // Preset data:
875 SeriesScreenData scrData = screenDataBuffer[seriesIndex];
876 scrData.plotPoints = 0;
877 int sp = 0;
878 synchronized(dataCache) {
879 int dataPointCount = dataCache.countDataSets();
880
881 // Look at each data point of the series:
882 double x, y;
883 DataSet ds;
884 for (int dp = 0; dp < dataPointCount; dp++) {
885
886 // Get raw Y and X:
887 ds = dataCache.getDataSet(dp);
888 y = ds.getValue(seriesIndex);
889 y = scrData.transformer.transf(y);
890 x = xCoordinates[dp];
891
892 // Transform the point to screen coordinates:
893 scrData.dsIndices[sp] = ds.getDataFileIndex();
894 scrData.points[sp].y = y;
895 scrData.points[sp].x = x;
896 datScrTransform.transform(scrData.points[sp], scrData.points[sp]);
897
898 // Save point index for latter sorting:
899 scrData.sortedPoints[sp].value = sp;
900
901 sp++;
902 }
903 } // synchronized(dataCache)
904
905 // Save the number of points actually computed:
906 scrData.plotPoints = sp;
907
908 // Sort points for fast access when highlighting with the mouse:
909 if (highlightPoints) {
910 Arrays.fill(scrData.hlPoints, 0, sp, false);
911 pointsByIndexComparator.setSeries(scrData);
912 Arrays.sort(scrData.sortedPoints, 0, sp, pointsByIndexComparator);
913 }
914
915 dataComputationRunning = dataComputationWasRunning;
916 }
917
918
919 /**
920 * Highlights the points around the specified point.
921 * This is normally called when the mouse is moved over the plotter canvas.
922 *
923 * @param sp A marker screen point.
924 * @return A list of series indices on which at least one point was highlighted.
925 */
926 public List<Integer> highlightAround(Point sp) {
927
928 // If highlighting should not be done for a reason, we do not highlight anything:
929 if (pointHighlightComputationRunning || dataComputationRunning || !highlightPoints) {
930 List<Integer> hlSeries = Collections.emptyList();
931 return hlSeries;
932 }
933
934 pointHighlightComputationRunning = true;
935
936 List<Integer> hlSeries = new ArrayList<Integer>();
937
938 // Get the rectabgle within which points will be highlighted:
939 Rectangle sRect = new Rectangle(sp.x - DATAPOINT_RAD - 1, sp.y - DATAPOINT_RAD - 1,
940 1 + 2 * (DATAPOINT_RAD + 1),1 + 2 * (DATAPOINT_RAD + 1));
941
942 // Look for points to highlight on each series:
943 SeriesScreenData series;
944 for (int s = 0; s < screenDataBuffer.length; s++) {
945
946 series = screenDataBuffer[s];
947 if (0 == series.plotPoints)
948 continue;
949
950 // Skip series which are not plotted:
951 if (null == series || !series.doShow)
952 continue;
953
954 // Clear highlight flags fopr all points of the series:
955 boolean hlThisSeries = false;
956 Arrays.fill(series.hlPoints, 0, series.plotPoints, false);
957
958 // Find index at which the marker point would be inserted into the x-sorted series points array:
959 pointsByIndexComparator.setSeries(series);
960 series.points[DataCache.CACHE_SIZE].setLocation(sp.x, sp.y);
961 int mi = Arrays.binarySearch(series.sortedPoints, 0, series.plotPoints,
962 new MutableInt(DataCache.CACHE_SIZE), pointsByIndexComparator);
963 if (0 > mi) mi = -mi;
964
965 // Extend array index to the left to include all points within the selection rectangle:
966 int li = mi;
967 if (li >= series.plotPoints) {
968 li = series.plotPoints - 1;
969 } else {
970 while (0 <= li && series.points[series.sortedPoints[li].value].x >= sRect.x)
971 li--;
972 if (-1 == li) li = 0;
973 }
974
975 // Extend array index to the right to include all points within the selection rectangle:
976 int ri = mi;
977 int sRectRB = sRect.x + sRect.width;
978 while (0 <= ri && ri < series.plotPoints && series.points[series.sortedPoints[ri].value].x <= sRectRB)
979 ri++;
980 if (ri >= series.plotPoints) ri = series.plotPoints - 1;
981
982 // Now loop through the points within the determined index boundaries:
983 for (int i = li; i <= ri; i++) {
984
985 // Highlight a point iff it actually lies within the selection rectangle:
986 if (sRect.contains(series.points[series.sortedPoints[i].value])) {
987 series.hlPoints[series.sortedPoints[i].value] = true;
988 hlThisSeries = true;
989 }
990 }
991
992 // If at least one point on the series was highlighted,
993 // than we add the series index to the highlighted series list:
994 if (hlThisSeries)
995 hlSeries.add(s);
996
997 }
998
999 // Done:
1000 pointHighlightComputationRunning = false;
1001 return hlSeries;
1002 }
1003
1004 /**
1005 * Computes the actual grid mesh sizes taking in account the current plot size.
1006 */
1007 private void computeGridSteps() {
1008
1009 // For horizontal grid:
1010
1011 if (HGridType.HGrid_Simple == graphSetts.getHGridType()) {
1012
1013 boolean hStepChanged = false;
1014
1015 if (hGridStep != userHGridStep) {
1016 hGridStep = userHGridStep;
1017 hStepChanged = true;
1018 }
1019
1020 if (hGridStep < 0.0) {
1021 hGridStep = -hGridStep;
1022 hStepChanged = true;
1023 }
1024
1025 double minHGridStep = dataViewport.height * MIN_GRIDLINE_DIST / screenSize.height;
1026 if (hGridStep < minHGridStep
1027 && !Double.isInfinite(dataViewport.height) && 0. != dataViewport.height) {
1028
1029 hGridStep = minHGridStep;
1030 double rounded = MathTools.round(hGridStep, -3);
1031 if (rounded < hGridStep)
1032 hGridStep = MathTools.round(rounded + 0.001, -3);
1033 else
1034 hGridStep = rounded;
1035
1036 hStepChanged = true;
1037 }
1038
1039 if (hStepChanged) {
1040 selfSettingHGridSize = true;
1041 graphSetts.setHGridSize(hGridStep);
1042 selfSettingHGridSize = false;
1043 }
1044 }
1045
1046 // For vertical grid if it is x-axis unit-alligned:
1047
1048 if (VGridType.VGrid_XAUnitAligned == graphSetts.getVGridType()) {
1049
1050 boolean vStepChanged = false;
1051
1052 if (vGridStep != userVGridStep) {
1053 vGridStep = userVGridStep;
1054 vStepChanged = true;
1055 }
1056
1057 if (vGridStep < 0.0) {
1058 vGridStep = -vGridStep;
1059 vStepChanged = true;
1060 }
1061
1062 double minVGridStep = dataViewport.width * MIN_GRIDLINE_DIST / screenSize.width;
1063 if (vGridStep < minVGridStep
1064 && !Double.isInfinite(dataViewport.width) && 0. != dataViewport.width) {
1065
1066 vGridStep = minVGridStep;
1067 double rounded = MathTools.round(vGridStep, -3);
1068 if (rounded < vGridStep)
1069 vGridStep = MathTools.round(rounded + 0.001, -3);
1070 else
1071 vGridStep = rounded;
1072
1073 vStepChanged = true;
1074 }
1075
1076 if (vStepChanged) {
1077 selfSettingVGridSize = true;
1078 graphSetts.setVGridSize(vGridStep);
1079 selfSettingVGridSize = false;
1080 }
1081 }
1082
1083 // For vertical grid if it is dataset-alligned:
1084
1085 if (VGridType.VGrid_DSNumAligned == graphSetts.getVGridType()) {
1086
1087 boolean vStepChanged = false;
1088
1089 if (vGridStep != userVGridStep) {
1090 vGridStep = userVGridStep;
1091 vStepChanged = true;
1092 }
1093
1094 double rounded = Math.rint(vGridStep);
1095 if (rounded != vGridStep) {
1096 vGridStep = rounded;
1097 vStepChanged = true;
1098 }
1099
1100 if (vGridStep < 0.0) {
1101 vGridStep = -vGridStep;
1102 vStepChanged = true;
1103 }
1104
1105 if (vGridStep == 0.0) {
1106 vGridStep = 1.;
1107 vStepChanged = true;
1108 }
1109
1110 if (vStepChanged) {
1111 selfSettingVGridSize = true;
1112 graphSetts.setVGridSize(vGridStep);
1113 selfSettingVGridSize = false;
1114 }
1115 }
1116 } // private void computeGridSteps()
1117
1118 /**
1119 * Map the specified point in screen coordinates into the data space.
1120 *
1121 * @param sp A point in screen coordinates.
1122 * @return The corresponding data point.
1123 */
1124 public Point2D.Double screenToDataPoint(Point sp) {
1125
1126 Point2D.Double dp = new Point2D.Double();
1127 try {
1128 datScrTransform.inverseTransform(sp, dp);
1129 } catch(NoninvertibleTransformException e) {
1130 dp.setLocation(0, 0);
1131 }
1132 return dp;
1133 }
1134
1135 /**
1136 * Updates the data to screen transform map according to the currently visible data area and screen size.
1137 */
1138 private void updateDatScrTransform() {
1139
1140 datScrTransform.setToIdentity();
1141
1142 datScrTransform.translate(HMARGIN, (screenSize.height + VMARGIN));
1143 datScrTransform.scale(1, -1);
1144 datScrTransform.scale(screenSize.width / dataViewport.width, screenSize.height / dataViewport.height);
1145 datScrTransform.translate(-dataViewport.x, dataViewport.height - dataViewport.y);
1146 }
1147
1148
1149 /**
1150 * Reallocates the screen data buffer.
1151 */
1152 private void resetScreenDataBuffer() {
1153 synchronized(screenDataBufferLock) {
1154 showAtLeastOneSeries = false;
1155 screenDataBuffer = new SeriesScreenData[dataCache.countDataSeries()];
1156 for (int s = 0; s < screenDataBuffer.length; s++) {
1157 screenDataBuffer[s] = new SeriesScreenData(s);
1158 screenDataBuffer[s].colour = seriesSetts.getColour(s);
1159 updateSeriesTransformer(s);
1160 }
1161 }
1162 }
1163
1164
1165 /**
1166 * First, recomputes the currently visible data area according to the current graph and series settings;
1167 * then, computes the screen coordinates for the visible data series.
1168 *
1169 */
1170 private void updateScreenData() {
1171 resetDataViewport();
1172 computeScreenData();
1173 }
1174
1175 /**
1176 * Updates the Transformer for the specified series (Transformer is the object that applies the
1177 * data transformation set in the series settings).
1178 *
1179 * @param seriesIndex The series index for which the Transformer must be updated.
1180 */
1181 private void updateSeriesTransformer(final int seriesIndex) {
1182
1183 if (null == screenDataBuffer)
1184 return;
1185
1186 if (seriesIndex < 0 || screenDataBuffer.length <= seriesIndex)
1187 return;
1188
1189 SeriesScreenData serData = screenDataBuffer[seriesIndex];
1190 if (null == serData)
1191 return;
1192
1193 TransformMode transformMode = seriesSetts.getTransformMode(seriesIndex);
1194 switch (transformMode) {
1195 case Transform_None: serData.transformer = IDTransform;
1196 break;
1197
1198 case Transform_ScaleBySetVal: final double f = seriesSetts.getTransformParam(seriesIndex);
1199 serData.transformer = new Transformer() {
1200 public final double transf(final double v) { return v * f; }
1201 };
1202 break;
1203
1204 case Transform_In0to1: synchronized(dataCache) {
1205 if (0 <= seriesIndex && seriesIndex < dataCache.countDataSeries()) {
1206 DataSeries dSer = dataCache.getDataSeries(seriesIndex);
1207 double serMax = dSer.getMaxValue();
1208 final double shift = dSer.getMinValue();
1209 final double scale = (0. == (serMax - shift)
1210 ? 0.
1211 : 1. / (serMax - shift));
1212 serData.transformer = new Transformer() {
1213 public final double transf(final double v) {
1214 return (v - shift) * scale;
1215 }
1216 };
1217 } else {
1218 serData.transformer = new Transformer() {
1219 public final double transf(final double v) {
1220 return 0.5;
1221 }
1222 };
1223 }
1224 }
1225 break;
1226
1227 case Transform_Logarithm: final double b = seriesSetts.getTransformParam(seriesIndex);
1228 serData.transformer = new Transformer() {
1229 public final double transf(final double v) {
1230 return MathTools.log(b, v);
1231 }
1232 };
1233 break;
1234
1235 default: throw new UnexpectedSwitchCase(transformMode);
1236 }
1237 }
1238
1239 /**
1240 * Recomputes the currently visible data area according to the current graph and series settings.
1241 */
1242 private void resetDataViewport() {
1243
1244 // If current screen is too small we dont compute:
1245 if (screenTooSmall())
1246 return;
1247
1248 // If computation is running, we do not compute any more:
1249 if (dataComputationRunning)
1250 return;
1251
1252 // Determine minY according to the options:
1253 double minY = graphSetts.getMinY();
1254 if (Double.isNaN(minY)) {
1255
1256 minY = Double.MAX_VALUE;
1257 synchronized(dataCache) {
1258 int serCnt = Math.min(dataCache.countDataSeries(), screenDataBuffer.length);
1259 for (int s = 0; s < serCnt; s++) {
1260
1261 if (!seriesSetts.getShow(s))
1262 continue;
1263
1264 double v = screenDataBuffer[s].transformer.transf(dataCache.getDataSeries(s).getMinValue());
1265 if (v < minY && !Double.isInfinite(v))
1266 minY = v;
1267 }
1268 }
1269 }
1270
1271 // Determine maxY according to the options:
1272 double maxY = graphSetts.getMaxY();
1273 if (Double.isNaN(maxY)) {
1274
1275 maxY = -Double.MAX_VALUE;
1276 synchronized(dataCache) {
1277 int serCnt = Math.min(dataCache.countDataSeries(), screenDataBuffer.length);
1278 for (int s = 0; s < serCnt; s++) {
1279
1280 if (!seriesSetts.getShow(s))
1281 continue;
1282
1283 double v = screenDataBuffer[s].transformer.transf(dataCache.getDataSeries(s).getMaxValue());
1284 if (v > maxY && !Double.isInfinite(v))
1285 maxY = v;
1286 }
1287 }
1288 }
1289
1290 // Determine minX and maxX accodring to the options:
1291 double minX = graphSetts.getMinX();
1292 double maxX = graphSetts.getMaxX();
1293
1294 // Get the X-Axis type:
1295 XAxisType xAxisType = graphSetts.getXAxisType();
1296
1297 // If minX or maxX are automatic and according to some data value (i.e. not dataset number):
1298 if ( (Double.isNaN(minX) || Double.isNaN(maxX) ) && xAxisType != XAxisType.XAxis_DSNum) {
1299
1300 // Check that x-axis data series index is valid and fetch min & max values:
1301 int xAxisSerIndex = graphSetts.getXAxisSeriesIndex();
1302 DataSeries xSer = null;
1303 double dsMinX = Double.NaN;
1304 double dsMaxX = Double.NaN;
1305 synchronized(dataCache) {
1306 if (0 <= xAxisSerIndex && dataCache.countDataSeries() > xAxisSerIndex) {
1307 xSer = dataCache.getDataSeries(xAxisSerIndex);
1308 dsMinX = xSer.getMinValue();
1309 dsMaxX = xSer.getMaxValue();
1310 }
1311 }
1312 // If x-axis data series index is valid:
1313 if (null != xSer) {
1314 switch(xAxisType) {
1315
1316 // X axis is an unscaled data series:
1317 case XAxis_DataValSimple:
1318 if (Double.isNaN(minX))
1319 minX = dsMinX;
1320 if (Double.isNaN(maxX))
1321 maxX = dsMaxX;
1322 break;
1323
1324 // X axis is a data series transformed into [0..1]:
1325 case XAxis_DataValTrans0to1:
1326 if (Double.isNaN(minX))
1327 minX = 0;
1328 if (Double.isNaN(maxX))
1329 maxX = 1;
1330 break;
1331
1332 // X axis is a data series scaled by a set value:
1333 case XAxis_DataValScaleBySetVal:
1334 double scaleF = graphSetts.getXAxisParamValue();
1335 if (Double.isNaN(minX))
1336 minX = dsMinX * scaleF;
1337 if (Double.isNaN(maxX))
1338 maxX = dsMaxX * scaleF;
1339 break;
1340
1341 // X axis is a the logarithm of a data series to a set base:
1342 case XAxis_DataValLogToSetBase:
1343 double base = graphSetts.getXAxisParamValue();
1344 if (Double.isNaN(minX)) {
1345 double v = MathTools.log(base, dsMinX);
1346 minX = (Double.isInfinite(v) || Double.isNaN(v) ? minX : v);
1347 }
1348 if (Double.isNaN(maxX)) {
1349 double v = MathTools.log(base, dsMaxX);
1350 maxX = (Double.isInfinite(v) || Double.isNaN(v) ? maxX : v);
1351 }
1352 break;
1353
1354 // X axis is seconds as data series values tken to a set power of 10:
1355 case XAxis_DataValSecsToSetPower:
1356 double power = graphSetts.getXAxisParamValue();
1357 double factor = Math.pow(10.0, power);
1358 if (Double.isNaN(minX))
1359 minX = dsMinX * factor;
1360 if (Double.isNaN(maxX))
1361 maxX = dsMaxX * factor;
1362 break;
1363
1364 default:
1365 throw new UnexpectedSwitchCase(xAxisType);
1366 } // switch(graphSetts.getXAxisType())
1367
1368 } // if (null != xSer)
1369 } // if minX or maxX are automatic and according to some data value (i.e. not dataset number)
1370
1371 // Now minX and maxX can only be NaN in one of the following cases:
1372 // - x axis type is XAxis_DSNum (dataset number)
1373 // - x axis series index is invalid
1374 // - xSer.getMinValue or xSer.getMaxValue returned NaN
1375 // - transformation of the x-axis returned infinity or NaN
1376 // In all cases we do the same thing: default to dataset index:
1377
1378 if (Double.isNaN(minX))
1379 minX = dataCache.getMinDataFileIndex();
1380 if (Double.isNaN(maxX))
1381 maxX = dataCache.getMaxDataFileIndex();
1382
1383 // If the X-boundaries are equal - shift them apart:
1384 if (minX == maxX) {
1385 if (0.0 == minX) { minX = -0.1; maxX = 0.1; }
1386 if (0.0 < minX) { minX = 0.0; maxX *= 1.1; }
1387 if (0.0 > minX) { minX *= 1.1; maxX = 0.0; }
1388 }
1389
1390 // If the X-boundaries are the wrong way around - swap them:
1391 if (minX > maxX) {
1392 double t = maxX; maxX = minX; minX = t;
1393 }
1394
1395 // If the Y-boundaries are equal - shift them apart:
1396 if (minY == maxY) {
1397 if (0.0 == minY) { minY = -0.1; maxY = 0.1; }
1398 if (0.0 < minY) { minY = 0.0; maxY *= 1.1; }
1399 if (0.0 > minY) { minY *= 1.1; maxY = 0.0; }
1400 }
1401
1402 // If the Y-boundaries are the wrong way around - swap them:
1403 if (minY > maxY) {
1404 double t = maxY; maxY = minY; minY = t;
1405 }
1406
1407 dataViewport.setRect(minX, maxY, maxX - minX, maxY - minY);
1408 updateDatScrTransform();
1409 computeGridSteps();
1410 } // private void resetDataViewport()
1411
1412
1413 /**
1414 * Set the current view screen size.
1415 * @param width Canvas width in pixels.
1416 * @param height Canvas height in pixels
1417 */
1418 public void setScreenSize(int width, int height) {
1419
1420 if (dataComputationRunning)
1421 return;
1422
1423 screenSize.width = width - (HMARGIN << 1);
1424 screenSize.height = height - (VMARGIN << 1);
1425 updateDatScrTransform();
1426 computeScreenData();
1427 computeGridSteps();
1428 }
1429
1430 /**
1431 * Gets canvas screen size (X).
1432 *
1433 * @return Canvas screen size (X).
1434 */
1435 public int getScreenWidth() {
1436 return screenSize.width + (HMARGIN << 1);
1437 }
1438
1439 /**
1440 * Gets canvas screen size (Y).
1441 *
1442 * @return Canvas screen size (Y).
1443 */
1444 public int getScreenHeight() {
1445 return screenSize.height + (VMARGIN << 1);
1446 }
1447
1448
1449 /**
1450 * Permits to register as listener with the main LiveGraph event manager and
1451 * only with the main LiveGraph event manager.
1452 *
1453 * @param manager The {@code EventManager} for the registering attempt.
1454 * @return {@code (LiveGraph.application().eventManager() == manager)}.
1455 * @see EventListener#permissionRegisterWithEventManager(EventManager)
1456 */
1457 public boolean permissionRegisterWithEventManager(EventManager manager) {
1458 return LiveGraph.application().eventManager() == manager;
1459 }
1460
1461 /**
1462 * Does not permit any unregistering.
1463 *
1464 * @param manager The {@code EventManager} for the registering attempt.
1465 * @return {@code false}.
1466 * @see EventListener#permissionUnregisterWithEventManager(EventManager)
1467 */
1468 public boolean permissionUnregisterWithEventManager(EventManager manager) {
1469 return false;
1470 }
1471
1472 /**
1473 * Does nothing.
1474 *
1475 * @param manager The {@code EventManager} with which this {@code EventListener} is now registered.
1476 * @see EventListener#completedRegisterWithEventManager(EventManager)
1477 */
1478 public void completedRegisterWithEventManager(EventManager manager) { }
1479
1480 /**
1481 * Does nothing.
1482 *
1483 * @param manager The {@code EventManager} with which this {@code EventListener} is now unregistered.
1484 * @see EventListener#completedUnregisterWithEventManager(EventManager)
1485 */
1486 public void completedUnregisterWithEventManager(EventManager manager) { }
1487
1488 /**
1489 * Does nothing.
1490 *
1491 * @param event An event in which this {@code EventListener} may be interested.
1492 * @return {@code false}.
1493 * @see EventListener#checkEventInterest(Event)
1494 */
1495 public boolean checkEventInterest(Event<? extends EventType> event) {
1496 return false;
1497 }
1498
1499 /**
1500 * Does nothing.
1501 *
1502 * @param event The event to be validated.
1503 * @param soFar Whether {@code event} has been successfuly validated by whichever {@code EventListener}s
1504 * (if any) were invoked to validate {@code event} before this {@code EventListener}.
1505 * @return {@code true}.
1506 * @see EventListener#checkEventValid(Event, boolean)
1507 */
1508 public boolean checkEventValid(Event<? extends EventType> event, boolean soFar) {
1509 return true;
1510 }
1511
1512 /**
1513 * Processes events.
1514 *
1515 * @param event Event to process.
1516 * @throws FileNotFoundException If could not read new data file.
1517 */
1518 public void eventRaised(Event<? extends EventType> event) throws FileNotFoundException {
1519
1520 if (event.getDomain() == SettingsEvent.class) {
1521 processSettingsEvent(event.cast(SettingsEvent.class));
1522 return;
1523 }
1524
1525 if (event.getDomain() == CacheEvent.class) {
1526 processCacheEvent(event.cast(CacheEvent.class));
1527 return;
1528 }
1529 }
1530
1531 /**
1532 * Calls the neccesary recomputations when graph settings or series settings have changed.
1533 *
1534 * @param event Describes the change event.
1535 */
1536 private void processSettingsEvent(Event<SettingsEvent> event) {
1537
1538 int seriesIndex;
1539
1540 switch(event.getType()) {
1541
1542 case DSS_Load:
1543 DataSeriesSettings dss = LiveGraph.application().getDataSeriesSettings();
1544 for (int s = 0; s < screenDataBuffer.length; s++) {
1545 screenDataBuffer[s].colour = dss.getColour(s);
1546 updateSeriesTransformer(s);
1547 }
1548 updateScreenData();
1549 break;
1550
1551 case DSS_SeriesRange_Visibility:
1552 Pair range = (Pair) event.getInfoObject();
1553 int from = (Integer) range.elem1;
1554 int to = (Integer) range.elem2;
1555 for (int s = from; s <= to && s < screenDataBuffer.length; s++)
1556 updateSeriesTransformer(s);
1557 break;
1558
1559 case DSS_Series_Visibility:
1560 case DSS_Series_TransformMode:
1561 seriesIndex = (int) event.getInfoLong();
1562 updateSeriesTransformer(seriesIndex);
1563 updateScreenData();
1564 break;
1565
1566 case DSS_Series_Colour:
1567 synchronized(screenDataBufferLock) {
1568 seriesIndex = (int) event.getInfoLong();
1569 if (0 >= seriesIndex && seriesIndex < screenDataBuffer.length) {
1570 screenDataBuffer[seriesIndex].colour = (Color) event.getInfoObject();
1571 }
1572 }
1573 break;
1574
1575 case DSS_Series_TransformParam:
1576 seriesIndex = (int) event.getInfoLong();
1577 TransformMode transformMode = (TransformMode) event.getInfoObject();
1578 if (TransformMode.Transform_ScaleBySetVal == transformMode
1579 || TransformMode.Transform_Logarithm == transformMode) {
1580 updateSeriesTransformer(seriesIndex);
1581 updateScreenData();
1582 }
1583 break;
1584
1585 case GS_Load:
1586 if (!selfSettingVGridSize) {
1587 userVGridStep = graphSetts.getVGridSize();
1588 userHGridStep = graphSetts.getHGridSize();
1589 computeGridSteps();
1590 }
1591 updateScreenData();
1592 highlightPoints = graphSetts.getHighlightDataPoints();
1593 highlightAround(new Point(-1, -1));
1594 break;
1595
1596 case GS_MinY:
1597 case GS_MaxY:
1598 case GS_MinX:
1599 case GS_MaxX:
1600 updateScreenData();
1601 break;
1602
1603 case GS_VGridType:
1604 case GS_VGridSize:
1605 if (!selfSettingVGridSize) {
1606 userVGridStep = graphSetts.getVGridSize();
1607 computeGridSteps();
1608 }
1609 break;
1610
1611 case GS_HGridType:
1612 case GS_HGridSize:
1613 if (!selfSettingHGridSize) {
1614 userHGridStep = graphSetts.getHGridSize();
1615 computeGridSteps();
1616 }
1617 break;
1618
1619 case GS_XAxisType:
1620 case GS_XAxisSeriesIndex:
1621 case GS_XAxisParamValue:
1622 updateScreenData();
1623 break;
1624
1625 case GS_HighlightDataPoints:
1626 highlightPoints = graphSetts.getHighlightDataPoints();
1627 highlightAround(new Point(-1, -1));
1628 break;
1629
1630 default:
1631 break;
1632
1633 }
1634
1635 } // private void processSettingsEvent(Event<SettingsEvent> event)
1636
1637
1638 /**
1639 * If cached label info is changed, the screen buffer is recreated;
1640 * if cached data is updated the view port and the screen data are recomputed.
1641 *
1642 * @param event The cache event.
1643 */
1644 private void processCacheEvent(Event<CacheEvent> event) {
1645
1646 if (event.getProducer() != dataCache)
1647 return;
1648
1649 switch(event.getType()) {
1650
1651 case CACHE_UpdatedLabels:
1652 resetScreenDataBuffer();
1653 updateScreenData();
1654 break;
1655
1656 case CACHE_UpdatedData:
1657 for (int s = 0; s < screenDataBuffer.length; s++) {
1658 if (screenDataBuffer[s].doShow)
1659 updateSeriesTransformer(s);
1660 }
1661 updateScreenData();
1662 break;
1663 }
1664 }
1665
1666
1667 /**
1668 * A data structure to hold the locally cached plot data for a data series.
1669 */
1670 private class SeriesScreenData {
1671
1672 /*package*/ Color colour = Color.BLACK;
1673 /*package*/ int series = -1;
1674 /*package*/ Point2D.Double[] points = new Point2D.Double[DataCache.CACHE_SIZE + 1];
1675 /*package*/ MutableInt[] sortedPoints = new MutableInt[DataCache.CACHE_SIZE];
1676 /*package*/ boolean[] hlPoints = new boolean[DataCache.CACHE_SIZE];
1677 /*package*/ int[] dsIndices = new int[DataCache.CACHE_SIZE];
1678 /*package*/ int plotPoints = 0;
1679 /*package*/ boolean doShow = false;
1680 /*package*/ Transformer transformer = IDTransform;
1681
1682 /*package*/ SeriesScreenData(int series) {
1683 this.series = series;
1684 for(int i = 0; i < DataCache.CACHE_SIZE; i++) {
1685 points[i] = new Point2D.Double();
1686 sortedPoints[i] = new MutableInt();
1687 }
1688 points[DataCache.CACHE_SIZE] = new Point2D.Double();
1689 }
1690
1691 } // private class SeriesScreenData
1692
1693
1694 /**
1695 * Used in order to compare points referenced by their index in {@link SeriesScreenData#points};
1696 * the comparison is by x-xoordinates.
1697 */
1698 private class PointsByIndexComparator implements Comparator<MutableInt> {
1699 private SeriesScreenData series = null;
1700 /*package*/ void setSeries(SeriesScreenData series) { this.series = series; }
1701 public int compare(MutableInt pi1, MutableInt pi2) {
1702 Point2D.Double p1 = series.points[pi1.value];
1703 Point2D.Double p2 = series.points[pi2.value];
1704 if (p1.x < p2.x)
1705 return -1;
1706 if (p1.x > p2.x)
1707 return 1;
1708 return 0;
1709 }
1710 } // private class PointsByIndexComparator
1711
1712 /**
1713 * Used to encapsulate data series points translation routines.
1714 */
1715 private interface Transformer {
1716 public double transf(final double v);
1717 }
1718 private static Transformer IDTransform = new Transformer(){
1719 public final double transf(final double v) { return v; }
1720 };
1721
1722 } // public class Plotter