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