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 &quot;AS IS&quot;, 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