001    package org.LiveGraph.settings;
002    
003    import java.awt.Color;
004    import java.io.FileInputStream;
005    import java.io.FileOutputStream;
006    import java.io.IOException;
007    import java.util.ArrayList;
008    import java.util.List;
009    import java.util.Properties;
010    
011    import org.LiveGraph.LiveGraph;
012    import org.LiveGraph.events.Event;
013    
014    import com.softnetConsult.utils.collections.Pair;
015    import com.softnetConsult.utils.math.MathTools;
016    import com.softnetConsult.utils.string.StringTools;
017    
018    import static org.LiveGraph.settings.SettingsEvent.*;
019    
020    
021    /**
022     * Ecapsulates the settings concerned with plotting each of the data series.
023     * 
024     * <p style="font-size:smaller;">This product includes software developed by the
025     *    <strong>LiveGraph</strong> project and its contributors.<br />
026     *    (<a href="http://www.live-graph.org" target="_blank">http://www.live-graph.org</a>)<br />
027     *    Copyright (c) 2007-2008 G. Paperin.<br />
028     *    All rights reserved.
029     * </p>
030     * <p style="font-size:smaller;">File: DataSeriesSettings.java</p> 
031     * <p style="font-size:smaller;">Redistribution and use in source and binary forms, with or
032     *    without modification, are permitted provided that the following terms and conditions are met:
033     * </p>
034     * <p style="font-size:smaller;">1. Redistributions of source code must retain the above
035     *    acknowledgement of the LiveGraph project and its web-site, the above copyright notice,
036     *    this list of conditions and the following disclaimer.<br />
037     *    2. Redistributions in binary form must reproduce the above acknowledgement of the
038     *    LiveGraph project and its web-site, the above copyright notice, this list of conditions
039     *    and the following disclaimer in the documentation and/or other materials provided with
040     *    the distribution.<br />
041     *    3. All advertising materials mentioning features or use of this software or any derived
042     *    software must display the following acknowledgement:<br />
043     *    <em>This product includes software developed by the LiveGraph project and its
044     *    contributors.<br />(http://www.live-graph.org)</em><br />
045     *    4. All advertising materials distributed in form of HTML pages or any other technology
046     *    permitting active hyper-links that mention features or use of this software or any
047     *    derived software must display the acknowledgment specified in condition 3 of this
048     *    agreement, and in addition, include a visible and working hyper-link to the LiveGraph
049     *    homepage (http://www.live-graph.org).
050     * </p>
051     * <p style="font-size:smaller;">THIS SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY
052     *    OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
053     *    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT SHALL
054     *    THE AUTHORS, CONTRIBUTORS OR COPYRIGHT  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
055     *    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING  FROM, OUT OF OR
056     *    IN CONNECTION WITH THE SOFTWARE OR THE USE OR  OTHER DEALINGS IN THE SOFTWARE.
057     * </p>
058     * 
059     * @author Greg Paperin (<a href="http://www.paperin.org" target="_blank">http://www.paperin.org</a>)
060     * @version {@value org.LiveGraph.LiveGraph#version}
061     */
062    public class DataSeriesSettings extends ObservableSettings {
063    
064    /**
065     * The default file extension.
066     */
067    public static final String preferredFileExtension = ".lgdss";
068    
069    /**
070     * The transformation mode to the series values.
071     */
072    public static enum TransformMode {
073            
074            /**
075             * Display the actual data value.
076             */
077            Transform_None,
078            
079            /**
080             * Linearly transform the whole data series into the [0, 1] interval. 
081             */
082            Transform_In0to1,
083            
084            /**
085             * Multiply (scale) each data value by a specified constant. 
086             */
087            Transform_ScaleBySetVal,
088            
089            /**
090             * Take the logarithm of each data value to a specified base.
091             */
092            Transform_Logarithm
093    };
094    
095    
096    /**
097     * Holds the current settigs.
098     */
099    private List<SeriesParameters> settings = null;
100    
101    /**
102     * Holds the default colours.
103     */
104    private List<Color> defaultColours = null;
105    
106    
107    /**
108     * Creates a new data series settings object and initialises it with default values.
109     */
110    public DataSeriesSettings() {   
111            createDefaultColours();
112            settings = new ArrayList<SeriesParameters>();
113    }
114    
115    /**
116     * Creates a new data series settings object and loads the settigs from the specified file.
117     * 
118     * @param fileName The file name to use.
119     */
120    public DataSeriesSettings(String fileName) {
121            this();
122            load(fileName);
123    }
124    
125    /**
126     * Creates a set of "nice" default colours for the plot.
127     */
128    private void createDefaultColours() {
129            final int DEF_COLOURS_COUNT = 14;
130            
131            defaultColours = new ArrayList<Color>(DEF_COLOURS_COUNT);
132            
133            for (int i = 0; i < DEF_COLOURS_COUNT; i++) {
134                    
135                    float h = (2.f / (float) DEF_COLOURS_COUNT) * ((float) i);
136                    float s = (0 == (2 * i / DEF_COLOURS_COUNT) % 2 ? 1.f : .5f);
137                    float b = (0 == i % 2 ? .7f : 1.f);
138                    
139                    Color col = Color.getHSBColor(h, s, b);
140                    defaultColours.add(col);
141            }
142    }
143    
144    // Default values for the series if none other spacified:
145    
146    private boolean           getDefaultShow(int s)                   { return true; }
147    private Color             getDefaultColour(int s)                 { return defaultColours.get(s % defaultColours.size()); }
148    private TransformMode getDefaultTransformMode(int s)  { return TransformMode.Transform_None; }
149    private double            getDefaultTransformParam(int s) { return 1.0; }
150    
151    
152    /**
153     * Ensures that this settings container contains at least the settings for the data
154     * series with the specified index and all indices before that. If this settings
155     * object does not yet contain any settings for any of the series with these indices,
156     * new settings data structures will be created and initialised with default values. 
157     *  
158     * @param maxSeriesIndex It will be ensured that this container contains settings for
159     * at least all data series up to this index.
160     */
161    private void ensureLength(int maxSeriesIndex) {
162            while (settings.size() < maxSeriesIndex + 1) {
163                    int newSerInd = settings.size();
164                    SeriesParameters params = new SeriesParameters(getDefaultShow(newSerInd),
165                                                                                                               getDefaultColour(newSerInd),
166                                                                                                               getDefaultTransformMode(newSerInd),
167                                                                                                               getDefaultTransformParam(newSerInd));                
168                    settings.add(params);
169            }
170    }
171    
172    /**
173     * Loads the settings from a specified file.
174     * 
175     * @param fileName The file to load the settings from.
176     * @return {@code true} if the settings were loaded, {@code false} if an exception occured. 
177     */
178    public boolean load(String fileName) {
179            
180            Event<? extends SettingsEvent> actionEvent = checkObservers(DSS_Load, fileName);
181            if (null == actionEvent)
182                    return false;
183            
184            Properties props = new Properties();
185            try {
186                    FileInputStream in = new FileInputStream(fileName);
187                    try { props.loadFromXML(in); }
188                    finally { in.close(); }         
189            } catch(IOException e) {
190                    e.printStackTrace();
191                    return false;
192            }
193            
194            int describedSeriesCount = 0;
195            try {
196                    describedSeriesCount = Integer.parseInt(props.getProperty("DescribedSeriesCount"));
197            } catch (NumberFormatException e) {
198                    return false;
199            }
200            
201            settings.clear();
202            for (int i = 0; i < describedSeriesCount; i++) {
203                    try {
204                            
205                            boolean show = "1".equals(props.getProperty("Show."+i));
206                            
207                            String colS = props.getProperty("Colour."+i);
208                            int r = Integer.parseInt(colS.substring(0, 2), 16);
209                            int g = Integer.parseInt(colS.substring(2, 4), 16);
210                            int b = Integer.parseInt(colS.substring(4, 6), 16);
211                            Color col = new Color(r, g, b);
212                            
213                            String transModeS = props.getProperty("TransformMode."+i);
214                            TransformMode transMode = TransformMode.Transform_None;
215                            if (TransformMode.Transform_In0to1.toString().equalsIgnoreCase(transModeS))
216                                    transMode = TransformMode.Transform_In0to1;
217                            else if (TransformMode.Transform_ScaleBySetVal.toString().equalsIgnoreCase(transModeS))
218                                    transMode = TransformMode.Transform_ScaleBySetVal;
219                            else if ("Transform_SetVal".equalsIgnoreCase(transModeS)) // For compatibility with ver 1.1.2.
220                                    transMode = TransformMode.Transform_ScaleBySetVal;
221                            else if (TransformMode.Transform_Logarithm.toString().equalsIgnoreCase(transModeS))
222                                    transMode = TransformMode.Transform_Logarithm;
223                            
224                            double param = 1.0;
225                            String paramStr = props.getProperty("TransformParam."+i);
226                            // For compatibility with ver 1.1.2 and prior:
227                            if (null == paramStr && props.containsKey("ScaleFactor."+i)) 
228                                    paramStr = props.getProperty("ScaleFactor."+i);
229                            
230                            if (null != paramStr) {
231                                    try     {
232                                            param = StringTools.parseDouble(paramStr);
233                                    } catch (NumberFormatException e) {
234                                            param = Double.parseDouble(paramStr);
235                                    }
236                            }
237                            
238                            param = ensureGoodTransformParam(transMode, param);
239                            
240                            settings.add(new SeriesParameters(show, col, transMode, param));
241                            
242                    } catch (NumberFormatException e) { }
243            }
244            
245            notifyObservers(actionEvent);
246            return true;
247    }
248    
249    
250    /**
251     * Saves the settings to a specified file.
252     * 
253     * @param fileName The file to save the settings to.
254     * @return {@code true} if the settings were saved, {@code false} if an exception occured. 
255     */
256    public boolean save(String fileName) {
257            
258            Event<? extends SettingsEvent> actionEvent = checkObservers(DSS_Save, fileName);
259            if (null == actionEvent)
260                    return false;
261            
262            Properties props = new Properties();
263            props.setProperty("DescribedSeriesCount", Integer.toString(settings.size()));
264            for (int i = 0; i < settings.size(); i++) {
265                    SeriesParameters series = settings.get(i);
266                    props.setProperty("Show."+i,      series.show ? "1" : "0");
267                    props.setProperty("Colour."+i,    String.format("%02x%02x%02x", series.colour.getRed(),
268                                                                                                                                                    series.colour.getGreen(),
269                                                                                                                                                    series.colour.getBlue()));
270                    props.setProperty("TransformMode."+i, series.transMode.toString());
271                    props.setProperty("TransformParam."+i, StringTools.toString(series.param));             
272            }       
273            
274            try {
275                    FileOutputStream out = new FileOutputStream(fileName);  
276                    try { props.storeToXML(out, "LiveGraph version " + LiveGraph.version + ". DataSeriesSettings."); }
277                    finally { out.close(); }
278                    notifyObservers(actionEvent);
279                    return true;
280            } catch(IOException e) {
281                    e.printStackTrace();
282                    return false;
283            }
284    }
285    
286    /**
287     * Sets whether the data series with the specified index should be included in the plot.
288     * 
289     * @param seriesIndex A data series index (corresponds to the column index in the data file).
290     * @param show {@code true} if the data series with the specified index is to be included in the plot,
291     * {@code false} otherwise.
292     */
293    public void setShow(int seriesIndex, boolean show) {
294            
295            if (seriesIndex < 0)
296                    return;
297            
298            if (show == getShow(seriesIndex))
299                    return;
300            
301            Event<? extends SettingsEvent> actionEvent = checkObservers(DSS_Series_Visibility,
302                                                                                                                                    show, seriesIndex, Double.NaN, null);
303            if (null == actionEvent)
304                    return;
305            
306            ensureLength(seriesIndex);
307            settings.get(seriesIndex).show = show;
308            notifyObservers(actionEvent);
309    }
310    
311    /**
312     * Sets whether the data series between the specified indices should be included in the plot.
313     * 
314     * @param from Starting data series index (inclusive).
315     * @param to Finishing data series index (inclusive).
316     * @param show {@code true} if the data series with the specified index is to be included in tthe plot,
317     * {@code false} otherwise.
318     */
319    public void setShowAll(int from, int to, boolean show) {
320            
321            from = Math.max(0, from);
322            to = Math.max(0, to);
323            
324            if (from > to) {
325                    int t = from; from = to; to = t;
326            }
327            
328            Pair<Integer, Integer> bounds = new Pair<Integer, Integer>(from, to);
329            Event<? extends SettingsEvent> actionEvent = checkObservers(DSS_SeriesRange_Visibility,
330                                                                                                                                    false, 0L, Double.NaN, bounds);
331            if (null == actionEvent)
332                    return;
333                    
334            ensureLength(to);
335            for (int i = from; i <= to; i++)
336                    settings.get(i).show = show;
337            notifyObservers(actionEvent);
338    }
339    
340    /**
341     * Toggles whether the data series between the specified indices should be included in the plot.
342     * 
343     * @param from Starting data series index (inclusive).
344     * @param to Finishing data series index (inclusive).
345     */
346    public void setShowToggleAll(int from, int to) {
347            
348            from = Math.max(0, from);
349            to = Math.max(0, to);
350            
351            if (from > to) {
352                    int t = from; from = to; to = t;
353            }
354            
355            Pair<Integer, Integer> bounds = new Pair<Integer, Integer>(from, to);
356            Event<? extends SettingsEvent> actionEvent = checkObservers(DSS_SeriesRange_Visibility,
357                                                                                                                                    false, 0L, Double.NaN, bounds);
358            if (null == actionEvent)
359                    return;
360                    
361            ensureLength(to);
362            for (int i = from; i <= to; i++)
363                    settings.get(i).show = !settings.get(i).show;
364            notifyObservers(actionEvent);
365    }
366    
367    /**
368     * Sets the colour for the plot of the data series with the specified index.
369     * 
370     * @param seriesIndex A data series index (corresponds to the column index in the data file).
371     * @param colour The colour for the plot of the data series with the specified index.
372     */
373    public void setColour(int seriesIndex, Color colour) {
374            
375            if (seriesIndex < 0)
376                    return;
377            
378            if (null == colour)
379                    throw new NullPointerException("Null colour is not allowed.");
380            
381            if (colour == getColour(seriesIndex))
382                    return;
383            
384            Event<? extends SettingsEvent> actionEvent = checkObservers(DSS_Series_Colour,
385                                                                                                                                    false, seriesIndex, Double.NaN, colour);
386            if (null == actionEvent)
387                    return;
388            
389            ensureLength(seriesIndex);
390            settings.get(seriesIndex).colour = colour;
391            notifyObservers(actionEvent);
392    }
393    
394    /**
395     * Sets the transformation mode for the plotted values of the data series with the specified index.
396     *  
397     * @param seriesIndex A data series index (corresponds to the column index in the data file).
398     * @param transformMode The transformation mode for the plotted values of the data series with
399     * the specified index.
400     */
401    public void setTransformMode(int seriesIndex, TransformMode transformMode) {
402            
403            if (seriesIndex < 0)
404                    return;
405            
406            if (null == transformMode)
407                    throw new NullPointerException("Null transformation mode mode is not allowed.");
408            
409            if (transformMode == getTransformMode(seriesIndex))
410                    return;
411            
412            double p = getTransformParam(seriesIndex);
413            double np = ensureGoodTransformParam(transformMode, p);
414            
415            Event<? extends SettingsEvent> actionEvent = checkObservers(DSS_Series_TransformMode,
416                                                                                                                                    false, seriesIndex, np, transformMode);
417            if (null == actionEvent)
418                    return;
419            
420            if (p != np && null == checkObservers(DSS_Series_TransformParam, false, seriesIndex, np, transformMode))
421                    return;
422            
423            ensureLength(seriesIndex);
424            
425            settings.get(seriesIndex).transMode = transformMode;
426            notifyObservers(actionEvent);
427            
428            if (p != np)
429                    setTransformParam(seriesIndex, np);     
430    }
431    
432    /**
433     * Sets the parameter for the transformation of the plotted values of the data
434     * series with the specified index; this parameter is currently required only for the
435     * mode {@code Transform_ScaleBySetVal};
436    
437     * @param seriesIndex A data series index (corresponds to the column index in the data file).
438     * @param parameter The parameter for the transformation of the plotted values of the data series with
439     * the specified index.
440     */
441    public void setTransformParam(int seriesIndex, double parameter) {
442            
443            if (seriesIndex < 0)
444                    return;
445            
446            TransformMode transMode = getTransformMode(seriesIndex);
447            parameter = ensureGoodTransformParam(transMode, parameter);
448            
449            if (parameter == getTransformParam(seriesIndex))
450                    return;
451            
452            Event<? extends SettingsEvent> actionEvent = checkObservers(DSS_Series_TransformParam,
453                                                                                                                                    false, seriesIndex, parameter, transMode);
454            if (null == actionEvent)
455                    return;
456            
457            ensureLength(seriesIndex);
458            settings.get(seriesIndex).param = parameter;
459            
460            notifyObservers(actionEvent);
461    }
462    
463    /**
464     * Setts whether the data series with the specified index should be included in tthe plot.
465     * If no setting value has been defined for the specified series, a defalut value will be
466     * returned as specified by {@link #getDefaultShow(int)}.
467     * 
468     * @param seriesIndex A data series index (corresponds to the column index in the data file).
469     * @return {@code true} if the data series with the specified index is to be included in tthe plot,
470     * {@code false} otherwise.
471     * @see #getDefaultShow(int)
472     */
473    public boolean getShow(int seriesIndex) {
474            if (seriesIndex < 0 || settings.size() <= seriesIndex)
475                    return getDefaultShow(seriesIndex);
476            return settings.get(seriesIndex).show;
477    }
478    
479    /**
480     * Gets the colour for the plot of the data series with the specified index.
481     * If no setting value has been defined for the specified series, a defalut value will be
482     * returned as specified by {@link #getDefaultColour(int)}.
483     * 
484     * @param seriesIndex A data series index (corresponds to the column index in the data file).
485     * @return The colour for the plot of the data series with the specified index.
486     * @see #getDefaultColour(int)
487     */
488    public Color getColour(int seriesIndex) {
489            if (seriesIndex < 0 || settings.size() <= seriesIndex)
490                    return getDefaultColour(seriesIndex);
491            return settings.get(seriesIndex).colour;
492    }
493    
494    /**
495     * Gets the transformation mode for the plotted values of the data series with the specified index.
496     * If no setting value has been defined for the specified series, a defalut value will be
497     * returned as specified by {@link #getDefaultTransformMode(int)}.
498     * 
499     * @param seriesIndex A data series index (corresponds to the column index in the data file).
500     * @return The transformation mode for the plotted values of the data series with the specified index.
501     * @see #getDefaultTransformMode(int)
502     */
503    public TransformMode getTransformMode(int seriesIndex) {
504            if (seriesIndex < 0 || settings.size() <= seriesIndex)
505                    return getDefaultTransformMode(seriesIndex);
506            return settings.get(seriesIndex).transMode;
507    }
508    
509    /**
510     * Gets the parameter for the transformation of the plotted values of the data series with
511     * the specified index; this parameter is currently required only for the mode {@code Transform_ScaleBySetVal}.
512     * If no setting value has been defined for the specified series, a defalut value will be
513     * returned as specified by {@link #getDefaultTransformParam(int)}.
514     * 
515     * @param seriesIndex A data series index (corresponds to the column index in the data file).
516     * @return The parameter for the transformation of the plotted values of the data series with
517     * the specified index.
518     * @see #getDefaultTransformParam(int)
519     */
520    public double getTransformParam(int seriesIndex) {
521            if (seriesIndex < 0 || settings.size() <= seriesIndex)
522                    return getDefaultTransformParam(seriesIndex);
523            return settings.get(seriesIndex).param;
524    }
525    
526    
527    /**
528     * Ensure that the transformation parameter has a legal value for the given
529     * transformation mode. The transformation parameter must be a real number and if
530     * the transformation mode is {@code Transform_Logarithm}, it must be
531     * non-negative and not 1.
532     * 
533     * @param transformMode The transform mode for which to verify the parameter.
534     * @param parameter The transfom parameter to check.
535     * @return The corrected transform parameter.
536     */
537    private double ensureGoodTransformParam(TransformMode transformMode, double parameter) {
538            
539            if (Double.isInfinite(parameter))
540                    parameter = (parameter > 0. ? 1. : -1.);
541            
542            if (Double.isNaN(parameter))
543                    parameter = 0.;
544    
545            if (transformMode == TransformMode.Transform_Logarithm) {
546                    
547                    double d = MathTools.log(parameter, 1.);
548                    if (Double.isNaN(d) || Double.isInfinite(d)) {
549                            
550                            if (0 > parameter)
551                                    parameter = -parameter;
552                            if (1. == parameter)
553                                    parameter = 0.;
554                    }
555            }
556            
557            return parameter;
558    }
559    
560    /**
561     * This struct-class is used to group the settings for one data series in a single
562     * data structure.
563     */
564    private static class SeriesParameters {
565    
566            /**
567             * Whether this data series should be shown at all.
568             */
569            private boolean show = false;
570            
571            /**
572             * Colour to use for this series.
573             */
574            private Color colour = null;
575            
576            /**
577             * Transformation mode for series values.
578             */
579            private TransformMode transMode = null;
580            
581            /**
582             * Parameter for series' values transformation. 
583             */
584            private double param = Double.NaN;
585            
586            /**
587             * Creates an uninitialised series settings data structure.
588             */
589            private SeriesParameters() {}
590            
591            /**
592             * Creates an series settings data structure and initialises it with the specified values.
593             * 
594             * @param show Display?
595             * @param colour Line colour.
596             * @param transMode Values transformation.
597             * @param param Transformation parameter.
598             */
599            private SeriesParameters(boolean show, Color colour, TransformMode transMode, double param) {
600                    this.show = show;
601                    this.colour = colour;
602                    this.transMode = transMode;
603                    this.param = param;
604            }
605    } // private class SeriesParameters
606    
607    } // public class DataSeriesSettings