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.util.ArrayList; 015 import java.util.Arrays; 016 import java.util.Collections; 017 import java.util.Comparator; 018 import java.util.List; 019 020 import org.LiveGraph.LiveGraph; 021 import org.LiveGraph.dataCache.CacheObserver; 022 import org.LiveGraph.dataCache.DataCache; 023 import org.LiveGraph.dataCache.DataSeries; 024 import org.LiveGraph.dataCache.DataSet; 025 import org.LiveGraph.settings.DataSeriesSettings; 026 import org.LiveGraph.settings.GraphSettings; 027 import org.LiveGraph.settings.ObservableSettings; 028 import org.LiveGraph.settings.SettingsObserver; 029 import org.LiveGraph.settings.DataSeriesSettings.TransformMode; 030 import org.LiveGraph.settings.GraphSettings.HGridType; 031 import org.LiveGraph.settings.GraphSettings.VGridType; 032 import org.LiveGraph.settings.GraphSettings.XAxisType; 033 034 035 /** 036 * This class handles the conversion of the cached data to a screen image and the 037 * drawing of the image on a {@code Graphics} object. 038 * <br /> 039 * This class uses an {@code AffineTransform} object to convert the data held in the 040 * cache to a data plot in screen coordinates. In order to keep the {@code AffineTransform} 041 * object appropriate for the current display at all times a plotter listens to 042 * various {@link DataCache} and {@link ObservableSettings} events; in addition it offers 043 * a {@link #setScreenSize(int, int)}-method which must be called each time when the 044 * canvas-panel that uses the plotter changes its size. 045 * <br /> 046 * Whenever the {@link #dataCache} changes, a plotter uses the current {@link #datScrTransform} 047 * object to convert the data from the cache into a plot in screen coordinates according to 048 * the current global graph- and series-settings. The screen data obtained this way is locally 049 * cached in the {@link #screenDataBuffer} array. This way the data does not need to be 050 * re-computed each time the plot must be drawn on the screen. 051 * <br /> 052 * In this version the plotter handles data values transformations required by the display 053 * options (if any) on the fly. If new options should be added to theinterface, this mechanism 054 * should be replaces by a more flexible solution. 055 * 056 * <p style="font-size:smaller;">This product includes software developed by the 057 * <strong>LiveGraph</strong> project and its contributors.<br /> 058 * (<a href="http://www.live-graph.org" target="_blank">http://www.live-graph.org</a>)<br /> 059 * Copyright (c) 2007 G. Paperin.<br /> 060 * All rights reserved. 061 * </p> 062 * <p style="font-size:smaller;">File: Plotter.java</p> 063 * <p style="font-size:smaller;">Redistribution and use in source and binary forms, with or 064 * without modification, are permitted provided that the following terms and conditions are met: 065 * </p> 066 * <p style="font-size:smaller;">1. Redistributions of source code must retain the above 067 * acknowledgement of the LiveGraph project and its web-site, the above copyright notice, 068 * this list of conditions and the following disclaimer.<br /> 069 * 2. Redistributions in binary form must reproduce the above acknowledgement of the 070 * LiveGraph project and its web-site, the above copyright notice, this list of conditions 071 * and the following disclaimer in the documentation and/or other materials provided with 072 * the distribution.<br /> 073 * 3. All advertising materials mentioning features or use of this software or any derived 074 * software must display the following acknowledgement:<br /> 075 * <em>This product includes software developed by the LiveGraph project and its 076 * contributors.<br />(http://www.live-graph.org)</em><br /> 077 * 4. All advertising materials distributed in form of HTML pages or any other technology 078 * permitting active hyper-links that mention features or use of this software or any 079 * derived software must display the acknowledgment specified in condition 3 of this 080 * agreement, and in addition, include a visible and working hyper-link to the LiveGraph 081 * homepage (http://www.live-graph.org). 082 * </p> 083 * <p style="font-size:smaller;">THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY 084 * OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 085 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 086 * THE AUTHORS, CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 087 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 088 * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 089 * </p> 090 * 091 * @author Greg Paperin (<a href="http://www.paperin.org" target="_blank">http://www.paperin.org</a>) 092 * @version {@value org.LiveGraph.LiveGraph#version} 093 */ 094 public class Plotter implements CacheObserver, SettingsObserver { 095 096 /** 097 * Vertical margin. 098 */ 099 private static final int VMARGIN = 20; 100 101 /** 102 * Horisiontal margin. 103 */ 104 private static final int HMARGIN = 20; 105 106 /* 107 * Minimum plot size. 108 */ 109 private static final Dimension minScreenSize = new Dimension(150, 100); 110 111 /** 112 * Y axis colour. 113 */ 114 private static final Color VAXIS_COL = Color.BLACK; 115 116 /** 117 * X axis colour. 118 */ 119 private static final Color HAXIS_COL = Color.BLACK; 120 121 /** 122 * Label font size. 123 */ 124 private static final int FONT_SIZE = 9; 125 126 /** 127 * Gap between axes labels. 128 */ 129 private static final int AXES_LBL_GAP = 100; 130 131 /** 132 * Size of the scale marks on the axes. 133 */ 134 private static final int AXES_MARKS_SIZE = 4; 135 136 /** 137 * Radius for datapoints marks on small graphs. 138 */ 139 private static final int DATAPOINT_RAD = 3; 140 141 /** 142 * The minimum distance between grid lines (in pixels). 143 */ 144 private static final int MIN_GRIDLINE_DIST = 3; 145 146 147 /** 148 * The data cache. 149 */ 150 private DataCache dataCache = null; 151 152 /** 153 * Data series settings. 154 */ 155 private DataSeriesSettings seriesSetts = null; 156 157 /** 158 * Graph settings. 159 */ 160 private GraphSettings graphSetts = null; 161 162 163 /** 164 * Whether anythig at all is to be displayed. 165 */ 166 private boolean showAtLeastOneSeries = false; 167 168 /** 169 * Buffers the screen coordinates of the graphs. 170 */ 171 private SeriesScreenData[] screenDataBuffer = null; 172 173 /** 174 * Buffers the x coordinates. 175 */ 176 private double[] xCoordinates = null; 177 178 /** 179 * Used for sorting points by x values. 180 */ 181 private PointsByIndexComparator pointsByIndexComparator = null; 182 183 184 /** 185 * Viewable area in data coordinates. 186 */ 187 private Rectangle2D.Double dataViewport = null; 188 189 /** 190 * Screen size in pixels. 191 */ 192 private Dimension screenSize = null; 193 194 /** 195 * Data space to screen space transformation. 196 */ 197 private AffineTransform datScrTransform = null; 198 199 200 /** 201 * Whether screen data computation is in progress. 202 */ 203 private boolean dataComputationRunning = false; 204 205 /** 206 * Whether screen data computation is in progress. 207 */ 208 private boolean pointHighlightComputationRunning = false; 209 210 /** 211 * Whether the next change of h-grid settings was initiated by this plotter and should 212 * therefore be ignored by the plotter's handler. 213 */ 214 private boolean selfSettingHGridSize = false; 215 216 /** 217 * The h-grid size get by a settings change that was not initiated by this 218 * plotter itself. 219 */ 220 private double userHGridStep = Double.NaN; 221 222 /** 223 * The actual h-grid step after the consideration of plot size. 224 */ 225 private double hGridStep = Double.NaN; 226 227 /** 228 * Whether the next change of v-grid settings was initiated by this plotter and should 229 * therefore be ignored by the plotter's handler. 230 */ 231 private boolean selfSettingVGridSize = false; 232 233 /** 234 * The v-grid size get by a settings change that was not initiated by this 235 * plotter itself. 236 */ 237 private double userVGridStep = Double.NaN; 238 239 /** 240 * The actual v-grid step after the consideration of plot size. 241 */ 242 private double vGridStep = Double.NaN; 243 244 245 /** 246 * Whether dara points close to the mouse position should be highlighted. 247 */ 248 private boolean highlightPoints = true; 249 250 251 /** 252 * Creates a plotter for the data held in the specified cache. 253 * @param dataCache Cache holding the data to plot. 254 */ 255 public Plotter(DataCache dataCache) { 256 257 if (null == dataCache) 258 throw new NullPointerException("Plotter cannot act on a null cache"); 259 260 this.dataCache = dataCache; 261 this.initGlobalParameters(); 262 263 this.resetScreenDataBuffer(); 264 this.xCoordinates = new double[DataCache.CACHE_SIZE]; 265 this.pointsByIndexComparator = new PointsByIndexComparator(); 266 267 this.dataViewport = new Rectangle2D.Double(); 268 this.screenSize = new Dimension(); 269 this.datScrTransform = new AffineTransform(); 270 271 this.dataComputationRunning = false; 272 this.pointHighlightComputationRunning = false; 273 274 this.selfSettingHGridSize = false; 275 this.userHGridStep = graphSetts.getHGridSize(); 276 this.hGridStep = graphSetts.getHGridSize(); 277 278 this.selfSettingVGridSize = false; 279 this.userVGridStep = graphSetts.getVGridSize(); 280 this.vGridStep = graphSetts.getVGridSize(); 281 282 this.computeGridSteps(); 283 284 this.highlightPoints = true; 285 } 286 287 /** 288 * Used by the constructor to initialise global settings references. 289 */ 290 private void initGlobalParameters() { 291 this.seriesSetts = LiveGraph.application().getDataSeriesSettings(); 292 this.graphSetts = LiveGraph.application().getGraphSettings(); 293 } 294 295 296 /** 297 * Gets whether the screen area is large enough to paint the graph. 298 * 299 * @return {@code true} iff the screen area is large enough to paint the graph. 300 */ 301 public boolean screenTooSmall() { 302 return (minScreenSize.height > screenSize.height || minScreenSize.width > screenSize.width); 303 } 304 305 /** 306 * Gets whether at least one series is to be plotted. 307 * 308 * @return {@code true} if at seast one data series should be plotted, {@code false} otherwise. 309 */ 310 public boolean getShowAtLeastOneSeries() { 311 return this.showAtLeastOneSeries; 312 } 313 314 /** 315 * Paints the previously computed graphs along with the axes, labels, grids and so on to the 316 * specified graphics context. 317 * @param g Paint context. 318 */ 319 public void paint(Graphics g) { 320 321 // If screen is to small, just paint a message: 322 if (screenTooSmall()) { 323 g.setColor(Color.BLACK); 324 g.setFont(new Font(g.getFont().getName(), g.getFont().getStyle(), FONT_SIZE)); 325 FontMetrics fMetrics = g.getFontMetrics(); 326 g.drawString("LiveGraph " + LiveGraph.version, 2, fMetrics.getHeight() + 2); 327 g.drawString("Enlarge this window to see the plot.", 2, 2 * fMetrics.getHeight() + 4); 328 return; 329 } 330 331 332 // If there is nothing to show, just print a message: 333 if (!showAtLeastOneSeries) { 334 g.setColor(Color.BLACK); 335 g.setFont(new Font(g.getFont().getName(), g.getFont().getStyle(), FONT_SIZE)); 336 FontMetrics fMetrics = g.getFontMetrics(); 337 g.drawString("LiveGraph " + LiveGraph.version, 2, fMetrics.getHeight() + 2); 338 g.drawString("No data to display.", 2, 2 * fMetrics.getHeight() + 4); 339 return; 340 } 341 342 343 // If data computation is running, do not do anything: 344 if (dataComputationRunning) 345 return; 346 347 // Now do the actual painting: 348 paintGrids(g); 349 paintAxes(g); 350 paintData(g); 351 352 } // public void paint(Graphics g) 353 354 355 /** 356 * Paints the grid. 357 * @param g The graphics context. 358 */ 359 private void paintGrids(Graphics g) { 360 361 // Plot horizontal grid: 362 363 if (HGridType.HGrid_Simple == graphSetts.getHGridType()) { 364 365 g.setColor(graphSetts.getHGridColour()); 366 367 double dataViewportBottom = dataViewport.y - dataViewport.height; 368 double gy = dataViewportBottom + hGridStep - (dataViewportBottom % hGridStep); 369 370 Point2D.Double p1 = new Point2D.Double(); 371 Point2D.Double p2 = new Point2D.Double(); 372 373 while (gy <= dataViewport.y) { 374 375 p1.setLocation(dataViewport.x, gy); 376 p2.setLocation(dataViewport.x + dataViewport.width, gy); 377 datScrTransform.transform(p1, p1); 378 datScrTransform.transform(p2, p2); 379 g.drawLine((int) p1.x, (int) p1.y, (int) p2.x, (int) p2.y); 380 gy += hGridStep; 381 } 382 } 383 384 385 // Plot vertical grid if it is alligned at x-axis units: 386 387 if (VGridType.VGrid_XAUnitAligned == graphSetts.getVGridType()) { 388 389 g.setColor(graphSetts.getVGridColour()); 390 391 double gx = dataViewport.x + vGridStep - (dataViewport.x % vGridStep); 392 393 Point2D.Double p1 = new Point2D.Double(); 394 Point2D.Double p2 = new Point2D.Double(); 395 396 while (gx <= dataViewport.x + dataViewport.width) { 397 398 p1.setLocation(gx, dataViewport.y); 399 p2.setLocation(gx, dataViewport.y - dataViewport.height); 400 datScrTransform.transform(p1, p1); 401 datScrTransform.transform(p2, p2); 402 g.drawLine((int) p1.x, (int) p1.y, (int) p2.x, (int) p2.y); 403 gx += vGridStep; 404 } 405 } 406 407 408 // Plot vertical grid if it is alligned at dataset file indices: 409 410 // Get any (e.g. the first) data series which will be drawn: 411 SeriesScreenData firstSeriesVisible = null; 412 for (int s = 0; s < screenDataBuffer.length; s++) { 413 if (screenDataBuffer[s].doShow) { 414 firstSeriesVisible = screenDataBuffer[s]; 415 break; 416 } 417 } 418 419 if (VGridType.VGrid_DSNumAligned == graphSetts.getVGridType()) { 420 421 g.setColor(graphSetts.getVGridColour()); 422 423 int gy1 = VMARGIN; 424 int gy2 = VMARGIN + screenSize.height; 425 426 int curDSInd, gx; 427 int lastDSInd = firstSeriesVisible.dsIndices[0]; 428 for (int p = 0; p < firstSeriesVisible.plotPoints; p++) { 429 430 curDSInd = firstSeriesVisible.dsIndices[p]; 431 if (curDSInd - lastDSInd >= vGridStep) { 432 gx = (int) firstSeriesVisible.points[p].x; 433 g.drawLine(gx, gy1, gx, gy2); 434 lastDSInd = curDSInd; 435 } 436 437 } 438 } 439 } // private void paintGrids 440 441 442 /** 443 * Paints the coordinate axes. 444 * @param g The graphics context. 445 */ 446 private void paintAxes(Graphics g) { 447 448 // Setup font: 449 450 Font font = g.getFont(); 451 g.setFont(new Font(font.getName(), font.getStyle(), FONT_SIZE)); 452 FontMetrics fMetrics = g.getFontMetrics(); 453 454 // Plot horisontal axis: 455 456 int xAxisY = VMARGIN + screenSize.height; 457 Point sph = new Point(); 458 459 g.setColor(HAXIS_COL); 460 g.drawLine(HMARGIN / 2, xAxisY, HMARGIN * 3 / 2 + screenSize.width, xAxisY); 461 462 int sx = HMARGIN; 463 while(true) { 464 sph.setLocation(sx, xAxisY); 465 Point2D.Double dp = screenToDataPoint(sph); 466 String lbl = String.format("%.3f", dp.x); 467 if (sx + 2 + fMetrics.stringWidth(lbl) + 2 > HMARGIN * 2 + screenSize.width) 468 break; 469 if (HMARGIN < sx) 470 g.drawLine(sx, xAxisY - AXES_MARKS_SIZE / 2, sx, xAxisY + AXES_MARKS_SIZE / 2); 471 g.drawString(lbl, sx + 2, xAxisY + AXES_MARKS_SIZE / 2 + fMetrics.getHeight()); 472 sx += AXES_LBL_GAP; 473 } 474 475 476 // Plot vertical axis: 477 478 int yAxisX = HMARGIN; 479 Point spv = new Point(); 480 481 g.setColor(VAXIS_COL); 482 g.drawLine(yAxisX, VMARGIN * 3 / 2 + screenSize.height, yAxisX, VMARGIN / 2); 483 484 int sy = VMARGIN + screenSize.height; 485 while(true) { 486 spv.setLocation(yAxisX, sy); 487 Point2D.Double dp = screenToDataPoint(spv); 488 String lbl = String.format("%.3f", dp.y); 489 if (sy - 2 - fMetrics.getHeight() - 2 < 0) 490 break; 491 if (VMARGIN + screenSize.height > sy) 492 g.drawLine(yAxisX - AXES_MARKS_SIZE / 2, sy, yAxisX + AXES_MARKS_SIZE / 2, sy); 493 g.drawString(lbl, yAxisX + AXES_MARKS_SIZE / 2 + 2, sy - 2); 494 sy -= AXES_LBL_GAP; 495 } 496 497 } // private void paintAxes 498 499 500 /** 501 * Paints the data series. 502 * @param g The graphics context. 503 */ 504 private void paintData(Graphics g) { 505 506 // Get any (e.g. the first) data series which will be drawn: 507 SeriesScreenData firstSeriesVisible = null; 508 for (int s = 0; s < screenDataBuffer.length; s++) { 509 if (screenDataBuffer[s].doShow) { 510 firstSeriesVisible = screenDataBuffer[s]; 511 break; 512 } 513 } 514 515 // Plot data: 516 boolean drawPoints = true; 517 if (0 < firstSeriesVisible.plotPoints) 518 drawPoints = (screenSize.width / firstSeriesVisible.plotPoints > 4 * DATAPOINT_RAD); 519 520 SeriesScreenData series = null; 521 for (int s = 0; s < screenDataBuffer.length; s++) { 522 523 series = screenDataBuffer[s]; 524 if (!series.doShow) 525 continue; 526 527 g.setColor(series.colour); 528 529 Point2D.Double[] points = series.points; 530 int x1 = (int) points[0].x; 531 int y1 = (int) points[0].y; 532 int x2, y2; 533 boolean connect = true; 534 for (int p = 0; p < series.plotPoints; p++) { 535 if (Double.isNaN(points[p].x) || Double.isNaN(points[p].y) 536 || Double.isInfinite(points[p].x) || Double.isInfinite(points[p].y)) { 537 connect = false; 538 continue; 539 } 540 x2 = (int) points[p].x; 541 y2 = (int) points[p].y; 542 if (!connect) { 543 x1 = x2; 544 y1 = y2; 545 connect = true; 546 } 547 g.drawLine(x1, y1, x2, y2); 548 if (drawPoints) { 549 g.drawLine(x2 - DATAPOINT_RAD, y2 - DATAPOINT_RAD, x2 + DATAPOINT_RAD, y2 + DATAPOINT_RAD); 550 g.drawLine(x2 - DATAPOINT_RAD, y2 + DATAPOINT_RAD, x2 + DATAPOINT_RAD, y2 - DATAPOINT_RAD); 551 g.drawLine(x2, y2 + DATAPOINT_RAD, x2, y2 - DATAPOINT_RAD); 552 g.drawLine(x2 - DATAPOINT_RAD, y2, x2 + DATAPOINT_RAD, y2); 553 } 554 if (series.hlPoints[p]) { 555 g.drawOval(x2 - DATAPOINT_RAD - 1, y2 - DATAPOINT_RAD - 1, 2 + 2 * DATAPOINT_RAD, 2 + 2 * DATAPOINT_RAD); 556 } 557 x1 = x2; 558 y1 = y2; 559 } 560 } 561 } // private void paintData 562 563 564 /** 565 * Computes the screen coordinates for the visible data series. 566 */ 567 private synchronized void computeScreenData() { 568 569 if (dataComputationRunning) 570 return; 571 572 if (screenTooSmall()) 573 return; 574 575 dataComputationRunning = true; 576 577 computeXCoordinates(); 578 579 showAtLeastOneSeries = false; 580 int seriesCount = dataCache.countDataSeries(); 581 for (int s = 0; s < seriesCount; s++) { 582 583 if (!seriesSetts.getShow(s)) { 584 screenDataBuffer[s].doShow = false; 585 continue; 586 } 587 588 screenDataBuffer[s].doShow = true; 589 showAtLeastOneSeries = true; 590 computeScreenDataForSeries(s); 591 } 592 593 dataComputationRunning = false; 594 } 595 596 /** 597 * Compute the x coordinates in data space according to the current settings. 598 */ 599 private void computeXCoordinates() { 600 601 // If the option is to use a data series, but the index is invalid, we default to dataset numbers: 602 603 XAxisType xAxisType = graphSetts.getXAxisType(); 604 int xSerInd = -1; 605 if (XAxisType.XAxis_DSNum != xAxisType) { 606 xSerInd = graphSetts.getXAxisSeriesIndex(); 607 if (0 > xSerInd || dataCache.countDataSeries() <= xSerInd) 608 xAxisType = XAxisType.XAxis_DSNum; 609 } 610 611 // Now we can follow the secure option: 612 613 int dataLen = dataCache.countDataSets(); 614 615 switch(xAxisType) { 616 617 case XAxis_DSNum: 618 for (int i = 0; i < dataLen; i++) 619 xCoordinates[i] = dataCache.getDataSet(i).getDataFileIndex(); 620 break; 621 622 case XAxis_DataValSimple: 623 for (int i = 0; i < dataLen; i++) 624 xCoordinates[i] = dataCache.getDataSet(i).getValue(xSerInd); 625 break; 626 627 case XAxis_DataValScaledSet: 628 double factor = graphSetts.getXAxisScaleValue(); 629 for (int i = 0; i < dataLen; i++) 630 xCoordinates[i] = dataCache.getDataSet(i).getValue(xSerInd) * factor; 631 break; 632 633 case XAxis_DataValTrans0to1: 634 DataSeries xSer = dataCache.getDataSeries(xSerInd); 635 double transfShift = xSer.getMinValue(); 636 double transfScale = xSer.getMaxValue() - transfShift; 637 transfScale = (0 == transfScale ? 0. : 1. / transfScale); 638 for (int i = 0; i < dataLen; i++) 639 xCoordinates[i] = (dataCache.getDataSet(i).getValue(xSerInd) - transfShift) * transfScale; 640 break; 641 642 default: 643 throw new Error("Unexpected x axis type"); 644 } 645 } 646 647 /** 648 * Compute the screen coordinates for the specified series. 649 * 650 * @param seriesIndex The cache index of the series to be computed. 651 */ 652 private void computeScreenDataForSeries(int seriesIndex) { 653 654 boolean dataComputationWasRunning = dataComputationRunning; 655 dataComputationRunning = true; 656 657 // Preset data: 658 int dataPointCount = dataCache.countDataSets(); 659 660 SeriesScreenData scrData = screenDataBuffer[seriesIndex]; 661 scrData.plotPoints = 0; 662 663 // Look at each data point of the series: 664 int sp = 0; 665 double x, y; DataSet ds; 666 for (int dp = 0; dp < dataPointCount; dp++) { 667 668 // Get raw Y and X: 669 ds = dataCache.getDataSet(dp); 670 y = ds.getValue(seriesIndex); 671 y = scrData.transformer.transf(y); 672 x = xCoordinates[dp]; 673 674 // Transform the point to screen coordinates: 675 scrData.dsIndices[sp] = ds.getDataFileIndex(); 676 scrData.points[sp].y = y; 677 scrData.points[sp].x = x; 678 datScrTransform.transform(scrData.points[sp], scrData.points[sp]); 679 680 // Save point index for latter sorting: 681 scrData.sortedPoints[sp].val = sp; 682 683 sp++; 684 } 685 686 // Save the number of points actually computed: 687 scrData.plotPoints = sp; 688 689 // Sort points for fast access when highlighting with the mouse: 690 if (highlightPoints) { 691 Arrays.fill(scrData.hlPoints, 0, sp, false); 692 pointsByIndexComparator.setSeries(scrData); 693 Arrays.sort(scrData.sortedPoints, 0, sp, pointsByIndexComparator); 694 } 695 696 dataComputationRunning = dataComputationWasRunning; 697 } 698 699 700 /** 701 * Highlights the points around the specified point. 702 * This is normally called when the mouse is moved over the plotter canvas. 703 * 704 * @param sp A marker screen point. 705 * @return A list of series indices on which at least one point was highlighted. 706 */ 707 public List<Integer> highlightAround(Point sp) { 708 709 // If highlighting should not be done for a reason, we do not highlight anything: 710 if (pointHighlightComputationRunning || dataComputationRunning || !highlightPoints) { 711 List<Integer> hlSeries = Collections.emptyList(); 712 return hlSeries; 713 } 714 715 pointHighlightComputationRunning = true; 716 717 List<Integer> hlSeries = new ArrayList<Integer>(); 718 719 // Get the rectabgle within which points will be highlighted: 720 Rectangle sRect = new Rectangle(sp.x - DATAPOINT_RAD - 1, sp.y - DATAPOINT_RAD - 1, 721 1 + 2 * (DATAPOINT_RAD + 1),1 + 2 * (DATAPOINT_RAD + 1)); 722 723 // Look for points to highlight on each series: 724 SeriesScreenData series; 725 for (int s = 0; s < screenDataBuffer.length; s++) { 726 727 series = screenDataBuffer[s]; 728 729 // Skip series which are not plotted: 730 if (null == series || !series.doShow) 731 continue; 732 733 // Clear highlight flags fopr all points of the series: 734 boolean hlThisSeries = false; 735 Arrays.fill(series.hlPoints, 0, series.plotPoints, false); 736 737 // Find index at which the marker point would be inserted into the x-sorted series points array: 738 pointsByIndexComparator.setSeries(series); 739 series.points[DataCache.CACHE_SIZE].setLocation(sp.x, sp.y); 740 int mi = Arrays.binarySearch(series.sortedPoints, 0, series.plotPoints, 741 new MutableInt(DataCache.CACHE_SIZE), pointsByIndexComparator); 742 if (0 > mi) mi = -mi; 743 744 // Extend array index to the left to include all points within the selection rectangle: 745 int li = mi; 746 if (li >= series.plotPoints) { 747 li = series.plotPoints - 1; 748 } else { 749 while (0 <= li && series.points[series.sortedPoints[li].val].x >= sRect.x) 750 li--; 751 if (-1 == li) li = 0; 752 } 753 754 // Extend array index to the right to include all points within the selection rectangle: 755 int ri = mi; 756 int sRectRB = sRect.x + sRect.width; 757 while (0 <= ri && ri < series.plotPoints && series.points[series.sortedPoints[ri].val].x <= sRectRB) 758 ri++; 759 if (ri >= series.plotPoints) ri = series.plotPoints - 1; 760 761 // Now loop through the points within the determined index boundaries: 762 for (int i = li; i <= ri; i++) { 763 764 // Highlight a point iff it actually lies within the selection rectangle: 765 if (sRect.contains(series.points[series.sortedPoints[i].val])) { 766 series.hlPoints[series.sortedPoints[i].val] = true; 767 hlThisSeries = true; 768 } 769 } 770 771 // If at least one point on the series was highlighted, 772 // than we add the series index to the highlighted series list: 773 if (hlThisSeries) 774 hlSeries.add(s); 775 776 } 777 778 // Done: 779 pointHighlightComputationRunning = false; 780 return hlSeries; 781 } 782 783 /** 784 * Computes the actual grid mesh sizes taking in account the current plot size. 785 */ 786 private void computeGridSteps() { 787 788 // For horizontal grid: 789 790 if (HGridType.HGrid_Simple == graphSetts.getHGridType()) { 791 792 boolean hStepChanged = false; 793 794 if (hGridStep != userHGridStep) { 795 hGridStep = userHGridStep; 796 hStepChanged = true; 797 } 798 799 if (hGridStep < 0.0) { 800 hGridStep = -hGridStep; 801 hStepChanged = true; 802 } 803 804 double minHGridStep = dataViewport.height * MIN_GRIDLINE_DIST / screenSize.height; 805 if (hGridStep < minHGridStep 806 && !Double.isInfinite(dataViewport.height) && 0. != dataViewport.height) { 807 808 hGridStep = minHGridStep; 809 double rounded = Double.parseDouble(String.format("%.3f", hGridStep)); 810 if (rounded < hGridStep) 811 hGridStep = Double.parseDouble(String.format("%.3f", rounded + 0.001)); 812 else 813 hGridStep = rounded; 814 815 hStepChanged = true; 816 } 817 818 if (hStepChanged) { 819 selfSettingHGridSize = true; 820 graphSetts.setHGridSize(hGridStep); 821 selfSettingHGridSize = false; 822 } 823 } 824 825 // For vertical grid if it is x-axis unit-alligned: 826 827 if (VGridType.VGrid_XAUnitAligned == graphSetts.getVGridType()) { 828 829 boolean vStepChanged = false; 830 831 if (vGridStep != userVGridStep) { 832 vGridStep = userVGridStep; 833 vStepChanged = true; 834 } 835 836 if (vGridStep < 0.0) { 837 vGridStep = -vGridStep; 838 vStepChanged = true; 839 } 840 841 double minVGridStep = dataViewport.width * MIN_GRIDLINE_DIST / screenSize.width; 842 if (vGridStep < minVGridStep 843 && !Double.isInfinite(dataViewport.width) && 0. != dataViewport.width) { 844 845 vGridStep = minVGridStep; 846 double rounded = Double.parseDouble(String.format("%.3f", vGridStep)); 847 if (rounded < vGridStep) 848 vGridStep = Double.parseDouble(String.format("%.3f", rounded + 0.001)); 849 else 850 vGridStep = rounded; 851 852 vStepChanged = true; 853 } 854 855 if (vStepChanged) { 856 selfSettingVGridSize = true; 857 graphSetts.setVGridSize(vGridStep); 858 selfSettingVGridSize = false; 859 } 860 } 861 862 // For vertical grid if it is dataset-alligned: 863 864 if (VGridType.VGrid_DSNumAligned == graphSetts.getVGridType()) { 865 866 boolean vStepChanged = false; 867 868 if (vGridStep != userVGridStep) { 869 vGridStep = userVGridStep; 870 vStepChanged = true; 871 } 872 873 double rounded = Math.rint(vGridStep); 874 if (rounded != vGridStep) { 875 vGridStep = rounded; 876 vStepChanged = true; 877 } 878 879 if (vGridStep < 0.0) { 880 vGridStep = -vGridStep; 881 vStepChanged = true; 882 } 883 884 if (vGridStep == 0.0) { 885 vGridStep = 1.; 886 vStepChanged = true; 887 } 888 889 if (vStepChanged) { 890 selfSettingVGridSize = true; 891 graphSetts.setVGridSize(vGridStep); 892 selfSettingVGridSize = false; 893 } 894 } 895 } // private void computeGridSteps() 896 897 /** 898 * Map the specified point in screen coordinates into the data space. 899 * 900 * @param sp A point in screen coordinates. 901 * @return The corresponding data point. 902 */ 903 public Point2D.Double screenToDataPoint(Point sp) { 904 905 Point2D.Double dp = new Point2D.Double(); 906 try { 907 datScrTransform.inverseTransform(sp, dp); 908 } catch(NoninvertibleTransformException e) { 909 dp.setLocation(0, 0); 910 } 911 return dp; 912 } 913 914 /** 915 * Updates the data to screen transform map according to the currently visible data area and screen size. 916 */ 917 private void updateDatScrTransform() { 918 919 datScrTransform.setToIdentity(); 920 921 datScrTransform.translate(HMARGIN, (screenSize.height + VMARGIN)); 922 datScrTransform.scale(1, -1); 923 datScrTransform.scale(screenSize.width / dataViewport.width, screenSize.height / dataViewport.height); 924 datScrTransform.translate(-dataViewport.x, dataViewport.height - dataViewport.y); 925 } 926 927 928 /** 929 * Reallocates the screen data buffer. 930 */ 931 private void resetScreenDataBuffer() { 932 showAtLeastOneSeries = false; 933 screenDataBuffer = new SeriesScreenData[dataCache.countDataSeries()]; 934 for (int s = 0; s < screenDataBuffer.length; s++) 935 screenDataBuffer[s] = new SeriesScreenData(s); 936 } 937 938 939 /** 940 * First, recomputes the currently visible data area according to the current graph and series settings; 941 * then, computes the screen coordinates for the visible data series.. 942 * 943 */ 944 private void updateScreenData() { 945 resetDataViewport(); 946 computeScreenData(); 947 } 948 949 private void updateSeriesTransformer(int seriesIndex) { 950 951 if (null == screenDataBuffer) 952 return; 953 954 SeriesScreenData serData = screenDataBuffer[seriesIndex]; 955 if (null == serData) 956 return; 957 958 final int serInd = seriesIndex; 959 960 TransformMode transformMode = seriesSetts.getTransformMode(seriesIndex); 961 switch (transformMode) { 962 case Transform_None: serData.transformer = IDTransform; 963 break; 964 case Transform_SetVal: final double f = seriesSetts.getScaleFactor(serInd); 965 serData.transformer = new Transformer() { 966 public double transf(double v) { return v * f; } 967 }; 968 break; 969 case Transform_In0to1: DataSeries dSer = dataCache.getDataSeries(serInd); 970 double serMax = dSer.getMaxValue(); 971 final double shift = dSer.getMinValue(); 972 final double scale = (0. == (serMax - shift) 973 ? 0. : 1. / (serMax - shift)); 974 serData.transformer = new Transformer() { 975 public double transf(double v) { return (v - shift) * scale; } 976 }; 977 break; 978 default: throw new Error("Unexpected series scale mode"); 979 } 980 } 981 982 /** 983 * Recomputes the currently visible data area according to the current graph and series settings. 984 */ 985 private void resetDataViewport() { 986 987 // If current screen is too small we dont compute: 988 if (screenTooSmall()) 989 return; 990 991 // If computation is running, we do not compute any more: 992 if (dataComputationRunning) 993 return; 994 995 // Determine minY according to the options: 996 double minY = graphSetts.getMinY(); 997 if (Double.isNaN(minY)) { 998 999 minY = Double.MAX_VALUE; 1000 for (int s = 0; s < dataCache.countDataSeries(); s++) { 1001 1002 if (!seriesSetts.getShow(s)) 1003 continue; 1004 1005 double v = screenDataBuffer[s].transformer.transf(dataCache.getDataSeries(s).getMinValue()); 1006 if (v < minY) 1007 minY = v; 1008 } 1009 } 1010 1011 // Determine maxY according to the options: 1012 double maxY = graphSetts.getMaxY(); 1013 if (Double.isNaN(maxY)) { 1014 1015 maxY = -Double.MAX_VALUE; 1016 for (int s = 0; s < dataCache.countDataSeries(); s++) { 1017 1018 if (!seriesSetts.getShow(s)) 1019 continue; 1020 1021 double v = screenDataBuffer[s].transformer.transf(dataCache.getDataSeries(s).getMaxValue()); 1022 if (v > maxY) 1023 maxY = v; 1024 } 1025 } 1026 1027 // Determine minX and maxX accodring to the options: 1028 double minX = graphSetts.getMinX(); 1029 double maxX = graphSetts.getMaxX(); 1030 1031 // If minX or maxX are automatic and according to some data value (i.e. not dataset number): 1032 if ((Double.isNaN(minX) || Double.isNaN(maxX)) 1033 && graphSetts.getXAxisType() != XAxisType.XAxis_DSNum) { 1034 1035 // Check that x-axis data series index is valid: 1036 int xAxisSerIndex = graphSetts.getXAxisSeriesIndex(); 1037 if (0 <= xAxisSerIndex && dataCache.countDataSeries() > xAxisSerIndex) { 1038 1039 DataSeries xSer = dataCache.getDataSeries(xAxisSerIndex); 1040 1041 // X axis is an unscaled data series: 1042 if (graphSetts.getXAxisType() == XAxisType.XAxis_DataValSimple) { 1043 if (Double.isNaN(minX)) 1044 minX = xSer.getMinValue(); 1045 if (Double.isNaN(maxX)) 1046 maxX = xSer.getMaxValue(); 1047 1048 // X axis is a data series transformed into [0..1]: 1049 } else if (graphSetts.getXAxisType() == XAxisType.XAxis_DataValTrans0to1) { 1050 if (Double.isNaN(minX)) 1051 minX = 0; 1052 if (Double.isNaN(maxX)) 1053 maxX = 1; 1054 1055 // X axis is a data series scaled by a set value: 1056 } else if (graphSetts.getXAxisType() == XAxisType.XAxis_DataValScaledSet) { 1057 double scaleF = graphSetts.getXAxisScaleValue(); 1058 if (Double.isNaN(minX)) 1059 minX = xSer.getMinValue() * scaleF; 1060 if (Double.isNaN(maxX)) 1061 maxX = xSer.getMaxValue() * scaleF; 1062 } 1063 } // if x-axis data series index is valid 1064 } // if minX or maxX are automatic and according to some data value (i.e. not dataset number) 1065 1066 // Now minX and maxX can only be NaN in one of the following cases: 1067 // - x axis type is XAxis_DSNum (dataset number) 1068 // - x axis series index is invalid 1069 // - xSer.getMinValue or xSer.getMaxValue returned NaN 1070 // In all cases we do the same thing: default to dataset index: 1071 1072 if (Double.isNaN(minX)) 1073 minX = dataCache.getMinDataFileIndex(); 1074 if (Double.isNaN(maxX)) 1075 maxX = dataCache.getMaxDataFileIndex(); 1076 1077 // If the X-boundaries are equal - shift them apart: 1078 if (minX == maxX) { 1079 if (0.0 == minX) { minX = -0.1; maxX = 0.1; } 1080 if (0.0 < minX) { minX = 0.0; maxX *= 1.1; } 1081 if (0.0 > minX) { minX *= 1.1; maxX = 0.0; } 1082 } 1083 1084 // If the X-boundaries are the wrong way around - swap them: 1085 if (minX > maxX) { 1086 double t = maxX; maxX = minX; minX = t; 1087 } 1088 1089 // If the Y-boundaries are equal - shift them apart: 1090 if (minY == maxY) { 1091 if (0.0 == minY) { minY = -0.1; maxY = 0.1; } 1092 if (0.0 < minY) { minY = 0.0; maxY *= 1.1; } 1093 if (0.0 > minY) { minY *= 1.1; maxY = 0.0; } 1094 } 1095 1096 // If the Y-boundaries are the wrong way around - swap them: 1097 if (minY > maxY) { 1098 double t = maxY; maxY = minY; minY = t; 1099 } 1100 1101 dataViewport.setRect(minX, maxY, maxX - minX, maxY - minY); 1102 updateDatScrTransform(); 1103 computeGridSteps(); 1104 } // private void resetDataViewport() 1105 1106 1107 /** 1108 * Set the current view screen size. 1109 * @param width Canvas width in pixels. 1110 * @param height Canvas height in pixels 1111 */ 1112 public void setScreenSize(int width, int height) { 1113 1114 if (dataComputationRunning) 1115 return; 1116 1117 screenSize.width = width - (HMARGIN << 1); 1118 screenSize.height = height - (VMARGIN << 1); 1119 updateDatScrTransform(); 1120 computeScreenData(); 1121 computeGridSteps(); 1122 } 1123 1124 /** 1125 * Gets canvas screen size (X). 1126 * 1127 * @return Canvas screen size (X). 1128 */ 1129 public int getScreenWidth() { 1130 return screenSize.width + (HMARGIN << 1); 1131 } 1132 1133 /** 1134 * Gets canvas screen size (Y). 1135 * 1136 * @return Canvas screen size (Y). 1137 */ 1138 public int getScreenHeight() { 1139 return screenSize.height + (VMARGIN << 1); 1140 } 1141 1142 /** 1143 * If cached label info is changed, the screen buffer is recreated; 1144 * if cached data is updated the view port and the screen data are recomputed. 1145 */ 1146 public void cacheEventFired(DataCache cache, CacheEvent event) { 1147 1148 //System.out.println(event); 1149 1150 if (cache != dataCache) 1151 return; 1152 1153 if (CacheEvent.UpdateLabels == event) { 1154 resetScreenDataBuffer(); 1155 for (int s = 0; s < screenDataBuffer.length; s++) { 1156 screenDataBuffer[s].colour = seriesSetts.getColour(s); 1157 updateSeriesTransformer(s); 1158 } 1159 } 1160 1161 if (CacheEvent.UpdateData == event) { 1162 updateScreenData(); 1163 } 1164 } 1165 1166 /** 1167 * Dispatches settings change events. 1168 */ 1169 public void settingHasChanged(ObservableSettings settings, Object info) { 1170 1171 //System.out.println(settings.getClass().getName() + ": " + info); 1172 1173 if (null == info || null == settings) 1174 return; 1175 1176 if ((settings instanceof DataSeriesSettings) && (info instanceof String)) { 1177 settingHasChanged((DataSeriesSettings) settings, (String) info); 1178 return; 1179 } 1180 1181 if ((settings instanceof GraphSettings) && (info instanceof String)) { 1182 settingHasChanged((GraphSettings) settings, (String) info); 1183 return; 1184 } 1185 } 1186 1187 /** 1188 * Calls the neccesary recoputations when seties settings are changed. 1189 * @param settings Series settings. 1190 * @param info Change event info. 1191 */ 1192 public void settingHasChanged(DataSeriesSettings settings, String info) { 1193 1194 if (null == info || null == settings) 1195 return; 1196 1197 if (info.equals("load")) { 1198 for (int s = 0; s < screenDataBuffer.length; s++) { 1199 screenDataBuffer[s].colour = settings.getColour(s); 1200 updateSeriesTransformer(s); 1201 } 1202 updateScreenData(); 1203 return; 1204 } 1205 1206 if (info.startsWith("Show")) { 1207 updateScreenData(); 1208 } 1209 1210 if (info.startsWith("TransformMode.")) { 1211 int affectedSeries = Integer.parseInt(info.substring(info.lastIndexOf(".") + 1)); 1212 updateSeriesTransformer(affectedSeries); 1213 updateScreenData(); 1214 } 1215 1216 if (info.startsWith("Colour.")) { 1217 int affectedSeries = Integer.parseInt(info.substring(info.lastIndexOf(".") + 1)); 1218 screenDataBuffer[affectedSeries].colour = settings.getColour(affectedSeries); 1219 } 1220 1221 if (info.startsWith("ScaleFactor.")) { 1222 int affectedSeries = Integer.parseInt(info.substring(info.lastIndexOf(".") + 1)); 1223 if (TransformMode.Transform_SetVal == seriesSetts.getTransformMode(affectedSeries)) { 1224 updateSeriesTransformer(affectedSeries); 1225 updateScreenData(); 1226 } 1227 } 1228 } 1229 1230 /** 1231 * Calls the neccesary recoputations when graph settings are changed. 1232 * @param settings Graph settings. 1233 * @param info Change event info. 1234 */ 1235 public void settingHasChanged(GraphSettings settings, String info) { 1236 1237 if (null == info || null == settings || settings != this.graphSetts) 1238 return; 1239 1240 if (info.equals("MinY") || info.equals("MinX") 1241 || info.equals("MaxY") || info.equals("MaxX") 1242 || info.equals("load")) { 1243 updateScreenData(); 1244 } 1245 1246 if (info.equals("VGridType") || info.equals("VGridSize") || info.equals("load")) { 1247 if (!selfSettingVGridSize) { 1248 userVGridStep = graphSetts.getVGridSize(); 1249 computeGridSteps(); 1250 } 1251 } 1252 1253 if (info.equals("HGridType") || info.equals("HGridSize") || info.equals("load")) { 1254 if (!selfSettingHGridSize) { 1255 userHGridStep = graphSetts.getHGridSize(); 1256 computeGridSteps(); 1257 } 1258 } 1259 1260 if (info.equals("XAxisType") || info.equals("XAxisSeriesIndex") || info.equals("XAxisScaleValue") 1261 || info.equals("load")) { 1262 updateScreenData(); 1263 } 1264 1265 if (info.equals("HighlightDataPoints") || info.equals("load")) { 1266 highlightPoints = settings.getHighlightDataPoints(); 1267 highlightAround(new Point(-1, -1)); 1268 } 1269 } 1270 1271 /** 1272 * For holding mutable ints as objects. 1273 */ 1274 private class MutableInt { 1275 /*package*/ int val = -1; 1276 /*package*/ MutableInt() { this.val = -1; } 1277 /*package*/ MutableInt(int v) { this.val = v; } 1278 } 1279 1280 /** 1281 * A data structure to hold the locally cached plot data for a data series. 1282 */ 1283 private class SeriesScreenData { 1284 1285 /*package*/ Color colour = Color.BLACK; 1286 /*package*/ int series = -1; 1287 /*package*/ Point2D.Double[] points = new Point2D.Double[DataCache.CACHE_SIZE + 1]; 1288 /*package*/ MutableInt[] sortedPoints = new MutableInt[DataCache.CACHE_SIZE]; 1289 /*package*/ boolean[] hlPoints = new boolean[DataCache.CACHE_SIZE]; 1290 /*package*/ int[] dsIndices = new int[DataCache.CACHE_SIZE]; 1291 /*package*/ int plotPoints = 0; 1292 /*package*/ boolean doShow = false; 1293 /*package*/ Transformer transformer = IDTransform; 1294 1295 /*package*/ SeriesScreenData(int series) { 1296 this.series = series; 1297 for(int i = 0; i < DataCache.CACHE_SIZE; i++) { 1298 points[i] = new Point2D.Double(); 1299 sortedPoints[i] = new MutableInt(); 1300 } 1301 points[DataCache.CACHE_SIZE] = new Point2D.Double(); 1302 } 1303 1304 } // private class SeriesScreenData 1305 1306 1307 /** 1308 * Used in order to compare points referenced by their index in {@link SeriesScreenData#points}; 1309 * the comparison is by x-xoordinates. 1310 */ 1311 private class PointsByIndexComparator implements Comparator<MutableInt> { 1312 private SeriesScreenData series = null; 1313 /*package*/ void setSeries(SeriesScreenData series) { this.series = series; } 1314 public int compare(MutableInt pi1, MutableInt pi2) { 1315 Point2D.Double p1 = series.points[pi1.val]; 1316 Point2D.Double p2 = series.points[pi2.val]; 1317 if (p1.x < p2.x) 1318 return -1; 1319 if (p1.x > p2.x) 1320 return 1; 1321 return 0; 1322 } 1323 } // private class PointsByIndexComparator 1324 1325 /** 1326 * Used to encapsulate data series points translation routines. 1327 */ 1328 private interface Transformer { 1329 public double transf(double v); 1330 } 1331 private static Transformer IDTransform = new Transformer(){public double transf(double v){return v;}}; 1332 1333 } // public class Plotter