Example #1
0
 /**
  * Returns an {@link IntervalsList} built from a set of {@link Timestamp} using the following
  * algorithm: if the {@link TimeDuration} between two {@link Timestamp} is inferior to the defined
  * tolerance, they are grouped in the same {@link TimeInterval}. A {@link TimeInterval} duration
  * can not be inferior to the defined tolerance. TODO: better algorithm ?
  */
 public IntervalsList toIntervalsList() {
   IntervalsList ilist = new IntervalsList();
   if (isEmpty()) // empty list
   return ilist;
   Iterator<Timestamp> iterator = timestamps.iterator();
   Timestamp first = iterator.next();
   Timestamp previous = first;
   while (iterator.hasNext()) {
     Timestamp next = iterator.next();
     if (previous.plus(tolerance).compareTo(next) < 0) {
       if (first.equals(previous)) {
         ilist.addToSelf(intervalOfSinglePoint(first));
       } else {
         ilist.addToSelf(TimeInterval.between(first, previous));
       }
       first = next;
     }
     previous = next;
   }
   // Process end of set
   if (first.equals(previous)) {
     ilist.addToSelf(intervalOfSinglePoint(first));
   } else {
     ilist.addToSelf(TimeInterval.between(first, previous));
   }
   return ilist;
 }
  /**
   * Tests {@code ApplianceArchiveReader#getRawValues(int, String, Timestamp, Timestamp)} method for
   * a float waveform type pv.
   *
   * @throws Exception
   */
  @Test
  public void testDataRetrievalFloat() throws Exception {
    Timestamp end = Timestamp.now();
    Timestamp start = end.minus(TimeDuration.ofHours(24.0));
    ArchiveVNumberArray[] vals = getValuesNumberArray("test_pv_wave_float", false, 0, start, end);
    assertEquals(
        "Number of values comparison", TestGenMsgIteratorRaw.MESSAGE_LIST_LENGTH, vals.length);

    ArchiveVNumberArray val = null;
    for (int i = 0; i < vals.length; i++) {
      val = vals[i];
      double[] array = new double[val.getData().size()];
      for (int j = 0; j < array.length; j++) {
        array[j] = val.getData().getFloat(j);
      }
      assertArrayEquals(
          "Value comparison", TestGenMsgIteratorWaveform.VALUE_FLOAT[i], array, 0.000001);
      assertEquals(
          "Timestamp comparison",
          TimestampHelper.toMillisecs(start) + i,
          TimestampHelper.toMillisecs(val.getTimestamp()));
      assertEquals(
          "Severity", getSeverity(TestGenMsgIteratorRaw.SEVERITIES[i]), val.getAlarmSeverity());
      assertEquals("Status", String.valueOf(TestGenMsgIteratorRaw.STATUS[i]), val.getAlarmName());
    }
  }
Example #3
0
  public void update(final VType value) {
    display_info = Optional.ofNullable(ValueUtil.displayOf(value));
    if (display_info.isPresent()) value_dbl = ValueUtil.numericValueOf(value).doubleValue();

    if (value instanceof VNumber)
      value_str = Double.toString(((VNumber) value).getValue().doubleValue());
    else if (value instanceof VEnum) {
      final VEnum ev = (VEnum) value;
      if (ev.getIndex() >= 0 && ev.getIndex() < ev.getLabels().size())
        value_str = ev.getLabels().get(ev.getIndex());
      else value_str = Integer.toString(ev.getIndex());
    } else if (value instanceof VString) value_str = ((VString) value).getValue();
    else value_str = Objects.toString(value);

    final Time vtime = ValueUtil.timeOf(value);
    if (vtime == null) return;

    final Timestamp stamp = vtime.getTimestamp();
    final Instant new_time = Instant.ofEpochSecond(stamp.getSec(), stamp.getNanoSec());
    if (time != null) {
      final Duration duration = Duration.between(time, new_time);
      final double period = duration.getSeconds() + duration.getNano() * 1e-9;
      value_period.add(period);
    } else value_period.reset();
    time = new_time;
  }
 /**
  * Tests {@code ApplianceArchiveReader#getRawValues(int, String, Timestamp, Timestamp)} method for
  * an enum waveform type pv.
  *
  * @throws Exception
  */
 @Test
 public void testDataRetrievalEnum() throws Exception {
   // this doesn't seem to be supported on the IOC side
   Timestamp end = Timestamp.now();
   Timestamp start = end.minus(TimeDuration.ofHours(24.0));
   try {
     getValuesEnumArray("test_pv_wave_enum", false, 0, start, end);
     fail();
   } catch (UnsupportedOperationException e) {
     assertNotNull(e);
   }
 }
 /**
  * Tests {@code ApplianceArchiveReader#getRawValues(int, String, Timestamp, Timestamp)} method for
  * a string wavefdorm type pv.
  *
  * @throws Exception
  */
 @Test
 public void testDataRetrievalString() throws Exception {
   // this is not supported on the CSS core side
   Timestamp end = Timestamp.now();
   Timestamp start = end.minus(TimeDuration.ofHours(24.0));
   try {
     getValuesStringArray("test_pv_wave_string", false, 0, start, end);
     fail();
   } catch (UnsupportedOperationException e) {
     assertNotNull(e);
   }
 }
 public static void main(String[] args) throws Exception {
   List<String> signals = new ArrayList<>();
   Timestamp start = null;
   Timestamp end = null;
   String filename = null;
   for (int i = 0; i < args.length; i++) {
     String arg = args[i];
     if (arg.equals("-s")) {
       i++;
       start = format.parse(args[i]);
     } else if (arg.equals("-e")) {
       i++;
       end = format.parse(args[i]);
     } else if (arg.equals("-o")) {
       i++;
       filename = args[i];
     } else {
       signals.add(arg);
     }
   }
   List<TimeSeries> data = RrdToolDB.fetchData(signals, start, end);
   Map<String, TimeSeries> series = new HashMap<>();
   for (int i = 0; i < signals.size(); i++) {
     series.put(signals.get(i), data.get(i));
   }
   TimeSeriesMulti correlated = TimeSeriesMulti.synchronizeSeries(series);
   final Point3DWithLabelDataset dataset =
       Point3DWithLabelDatasets.build(
           correlated.getValues().get(signals.get(0)),
           correlated.getValues().get(signals.get(1)),
           correlated.getValues().get(signals.get(2)),
           Collections.nCopies(correlated.getValues().get(signals.get(0)).size(), "label"));
   BubbleUtil.createBubblePlot(filename, dataset, "DATASETLABEL", "", "", "", "", Timestamp.now());
 }
 @Test
 public void parse1() {
   BufferedReader reader =
       new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("rrdtool1.out")));
   RrdToolOutputParser instance = new RrdToolOutputParser();
   TimeSeriesMulti result = instance.parse(reader);
   assertThat(result.getTime().size(), equalTo(241));
   assertThat(result.getValues().size(), equalTo(1));
   ListDouble values = result.getValues().get("load_1min");
   assertThat(values.size(), equalTo(241));
   assertThat(result.getTime().get(0), equalTo(Timestamp.of(1349877960, 0)));
   assertThat(values.getDouble(0), equalTo(Double.NaN));
   assertThat(result.getTime().get(150), equalTo(Timestamp.of(1349931960, 0)));
   assertThat(values.getDouble(150), equalTo(1.1737083333e+00));
   assertThat(result.getTime().get(240), equalTo(Timestamp.of(1349964360, 0)));
   assertThat(values.getDouble(240), equalTo(Double.NaN));
 }
Example #8
0
 /**
  * @param start_time Start and ..
  * @param end_time end time of the range to display
  */
 public void setTimerange(final Timestamp start_time, final Timestamp end_time) {
   final double new_span = end_time.durationFrom(start_time).toSeconds();
   if (new_span > 0) {
     synchronized (this) {
       this.end_time = end_time;
       time_span = new_span;
     }
   }
   // Notify listeners
   for (ModelListener listener : listeners) listener.changedTimerange();
 }
 @Test
 public void parse2() {
   BufferedReader reader =
       new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("rrdtool2.out")));
   RrdToolOutputParser instance = new RrdToolOutputParser();
   TimeSeriesMulti result = instance.parse(reader);
   assertThat(result.getTime().size(), equalTo(241));
   assertThat(result.getValues().size(), equalTo(18));
   assertThat(
       result.getValues().keySet(),
       equalTo(
           (Set<String>)
               new HashSet<String>(
                   Arrays.asList(
                       "Setpoint",
                       "Fan1cfm",
                       "Temp1",
                       "Temp3",
                       "Fan2Rpm",
                       "TempOvrSet",
                       "Fan2cfm",
                       "Totalcfm",
                       "Fan1Status",
                       "Temp2",
                       "Fan1Rpm",
                       "Fan2Status",
                       "Fan3cfm",
                       "Fan3Rpm",
                       "Fan3Status",
                       "Fan4cfm",
                       "Fan4Rpm",
                       "Fan4Status"))));
   ListDouble values = result.getValues().get("Temp1");
   assertThat(values.size(), equalTo(241));
   assertThat(result.getTime().get(0), equalTo(Timestamp.of(1355416920, 0)));
   assertThat(values.getDouble(0), equalTo(9.2500000000e+01));
   assertThat(result.getTime().get(150), equalTo(Timestamp.of(1355470920, 0)));
   assertThat(values.getDouble(150), equalTo(9.1000000000e+01));
   assertThat(result.getTime().get(240), equalTo(Timestamp.of(1355503320, 0)));
   assertThat(values.getDouble(240), equalTo(Double.NaN));
 }
  /**
   * Given the time and first element of the sample, see if there are more array elements.
   *
   * @param stamp Time stamp of the sample
   * @param dbl0 Value of the first (maybe only) array element
   * @param severity Severity of the sample
   * @return Array with given element and maybe more.
   * @throws Exception on error, including 'cancel'
   */
  private double[] readArrayElements(
      final Timestamp stamp, final double dbl0, final AlarmSeverity severity) throws Exception {
    // For performance reasons, only look for array data until we hit a scalar sample.
    if (is_an_array == false) return new double[] {dbl0};

    // See if there are more array elements
    if (sel_array_samples == null) { // Lazy initialization
      sel_array_samples =
          reader.getRDB().getConnection().prepareStatement(reader.getSQL().sample_sel_array_vals);
    }
    sel_array_samples.setInt(1, channel_id);
    sel_array_samples.setTimestamp(2, TimestampHelper.toSQLTimestamp(stamp));
    // MySQL keeps nanoseconds in designated column, not TIMESTAMP
    if (!reader.isOracle()) sel_array_samples.setInt(3, stamp.getNanoSec());

    // Assemble array of unknown size in ArrayList ....
    final ArrayList<Double> vals = new ArrayList<Double>();
    reader.addForCancellation(sel_array_samples);
    try {
      final ResultSet res = sel_array_samples.executeQuery();
      vals.add(new Double(dbl0));
      while (res.next()) vals.add(res.getDouble(1));
      res.close();
    } finally {
      reader.removeFromCancellation(sel_array_samples);
    }
    // Convert to plain double array
    final int N = vals.size();
    final double ret[] = new double[N];
    for (int i = 0; i < N; i++) ret[i] = vals.get(i).doubleValue();
    // Check if it's in fact just a scalar, and a valid one
    if (N == 1 && severity != AlarmSeverity.UNDEFINED) { // Found a perfect non-array sample:
      // Assume that the data is scalar, skip the array check from now on
      is_an_array = false;
    }
    return ret;
  }
Example #11
0
 /**
  * Creates a sine shaped signal between min and max, updating every interval seconds with
  * samplesPerCycles samples every full sine cycle.
  *
  * @param min minimum value
  * @param max maximum value
  * @param samplesPerCycle number of samples for each full cycle (each 2 Pi)
  * @param secondsBeetwenSamples interval between samples in seconds
  */
 public Sine(Double min, Double max, Double samplesPerCycle, Double secondsBeetwenSamples) {
   super(secondsBeetwenSamples, VDouble.class);
   this.min = min;
   this.max = max;
   this.currentValue = 0;
   this.samplesPerCycle = samplesPerCycle;
   range = this.max - this.min;
   lastValue =
       newVDouble(
           0.0,
           alarmNone(),
           newTime(Timestamp.now()),
           newDisplay(
               min,
               min + range * 0.1,
               min + range * 0.2,
               "x",
               Constants.DOUBLE_FORMAT,
               min + range * 0.8,
               min + range * 0.9,
               max,
               min,
               max));
 }
Example #12
0
 // Generate a TimeInterval from a single timestamp
 private TimeInterval intervalOfSinglePoint(Timestamp first) {
   return TimeInterval.between(first, first.plus(tolerance));
 }
Example #13
0
 /**
  * @return End time of the data range
  * @see #isScrollEnabled()
  */
 public synchronized Timestamp getEndTime() {
   if (scroll_enabled) end_time = Timestamp.now();
   return end_time;
 }
Example #14
0
/**
 * Data Browser model
 *
 * <p>Maintains a list of {@link ModelItem}s
 *
 * @author Kay Kasemir
 * @author Takashi Nakamoto changed the model to accept multiple items with the same name so that
 *     Data Browser can show the trend of the same PV in different axes or with different waveform
 *     indexes.
 */
@SuppressWarnings("nls")
public class Model {
  /**
   * File extension for data browser config files. plugin.xml registers the editor for this file
   * extension
   */
  public static final String FILE_EXTENSION = "plt"; // $NON-NLS-1$

  /** Previously used file extension */
  public static final String FILE_EXTENSION_OLD = "css-plt"; // $NON-NLS-1$

  // XML file tags
  public static final String TAG_DATABROWSER = "databrowser";
  public static final String TAG_SCROLL = "scroll";
  public static final String TAG_UPDATE_PERIOD = "update_period";
  public static final String TAG_LIVE_SAMPLE_BUFFER_SIZE = "ring_size";
  public static final String TAG_PVLIST = "pvlist";
  public static final String TAG_PV = "pv";
  public static final String TAG_NAME = "name";
  public static final String TAG_DISPLAYNAME = "display_name";
  public static final String TAG_FORMULA = "formula";
  public static final String TAG_AXES = "axes";
  public static final String TAG_AXIS = "axis";
  public static final String TAG_LINEWIDTH = "linewidth";
  public static final String TAG_COLOR = "color";
  public static final String TAG_RED = "red";
  public static final String TAG_GREEN = "green";
  public static final String TAG_BLUE = "blue";
  public static final String TAG_TRACE_TYPE = "trace_type";
  public static final String TAG_SCAN_PERIOD = "period";
  public static final String TAG_INPUT = "input";
  public static final String TAG_ARCHIVE = "archive";
  public static final String TAG_URL = "url";
  public static final String TAG_KEY = "key";
  public static final String TAG_START = "start";
  public static final String TAG_END = "end";
  public static final String TAG_LOG_SCALE = "log_scale";
  public static final String TAG_AUTO_SCALE = "autoscale";
  public static final String TAG_MAX = "max";
  public static final String TAG_MIN = "min";
  public static final String TAG_BACKGROUND = "background";
  public static final String TAG_ARCHIVE_RESCALE = "archive_rescale";
  public static final String TAG_REQUEST = "request";
  public static final String TAG_VISIBLE = "visible";

  public static final String TAG_ANNOTATIONS = "annotations";
  public static final String TAG_ANNOTATION = "annotation";
  public static final String TAG_ANNOTATION_CURSOR_LINE_STYLE = "line_style";
  public static final String TAG_ANNOTATION_SHOW_NAME = "show_name";
  public static final String TAG_ANNOTATION_SHOW_POSITION = "show_position";
  public static final String TAG_ANNOTATION_COLOR = "color";
  public static final String TAG_ANNOTATION_FONT = "font";

  public static final String TAG_TIME = "time";
  public static final String TAG_VALUE = "value";
  public static final String TAG_WAVEFORM_INDEX = "waveform_index";

  /**
   * AJOUT XYGraphMemento
   *
   * @author L.PHILIPPE GANIL
   */
  public static final String TAG_TITLE = "title";

  public static final String TAG_TITLE_TEXT = "text";
  public static final String TAG_TITLE_COLOR = "color";
  public static final String TAG_TITLE_FONT = "font";

  public static final String TAG_FONT = "font";
  public static final String TAG_SCALE_FONT = "scale_font";

  public static final String TAG_TIME_AXIS = "time_axis";

  // GRID LINE
  public static final String TAG_GRID_LINE = "grid_line";
  public static final String TAG_SHOW_GRID_LINE = "show_grid_line";
  public static final String TAG_DASH_GRID_LINE = "dash_grid_line";

  // FORMAT
  public static final String TAG_FORMAT = "format";
  public static final String TAG_AUTO_FORMAT = "auto_format";
  public static final String TAG_TIME_FORMAT = "time_format";
  public static final String TAG_FORMAT_PATTERN = "format_pattern";

  public static final String TAG_GRAPH_SETTINGS = "graph_settings";
  public static final String TAG_SHOW_TITLE = "show_title";
  public static final String TAG_SHOW_LEGEND = "show_legend";
  public static final String TAG_SHOW_PLOT_AREA_BORDER = "show_plot_area_border";
  public static final String TAG_TRANSPARENT = "transparent";

  /**
   * Default colors for newly added item, used over when reaching the end.
   *
   * <p>Very hard to find a long list of distinct colors. This list is definitely too short...
   */
  private static final RGB[] default_colors = {
    new RGB(21, 21, 196), // blue
    new RGB(242, 26, 26), // red
    new RGB(33, 179, 33), // green
    new RGB(0, 0, 0), // black
    new RGB(128, 0, 255), // violett
    new RGB(255, 170, 0), // (darkish) yellow
    new RGB(255, 0, 240), // pink
    new RGB(243, 132, 132), // peachy
    new RGB(0, 255, 11), // neon green
    new RGB(0, 214, 255), // neon blue
    new RGB(114, 40, 3), // brown
    new RGB(219, 128, 4), // orange
  };

  /** Macros */
  private IMacroTableProvider macros = null;

  /** Listeners to model changes */
  private final ArrayList<ModelListener> listeners = new ArrayList<ModelListener>();

  /** Axes configurations */
  private final ArrayList<AxisConfig> axes = new ArrayList<AxisConfig>();

  /**
   * Time Axes configurations Ignore MIN-MAX part because the range is set by start & end properties
   */
  private AxisConfig timeAxis;

  public AxisConfig getTimeAxis() {
    return timeAxis;
  }

  /** All the items in this model */
  private final ArrayList<ModelItem> items = new ArrayList<ModelItem>();

  /**
   * 'run' flag
   *
   * @see #start()
   * @see #stop()
   */
  private boolean is_running = false;

  /** Period in seconds for scrolling or refreshing */
  private double update_period = Preferences.getUpdatePeriod();

  /** Timer used to scan PVItems */
  private final Timer scanner = new Timer("ScanTimer", true);

  /** <code>true</code> if scrolling is enabled */
  private boolean scroll_enabled = true;

  /** Time span of data in seconds */
  private double time_span = Preferences.getTimeSpan();

  /** End time of the data range */
  private Timestamp end_time = Timestamp.now();

  /** Background color */
  private RGB background = new RGB(255, 255, 255);

  /** Annotations */
  private AnnotationInfo[] annotations = new AnnotationInfo[0];

  /** How should plot rescale when archived data arrives? */
  private ArchiveRescale archive_rescale = Preferences.getArchiveRescale();

  /**
   * Manage XYGraph Configuration Settings
   *
   * @author L.PHILIPPE GANIL
   */
  private XYGraphSettings graphSettings = new XYGraphSettings();

  public XYGraphSettings getGraphSettings() {
    return graphSettings;
  }

  public void setGraphSettings(XYGraphSettings xYGraphMem) {
    graphSettings = xYGraphMem;
    // fireXYGraphMemChanged(settings);
  }

  public void fireGraphConfigChanged() {

    for (ModelListener listener : listeners) listener.changedXYGraphConfig();
  }

  /** @param macros Macros to use in this model */
  public void setMacros(final IMacroTableProvider macros) {
    this.macros = macros;
  }

  /**
   * Resolve macros
   *
   * @param text Text that might contain "$(macro)"
   * @return Text with all macros replaced by their value
   */
  public String resolveMacros(final String text) {
    if (macros == null) return text;
    try {
      return MacroUtil.replaceMacros(text, macros);
    } catch (InfiniteLoopException ex) {
      Activator.getLogger()
          .log(Level.WARNING, "Problem in macro {0}: {1}", new Object[] {text, ex.getMessage()});
      return "Macro Error";
    }
  }

  /** @param listener New listener to notify */
  public void addListener(final ModelListener listener) {
    listeners.add(listener);
  }

  /** @param listener Listener to remove */
  public void removeListener(final ModelListener listener) {
    listeners.remove(listener);
  }

  /** @return Number of axes in model */
  public int getAxisCount() {
    return axes.size();
  }

  /**
   * @param axis_index Index of axis, 0 ... <code>getAxisCount()-1</code>
   * @return {@link AxisConfig}
   */
  public AxisConfig getAxis(final int axis_index) {
    return axes.get(axis_index);
  }

  /**
   * Return the AxisConfig with the specifc name or null
   *
   * @param axis_index Index of axis, 0 ... <code>getAxisCount()-1</code>
   * @return {@link AxisConfig}
   */
  public AxisConfig getAxis(final String name) {
    for (AxisConfig axis : axes) {
      // System.err.println(axis.getName() + " == " + name + "=" + (axis.getName().equals(name)));
      if (axis.getName().equals(name)) return axis;
    }

    return null;
  }

  /**
   * Locate index of value axis
   *
   * @param axis Value axis configuration
   * @return Index of axis (0, ...) or -1 if not in Model
   */
  public int getAxisIndex(final AxisConfig axis) {
    return axes.indexOf(axis);
  }

  /**
   * @param axis Axis to test
   * @return First ModelItem that uses the axis, <code>null</code> if axis is empty
   */
  public ModelItem getFirstItemOnAxis(final AxisConfig axis) {
    for (ModelItem item : items) if (item.getAxis() == axis) return item;
    return null;
  }

  /**
   * @param axis Axis to test
   * @return ModelItem linked to this axis count
   */
  public int countActiveItemsOnAxis(final AxisConfig axis) {
    int count = 0;
    for (ModelItem item : items) if (item.getAxis() == axis && item.isVisible()) count++;
    return count;
  }

  /** @return First unused axis (no items on axis), <code>null</code> if none found */
  public AxisConfig getEmptyAxis() {
    for (AxisConfig axis : axes) if (getFirstItemOnAxis(axis) == null) return axis;
    return null;
  }

  /**
   * Add value axis with default settings
   *
   * @return Newly added axis configuration
   */
  public AxisConfig addAxis(String name) {
    if (name == null) name = NLS.bind(Messages.Plot_ValueAxisNameFMT, getAxisCount() + 1);
    final AxisConfig axis = new AxisConfig(name);
    axis.setColor(getNextItemColor());
    addAxis(axis);
    return axis;
  }

  /** @param axis New axis to add */
  public void addAxis(final AxisConfig axis) {
    axes.add(axis);
    axis.setModel(this);
    fireAxisChangedEvent(null);
  }

  /**
   * Add axis at given index. Adding at '1' means the new axis will be at index '1', and what used
   * to be at '1' will be at '2' and so on.
   *
   * @param index Index where axis will be placed.
   * @param axis New axis to add
   */
  public void addAxis(final int index, final AxisConfig axis) {
    axes.add(index, axis);
    axis.setModel(this);
    fireAxisChangedEvent(null);
  }

  /**
   * @param axis Axis to remove
   * @throws Error when axis not in model, or axis in use by model item
   */
  public void removeAxis(final AxisConfig axis) {
    if (!axes.contains(axis)) throw new Error("Unknown AxisConfig");
    for (ModelItem item : items)
      if (item.getAxis() == axis) throw new Error("Cannot removed AxisConfig while in use");
    axis.setModel(null);
    axes.remove(axis);
    fireAxisChangedEvent(null);
  }

  /** @return How should plot rescale after archived data arrived? */
  public ArchiveRescale getArchiveRescale() {
    return archive_rescale;
  }

  /** @param archive_rescale How should plot rescale after archived data arrived? */
  public void setArchiveRescale(final ArchiveRescale archive_rescale) {
    if (this.archive_rescale == archive_rescale) return;
    this.archive_rescale = archive_rescale;
    for (ModelListener listener : listeners) listener.changedArchiveRescale();
  }

  /** @return {@link ModelItem} count in model */
  public int getItemCount() {
    return items.size();
  }

  /**
   * Get one {@link ModelItem}
   *
   * @param i 0... getItemCount()-1
   * @return {@link ModelItem}
   */
  public ModelItem getItem(final int i) {
    return items.get(i);
  }

  /**
   * Locate item by name. If different items with the same exist in this model, the first occurrence
   * will be returned. If no item is found with the given name, <code>null</code> will be returned.
   * Now that this model may have different items with the same name, this method is not recommended
   * to locate an item. This method just returns an item which just happens to have the given name.
   * Use {@link #indexOf(ModelItem)} or {@link #getItem(int)} to locate an item in this model.
   *
   * @param name
   * @return ModelItem by that name or <code>null</code>
   */
  public ModelItem getItem(final String name) {
    for (ModelItem item : items) if (item.getName().equals(name)) return item;
    return null;
  }

  /**
   * Returns the index of the specified item, or -1 if this list does not contain the item.
   *
   * @param item
   * @return ModelItem
   */
  public int indexOf(final ModelItem item) {
    return items.indexOf(item);
  }

  /**
   * Called by items to set their initial color
   *
   * @return 'Next' suggested item color
   */
  private RGB getNextItemColor() {
    return default_colors[items.size() % default_colors.length];
  }

  /**
   * Add item to the model.
   *
   * <p>If the item has no color, this will define its color based on the model's next available
   * color.
   *
   * <p>If the model is already 'running', the item will be 'start'ed.
   *
   * @param item {@link ModelItem} to add
   * @throws RuntimeException if item is already in model
   * @throws Exception on error trying to start a PV Item that's added to a running model
   */
  public void addItem(final ModelItem item) throws Exception {
    // A new item with the same PV name are allowed to be added in the
    // model. This way Data Browser can show the trend of the same PV
    // in different axes or with different waveform indexes. For example,
    // one may want to show the first element of epics://aaa:bbb in axis 1
    // while showing the third element of the same PV in axis 2 to compare
    // their trends in one chart.
    //
    // if (getItem(item.getName()) != null)
    //        throw new RuntimeException("Item " + item.getName() + " already in Model");

    // But, if exactly the same instance of the given ModelItem already exists in this
    // model, it will not be added.
    if (items.indexOf(item) != -1)
      throw new RuntimeException("Item " + item.getName() + " already in Model");

    // Assign default color
    if (item.getColor() == null) item.setColor(getNextItemColor());

    // Force item to be on an axis
    if (item.getAxis() == null) {
      if (axes.size() == 0) addAxis(item.getDisplayName());
      item.setAxis(axes.get(0));
    }
    // Check item axis
    if (!axes.contains(item.getAxis()))
      throw new Exception("Item " + item.getName() + " added with invalid axis " + item.getAxis());

    // Add to model
    items.add(item);
    item.setModel(this);
    if (is_running && item instanceof PVItem) ((PVItem) item).start(scanner);
    // Notify listeners of new item
    for (ModelListener listener : listeners) listener.itemAdded(item);
  }

  /**
   * Remove item from the model.
   *
   * <p>If the model and thus item are 'running', the item will be 'stopped'.
   *
   * @param item
   * @throws RuntimeException if item is already in model
   */
  public void removeItem(final ModelItem item) {
    if (is_running && item instanceof PVItem) {
      final PVItem pv = (PVItem) item;
      pv.stop();
      // Delete its samples:
      // For one, so save memory.
      // Also, in case item is later added back in, its old samples
      // will have gaps because the item was stopped
      pv.getSamples().clear();
    }
    if (!items.remove(item)) throw new RuntimeException("Unknown item " + item.getName());
    // Detach item from model
    item.setModel(null);

    // Notify listeners of removed item
    for (ModelListener listener : listeners) listener.itemRemoved(item);

    // Remove axis if unused
    AxisConfig axis = item.getAxis();
    item.setAxis(null);
    if (countActiveItemsOnAxis(axis) == 0) {
      removeAxis(axis);
      fireAxisChangedEvent(null);
    }
  }

  /** @return Period in seconds for scrolling or refreshing */
  public double getUpdatePeriod() {
    return update_period;
  }

  /** @param period_secs New update period in seconds */
  public void setUpdatePeriod(final double period_secs) {
    // Don't allow updates faster than 10Hz (0.1 seconds)
    if (period_secs < 0.1) update_period = 0.1;
    else update_period = period_secs;
    // Notify listeners
    for (ModelListener listener : listeners) listener.changedUpdatePeriod();
  }

  /**
   * The model supports two types of start/end time handling:
   *
   * <ol>
   *   <li>Scroll mode: While <code>isScrollEnabled=true</code>, the end time is supposed to be
   *       'now' and the start time is supposed to be <code>getTimespan()</code> seconds before
   *       'now'.
   *   <li>Fixed start/end time: While <code>isScrollEnabled=false</code>, the methods <code>
   *       getStartTime()</code>, <code>getEndTime</code> return a fixed start/end time.
   * </ol>
   *
   * @return <code>true</code> if scrolling is enabled
   */
  public synchronized boolean isScrollEnabled() {
    return scroll_enabled;
  }

  /** @param scroll_enabled Should scrolling be enabled? */
  public void enableScrolling(final boolean scroll_enabled) {
    synchronized (this) {
      if (this.scroll_enabled == scroll_enabled) return;
      this.scroll_enabled = scroll_enabled;
    }
    // Notify listeners
    for (ModelListener listener : listeners) listener.scrollEnabled(scroll_enabled);
  }

  /**
   * @return time span of data in seconds
   * @see #isScrollEnabled()
   */
  public synchronized double getTimespan() {
    return time_span;
  }

  /**
   * @param start_time Start and ..
   * @param end_time end time of the range to display
   */
  public void setTimerange(final Timestamp start_time, final Timestamp end_time) {
    final double new_span = end_time.durationFrom(start_time).toSeconds();
    if (new_span > 0) {
      synchronized (this) {
        this.end_time = end_time;
        time_span = new_span;
      }
    }
    // Notify listeners
    for (ModelListener listener : listeners) listener.changedTimerange();
  }

  /**
   * @param time_span time span of data in seconds
   * @see #isScrollEnabled()
   */
  public void setTimespan(final double time_span) {
    if (time_span > 0) {
      synchronized (this) {
        this.time_span = time_span;
      }
    }
    // Notify listeners
    for (ModelListener listener : listeners) listener.changedTimerange();
  }

  /**
   * @return Start time of the data range
   * @see #isScrollEnabled()
   */
  public synchronized Timestamp getStartTime() {
    return getEndTime().minus(TimeDuration.ofSeconds(time_span));
  }

  /**
   * @return End time of the data range
   * @see #isScrollEnabled()
   */
  public synchronized Timestamp getEndTime() {
    if (scroll_enabled) end_time = Timestamp.now();
    return end_time;
  }

  /**
   * @return String representation of start time. While scrolling, this is a relative time,
   *     otherwise an absolute date/time.
   */
  public synchronized String getStartSpecification() {
    if (scroll_enabled) return new RelativeTime(-time_span).toString();
    else return TimestampHelper.format(getStartTime());
  }

  /**
   * @return String representation of end time. While scrolling, this is a relative time, otherwise
   *     an absolute date/time.
   */
  public synchronized String getEndSpecification() {
    if (scroll_enabled) return RelativeTime.NOW;
    else return TimestampHelper.format(end_time);
  }

  /** @return Background color */
  public RGB getPlotBackground() {
    return background;
  }

  /** @param rgb New background color */
  public void setPlotBackground(final RGB rgb) {
    if (background.equals(rgb)) return;
    background = rgb;
    // Notify listeners
    System.out.println("**** Model.setPlotBackground() ****");

    for (ModelListener listener : listeners) listener.changedColors();
  }

  /** @param annotations Annotations to keep in model */
  public void setAnnotations(final AnnotationInfo[] annotations) {
    setAnnotations(annotations, true);
  }

  public void setAnnotations(final AnnotationInfo[] annotations, final boolean fireChanged) {
    this.annotations = annotations;
    if (fireChanged) fireAnnotationsChanged();
  }

  protected void fireAnnotationsChanged() {
    for (ModelListener listener : listeners) listener.changedAnnotations();
  }

  /** @return Annotation infos of model */
  public AnnotationInfo[] getAnnotations() {
    return annotations;
  }

  /**
   * Start all items: Connect PVs, initiate scanning, ...
   *
   * @throws Exception on error
   */
  public void start() throws Exception {
    if (is_running) throw new RuntimeException("Model already started");
    for (ModelItem item : items) {
      if (!(item instanceof PVItem)) continue;
      final PVItem pv_item = (PVItem) item;
      pv_item.start(scanner);
    }
    is_running = true;
  }

  /** Stop all items: Disconnect PVs, ... */
  public void stop() {
    if (!is_running) throw new RuntimeException("Model wasn't started");
    is_running = false;
    for (ModelItem item : items) {
      if (!(item instanceof PVItem)) continue;
      final PVItem pv_item = (PVItem) item;
      pv_item.stop();
      ImportArchiveReaderFactory.removeCachedArchives(pv_item.getArchiveDataSources());
    }
  }

  /**
   * Test if any ModelItems received new samples, if formulas need to be re-computed, since the last
   * time this method was called.
   *
   * @return <code>true</code> if there were new samples
   */
  public boolean updateItemsAndCheckForNewSamples() {
    boolean anything_new = false;
    // Update any formulas
    for (ModelItem item : items) {
      if (item instanceof FormulaItem && ((FormulaItem) item).reevaluate()) anything_new = true;
    }
    // Check and reset PV Items
    for (ModelItem item : items) {
      if (item instanceof PVItem && item.getSamples().testAndClearNewSamplesFlag())
        anything_new = true;
    }
    return anything_new;
  }

  /**
   * Notify listeners of changed axis configuration
   *
   * @param axis Axis that changed
   */
  public void fireAxisChangedEvent(final AxisConfig axis) {
    for (ModelListener listener : listeners) listener.changedAxis(axis);
  }

  /**
   * Notify listeners of changed item visibility
   *
   * @param item Item that changed
   */
  void fireItemVisibilityChanged(final ModelItem item) {
    for (ModelListener listener : listeners) listener.changedItemVisibility(item);
  }

  /**
   * Notify listeners of changed item configuration
   *
   * @param item Item that changed
   */
  void fireItemLookChanged(final ModelItem item) {
    for (ModelListener listener : listeners) listener.changedItemLook(item);
  }

  /**
   * Notify listeners of changed item configuration
   *
   * @param item Item that changed
   */
  void fireItemDataConfigChanged(final PVItem item) {
    for (ModelListener listener : listeners) listener.changedItemDataConfig(item);
  }

  /**
   * Find a formula that uses a model item as an input.
   *
   * @param item Item that's potentially used in a formula
   * @return First Formula found that uses this item, or <code>null</code> if none found
   */
  public FormulaItem getFormulaWithInput(final ModelItem item) {
    // Update any formulas
    for (ModelItem i : items) {
      if (!(i instanceof FormulaItem)) continue;
      final FormulaItem formula = (FormulaItem) i;
      if (formula.usesInput(item)) return formula;
    }
    return null;
  }

  /**
   * Write RGB color to XML document
   *
   * @param writer
   * @param level Indentation level
   * @param tag_name
   * @param color
   */
  static void writeColor(
      final PrintWriter writer, final int level, final String tag_name, final RGB color) {
    XMLWriter.start(writer, level, tag_name);
    writer.println();
    XMLWriter.XML(writer, level + 1, Model.TAG_RED, color.red);
    XMLWriter.XML(writer, level + 1, Model.TAG_GREEN, color.green);
    XMLWriter.XML(writer, level + 1, Model.TAG_BLUE, color.blue);
    XMLWriter.end(writer, level, tag_name);
    writer.println();
  }

  /**
   * Load RGB color from XML document
   *
   * @param node Parent node of the color
   * @param color_tag Name of tag that contains the color
   * @return RGB or <code>null</code> if no color found
   */
  static RGB loadColorFromDocument(final Element node, final String color_tag) {
    if (node == null) return new RGB(0, 0, 0);
    final Element color = DOMHelper.findFirstElementNode(node.getFirstChild(), color_tag);
    if (color == null) return null;
    final int red = DOMHelper.getSubelementInt(color, Model.TAG_RED, 0);
    final int green = DOMHelper.getSubelementInt(color, Model.TAG_GREEN, 0);
    final int blue = DOMHelper.getSubelementInt(color, Model.TAG_BLUE, 0);
    return new RGB(red, green, blue);
  }

  /**
   * Load RGB color from XML document
   *
   * @param node Parent node of the color
   * @return RGB or <code>null</code> if no color found
   */
  static RGB loadColorFromDocument(final Element node) {
    return loadColorFromDocument(node, Model.TAG_COLOR);
  }

  /**
   * Write XML formatted Model content.
   *
   * @param out OutputStream, will be closed when done.
   */
  public void write(final OutputStream out) {
    final PrintWriter writer = new PrintWriter(out);

    XMLWriter.header(writer);
    XMLWriter.start(writer, 0, TAG_DATABROWSER);
    writer.println();

    // L.PHILIPPE
    // Save config graph settings
    XYGraphSettingsXMLUtil XYGraphMemXML = new XYGraphSettingsXMLUtil(graphSettings);
    XYGraphMemXML.write(writer);

    // Time axis
    XMLWriter.XML(writer, 1, TAG_SCROLL, isScrollEnabled());
    XMLWriter.XML(writer, 1, TAG_UPDATE_PERIOD, getUpdatePeriod());
    if (isScrollEnabled()) {
      XMLWriter.XML(writer, 1, TAG_START, new RelativeTime(-getTimespan()));
      XMLWriter.XML(writer, 1, TAG_END, RelativeTime.NOW);
    } else {
      XMLWriter.XML(writer, 1, TAG_START, getStartTime());
      XMLWriter.XML(writer, 1, TAG_END, getEndTime());
    }

    // Time axis config
    if (timeAxis != null) {
      XMLWriter.start(writer, 1, TAG_TIME_AXIS);
      writer.println();
      timeAxis.write(writer);
      XMLWriter.end(writer, 1, TAG_TIME_AXIS);
      writer.println();
    }

    // Misc.
    writeColor(writer, 1, TAG_BACKGROUND, background);
    XMLWriter.XML(writer, 1, TAG_ARCHIVE_RESCALE, archive_rescale.name());

    // Value axes
    XMLWriter.start(writer, 1, TAG_AXES);
    writer.println();
    for (AxisConfig axis : axes) axis.write(writer);
    XMLWriter.end(writer, 1, TAG_AXES);
    writer.println();

    // Annotations
    XMLWriter.start(writer, 1, TAG_ANNOTATIONS);
    writer.println();
    for (AnnotationInfo annotation : annotations) annotation.write(writer);
    XMLWriter.end(writer, 1, TAG_ANNOTATIONS);
    writer.println();

    // PVs (Formulas)
    XMLWriter.start(writer, 1, TAG_PVLIST);
    writer.println();
    for (ModelItem item : items) item.write(writer);
    XMLWriter.end(writer, 1, TAG_PVLIST);
    writer.println();
    XMLWriter.end(writer, 0, TAG_DATABROWSER);
    writer.close();
  }

  public void setTimeAxis(AxisConfig timeAxis) {
    this.timeAxis = timeAxis;
  }

  /**
   * Read XML formatted Model content.
   *
   * @param stream InputStream, will be closed when done.
   * @throws Exception on error
   * @throws RuntimeException if model was already in use
   */
  public void read(final InputStream stream) throws Exception {
    final DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
    final Document doc = docBuilder.parse(stream);
    loadFromDocument(doc);
  }

  /**
   * Load model
   *
   * @param doc DOM document
   * @throws Exception on error
   * @throws RuntimeException if model was already in use
   */
  private void loadFromDocument(final Document doc) throws Exception {
    if (is_running || items.size() > 0) throw new RuntimeException("Model was already in use");

    // Check if it's a <databrowser/>.
    doc.getDocumentElement().normalize();
    final Element root_node = doc.getDocumentElement();
    if (!root_node.getNodeName().equals(TAG_DATABROWSER))
      throw new Exception("Wrong document type");

    synchronized (this) {
      scroll_enabled = DOMHelper.getSubelementBoolean(root_node, TAG_SCROLL, scroll_enabled);
    }
    update_period = DOMHelper.getSubelementDouble(root_node, TAG_UPDATE_PERIOD, update_period);

    final String start = DOMHelper.getSubelementString(root_node, TAG_START);
    final String end = DOMHelper.getSubelementString(root_node, TAG_END);
    if (start.length() > 0 && end.length() > 0) {
      final StartEndTimeParser times = new StartEndTimeParser(start, end);
      setTimerange(
          TimestampHelper.fromCalendar(times.getStart()),
          TimestampHelper.fromCalendar(times.getEnd()));
    }

    RGB color = loadColorFromDocument(root_node, TAG_BACKGROUND);
    if (color != null) background = color;

    try {
      archive_rescale =
          ArchiveRescale.valueOf(DOMHelper.getSubelementString(root_node, TAG_ARCHIVE_RESCALE));
    } catch (Throwable ex) {
      archive_rescale = ArchiveRescale.STAGGER;
    }

    // Load Time Axis
    final Element timeAxisNode =
        DOMHelper.findFirstElementNode(root_node.getFirstChild(), TAG_TIME_AXIS);
    if (timeAxisNode != null) {
      // Load PV items
      Element axisNode = DOMHelper.findFirstElementNode(timeAxisNode.getFirstChild(), TAG_AXIS);
      timeAxis = AxisConfig.fromDocument(axisNode);
    }

    // Load value Axes
    Element list = DOMHelper.findFirstElementNode(root_node.getFirstChild(), TAG_AXES);
    if (list != null) {
      // Load PV items
      Element item = DOMHelper.findFirstElementNode(list.getFirstChild(), TAG_AXIS);
      while (item != null) {
        addAxis(AxisConfig.fromDocument(item));
        item = DOMHelper.findNextElementNode(item, TAG_AXIS);
      }
    }

    // Load Annotations
    list = DOMHelper.findFirstElementNode(root_node.getFirstChild(), TAG_ANNOTATIONS);
    if (list != null) {
      // Load PV items
      Element item = DOMHelper.findFirstElementNode(list.getFirstChild(), TAG_ANNOTATION);
      final List<AnnotationInfo> infos = new ArrayList<AnnotationInfo>();
      try {
        while (item != null) {
          final AnnotationInfo annotation = AnnotationInfo.fromDocument(item);
          infos.add(annotation);
          item = DOMHelper.findNextElementNode(item, TAG_ANNOTATION);
        }
      } catch (Throwable ex) {
        Activator.getLogger().log(Level.INFO, "XML error in Annotation", ex);
      }
      // Add to document
      annotations = infos.toArray(new AnnotationInfo[infos.size()]);
    }

    // ADD by Laurent PHILIPPE
    // Load Title and graph settings
    try {
      graphSettings = XYGraphSettingsXMLUtil.fromDocument(root_node.getFirstChild());
    } catch (Throwable ex) {
      Activator.getLogger().log(Level.INFO, "XML error in Title or  graph settings", ex);
    }

    // Backwards compatibility with previous data browser which
    // used global buffer size for all PVs
    final int buffer_size =
        DOMHelper.getSubelementInt(root_node, Model.TAG_LIVE_SAMPLE_BUFFER_SIZE, -1);

    // Load PVs/Formulas
    list = DOMHelper.findFirstElementNode(root_node.getFirstChild(), TAG_PVLIST);
    if (list != null) {
      // Load PV items
      Element item = DOMHelper.findFirstElementNode(list.getFirstChild(), TAG_PV);
      while (item != null) {
        final PVItem model_item = PVItem.fromDocument(this, item);
        if (buffer_size > 0) model_item.setLiveCapacity(buffer_size);
        // Adding item creates the axis for it if not already there
        addItem(model_item);
        // Backwards compatibility with previous data browser which
        // stored axis configuration with each item: Update axis from that.
        final AxisConfig axis = model_item.getAxis();
        String s = DOMHelper.getSubelementString(item, TAG_AUTO_SCALE);
        if (s.equalsIgnoreCase("true")) axis.setAutoScale(true);
        s = DOMHelper.getSubelementString(item, TAG_LOG_SCALE);
        if (s.equalsIgnoreCase("true")) axis.setLogScale(true);
        final double min = DOMHelper.getSubelementDouble(item, Model.TAG_MIN, axis.getMin());
        final double max = DOMHelper.getSubelementDouble(item, Model.TAG_MAX, axis.getMax());
        axis.setRange(min, max);

        item = DOMHelper.findNextElementNode(item, TAG_PV);
      }
      // Load Formulas
      item = DOMHelper.findFirstElementNode(list.getFirstChild(), TAG_FORMULA);
      while (item != null) {
        addItem(FormulaItem.fromDocument(this, item));
        item = DOMHelper.findNextElementNode(item, TAG_FORMULA);
      }
    }
  }
}