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