/** * Create PVItem from XML document * * @param model Model to which this item will belong (but doesn't, yet) * @param node XML node with item configuration * @return PVItem * @throws Exception on error */ public static PVItem fromDocument(final Model model, final Element node) throws Exception { final String name = DOMHelper.getSubelementString(node, Model.TAG_NAME); final double period = DOMHelper.getSubelementDouble(node, Model.TAG_SCAN_PERIOD, 0.0); final PVItem item = new PVItem(name, period); final int buffer_size = DOMHelper.getSubelementInt( node, Model.TAG_LIVE_SAMPLE_BUFFER_SIZE, Preferences.getLiveSampleBufferSize()); item.setLiveCapacity(buffer_size); final String req_txt = DOMHelper.getSubelementString(node, Model.TAG_REQUEST, RequestType.OPTIMIZED.name()); try { final RequestType request = RequestType.valueOf(req_txt); item.setRequestType(request); } catch (Throwable ex) { // Ignore } item.configureFromDocument(model, node); // Load archives from saved configuration boolean have_imported_data = false; Element archive = DOMHelper.findFirstElementNode(node.getFirstChild(), Model.TAG_ARCHIVE); while (archive != null) { final String url = DOMHelper.getSubelementString(archive, Model.TAG_URL); final int key = DOMHelper.getSubelementInt(archive, Model.TAG_KEY); final String arch = DOMHelper.getSubelementString(archive, Model.TAG_NAME); if (url.startsWith(ImportArchiveReaderFactory.PREFIX)) have_imported_data = true; item.addArchiveDataSource(new ArchiveDataSource(url, key, arch)); archive = DOMHelper.findNextElementNode(archive, Model.TAG_ARCHIVE); } // When requested, use default archive sources for 'real' archives (RDB, ...) // Do not clobber an imported archive data source, a specific file which was // probably not meant to be replaced by a default. if (Preferences.useDefaultArchives() && !have_imported_data) item.useDefaultArchiveDataSources(); return item; }
/** * 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); } } } }
/** {@inheritDoc} */ @Override public void run() { Activator.getLogger().log(Level.FINE, "Starting {0}", ArchiveFetchJob.this); // $NON-NLS-1$ final BenchmarkTimer timer = new BenchmarkTimer(); long samples = 0; final int bins = Preferences.getPlotBins(); final ArchiveDataSource archives[] = item.getArchiveDataSources(); List<ArchiveDataSource> sourcesWhereChannelDoesntExist = new ArrayList<>(); for (int i = 0; i < archives.length && !cancelled; ++i) { final ArchiveDataSource archive = archives[i]; final String url = archive.getUrl(); // Display "N/total", using '1' for the first sub-archive. synchronized (this) { message = NLS.bind( Messages.ArchiveFetchDetailFmt, new Object[] {archive.getName(), (i + 1), archives.length}); } try { final ArchiveReader the_reader; synchronized (this) { the_reader = reader = ArchiveRepository.getInstance().getArchiveReader(url); } the_reader.enableConcurrency(concurrency); final ValueIterator value_iter; try { if (item.getRequestType() == RequestType.RAW) value_iter = the_reader.getRawValues(archive.getKey(), item.getResolvedName(), start, end); else value_iter = the_reader.getOptimizedValues( archive.getKey(), item.getResolvedName(), start, end, bins); } catch (UnknownChannelException e) { // Do not immediately notify about unknown channels. First search for the data in all // archive // sources and only report this kind of errors at the end sourcesWhereChannelDoesntExist.add(archives[i]); continue; } // Get samples into array final List<VType> result = new ArrayList<VType>(); while (value_iter.hasNext()) result.add(value_iter.next()); samples += result.size(); item.mergeArchivedSamples(the_reader.getServerName(), result); if (cancelled) break; value_iter.close(); } catch (Exception ex) { // Tell listener unless it's the result of a 'cancel'? if (!cancelled) listener.archiveFetchFailed(ArchiveFetchJob.this, archive, ex); // Continue with the next data source } finally { synchronized (this) { if (reader != null) reader.close(); reader = null; } } } if (!sourcesWhereChannelDoesntExist.isEmpty() && !cancelled) { listener.channelNotFound( ArchiveFetchJob.this, sourcesWhereChannelDoesntExist.size() < archives.length, sourcesWhereChannelDoesntExist.toArray( new ArchiveDataSource[sourcesWhereChannelDoesntExist.size()])); } timer.stop(); if (!cancelled) listener.fetchCompleted(ArchiveFetchJob.this); Activator.getLogger() .log( Level.FINE, "Ended {0} with {1} samples in {2}", //$NON-NLS-1$ new Object[] {ArchiveFetchJob.this, samples, timer}); }
/** * Data Browser Model Item for 'live' PV. * * <p>Holds both historic and live data in PVSamples. Performs the periodic scans of a control * system PV. * * <p>Also implements IProcessVariable so that context menus can link to related CSS tools. * * @author Kay Kasemir * @author Takashi Nakamoto changed PVItem to handle waveform index. */ public class PVItem extends ModelItem implements PVReaderListener<List<VType>>, Cloneable { /** Historic and 'live' samples for this PV */ private PVSamples samples = new PVSamples(); /** Where to get archived data for this item. */ private ArrayList<ArchiveDataSource> archives = new ArrayList<ArchiveDataSource>(); /** Control system PV, set when running */ private PVReader<List<VType>> pv = null; /** Most recently received value */ private volatile VType current_value; /** Scan period in seconds, ≤0 to 'monitor' */ private double period; /** Timer that was used to schedule the scanner */ private Timer scan_timer = null; /** For a period >0, this timer task performs the scanning */ private TimerTask scanner = null; /** Archive data request type */ private RequestType request_type = RequestType.OPTIMIZED; /** Waveform Index */ private int waveform_index = 0; /** * Indicating if the history data is automatically refreshed, whenever the live buffer is too * small to show all the data */ private boolean automaticRefresh = Preferences.isAutomaticHistoryRefresh(); /** * Initialize * * @param name PV name * @param period Scan period in seconds, ≤0 to 'monitor' * @throws Exception on error */ public PVItem(final String name, final double period) throws Exception { super(name); this.period = period; } /** @return Waveform index */ @Override public int getWaveformIndex() { return waveform_index; } /** @param index New waveform index */ @Override public void setWaveformIndex(int index) { if (index < 0) index = 0; if (index == waveform_index) return; waveform_index = index; // change all the index of samples in this instance samples.setWaveformIndex(waveform_index); // fireItemLookChanged(); } /** Set new item name, which changes the underlying PV name {@inheritDoc} */ @Override public boolean setName(final String new_name) throws Exception { if (!super.setName(new_name)) return false; // Stop PV, clear samples final boolean running = (pv != null); if (running) stop(); samples.clear(); // Create new PV, maybe start it if (running) start(scan_timer); return true; } /** @return Scan period in seconds, ≤0 to 'monitor' */ public double getScanPeriod() { return period; } /** * Update scan period. * * <p>When called on a running item, this stops and re-starts the PV. * * @param period New scan period in seconds, ≤0 to 'monitor' * @throws Exception On error re-starting a running PVItem */ public void setScanPeriod(double period) throws Exception { // Don't 'scan' faster than 1 Hz. Instead switch to on-change. if (period < 0.1) period = 0.0; final boolean running = (pv != null); if (running) stop(); this.period = period; if (running) start(scan_timer); // fireItemLookChanged(); } @Override void setModel(Model model) { super.setModel(model); this.automaticRefresh = model.isAutomaticHistoryRefresh(); } /** @return Maximum number of live samples in ring buffer */ public int getLiveCapacity() { return samples.getLiveCapacity(); } /** * Set new capacity for live sample ring buffer * * <p> * * @param new_capacity New sample count capacity * @throws Exception on out-of-memory error */ public void setLiveCapacity(final int new_capacity) throws Exception { samples.setLiveCapacity(new_capacity); // fireItemLookChanged(); } /** @return Archive data sources for this item */ public ArchiveDataSource[] getArchiveDataSources() { return (ArchiveDataSource[]) archives.toArray(new ArchiveDataSource[archives.size()]); } /** Replace archives with settings from preferences */ public void useDefaultArchiveDataSources() { archives.clear(); for (ArchiveDataSource arch : Preferences.getArchives()) archives.add(arch); fireItemDataConfigChanged(); } /** * @param archive Archive data source * @return <code>true</code> if PV uses given data source */ public boolean hasArchiveDataSource(final ArchiveDataSource archive) { for (ArchiveDataSource arch : archives) if (arch.equals(archive)) return true; return false; } /** * @param archive Archive to add as a source to this item * @throws Error when archive is already used */ @SuppressWarnings("nls") public void addArchiveDataSource(final ArchiveDataSource archive) { if (hasArchiveDataSource(archive)) throw new Error("Duplicate archive " + archive); archives.add(archive); fireItemDataConfigChanged(); } /** @param archive Archives to add as a source to this item. Duplicates are ignored */ public void addArchiveDataSource(final ArchiveDataSource archs[]) { boolean change = false; for (ArchiveDataSource archive : archs) if (!archives.contains(archive)) { change = true; archives.add(archive); } if (change) fireItemDataConfigChanged(); } /** @param archive Archive to remove as a source from this item. */ public void removeArchiveDataSource(final ArchiveDataSource archive) { if (archives.remove(archive)) fireItemDataConfigChanged(); } /** @param archive Archives to remove as a source from this item. Ignored when not used. */ public void removeArchiveDataSource(final ArchiveDataSource archs[]) { boolean change = false; for (ArchiveDataSource archive : archs) if (archives.remove(archive)) change = true; if (change) fireItemDataConfigChanged(); } /** * Replace existing archive data sources with given archives * * @param archs ArchiveDataSources to use for this item */ public void setArchiveDataSource(final ArchiveDataSource archs[]) { // Check if they are the same, i.e. count AND order match if (archs.length == archives.size()) { boolean same = true; for (int i = 0; i < archs.length; ++i) if (!archs[i].equals(archives.get(i))) { same = false; break; } if (same) return; } // Different archives archives.clear(); for (ArchiveDataSource arch : archs) archives.add(arch); fireItemDataConfigChanged(); } /** @return Archive data request type */ public RequestType getRequestType() { return request_type; } /** @param request_type New request type */ public void setRequestType(final RequestType request_type) { if (this.request_type == request_type) return; this.request_type = request_type; fireItemDataConfigChanged(); } /** Notify listeners */ private void fireItemDataConfigChanged() { if (model != null) model.fireItemDataConfigChanged(this); } /** * Connect control system PV, start scanning, ... * * @throws Exception on error */ @SuppressWarnings("nls") public void start(final Timer timer) throws Exception { if (pv != null) throw new RuntimeException("Already started " + getName()); this.scan_timer = timer; pv = PVManager.read(newValuesOf(vType(getResolvedName()))) .timeout(ofSeconds(30.0)) .readListener(this) .maxRate(ofSeconds(0.1)); // Log every received value? if (period <= 0.0) return; // Start scanner for periodic log scanner = new TimerTask() { @Override public void run() { final VType value = current_value; Activator.getLogger() .log(Level.FINE, "PV {0} scans {1}", new Object[] {getName(), value}); logValueAsNow(value); } }; final long delay = (long) (period * 1000); timer.schedule(scanner, delay, delay); } /** Disconnect from control system PV, stop scanning, ... */ @SuppressWarnings("nls") public void stop() { if (pv == null) throw new RuntimeException("Not running " + getName()); if (scanner != null) { scanner.cancel(); scanner = null; } pv.close(); pv = null; } /** {@inheritDoc} */ @Override public PVSamples getSamples() { return samples; } /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public void pvChanged(final PVReaderEvent<List<VType>> event) { final PVReader<List<VType>> pv = event.getPvReader(); // Check for error final Exception error = pv.lastException(); if (error != null) Activator.getLogger().log(Level.FINE, "PV " + pv.getName() + " error", error); final List<VType> values = pv.getValue(); if (values == null) { // No current value current_value = null; // In 'monitor' mode, mark in live sample buffer if (period <= 0) logDisconnected(); return; } else { boolean added = false; for (VType value : values) { // Cache most recent for 'scanned' operation current_value = value; // In 'monitor' mode, add to live sample buffer if (period <= 0) { Activator.getLogger() .log(Level.FINE, "PV {0} received {1}", new Object[] {getName(), value}); samples.addLiveSample(value); added = true; } } if (automaticRefresh && added && samples.isHistoryRefreshNeeded(model.getStartTime(), model.getEndTime())) { model.fireItemRefreshRequested(this); } } } /** @param value Value to log with 'now' as time stamp */ private void logValueAsNow(final VType value) { if (value == null) logDisconnected(); else // Transform value to have 'now' as time stamp samples.addLiveSample(VTypeHelper.transformTimestampToNow(value)); } /** Add one(!) 'disconnected' sample */ private void logDisconnected() { synchronized (samples) { final int size = samples.getSize(); if (size > 0) { final String last = VTypeHelper.getMessage(samples.getSample(size - 1).getValue()); // Does last sample already have 'disconnected' status? if (Messages.Model_Disconnected.equals(last)) return; } samples.addLiveSample(new PlotSample(Messages.LiveData, Messages.Model_Disconnected)); } } /** * Add data retrieved from an archive to the 'historic' section * * @param server_name Archive server that provided these samples * @param new_samples Historic data */ public synchronized void mergeArchivedSamples( final String server_name, final List<VType> new_samples) { samples.mergeArchivedData(server_name, new_samples); if (automaticRefresh && samples.isHistoryRefreshNeeded(model.getStartTime(), model.getEndTime())) { model.fireItemRefreshRequested(this); } } /** * Write XML formatted PV configuration * * @param writer PrintWriter */ @Override public void write(final PrintWriter writer) { XMLWriter.start(writer, 2, Model.TAG_PV); writer.println(); writeCommonConfig(writer); XMLWriter.XML(writer, 3, Model.TAG_SCAN_PERIOD, getScanPeriod()); XMLWriter.XML(writer, 3, Model.TAG_LIVE_SAMPLE_BUFFER_SIZE, getLiveCapacity()); XMLWriter.XML(writer, 3, Model.TAG_REQUEST, getRequestType().name()); for (ArchiveDataSource archive : archives) { XMLWriter.start(writer, 3, Model.TAG_ARCHIVE); writer.println(); XMLWriter.XML(writer, 4, Model.TAG_NAME, archive.getName()); XMLWriter.XML(writer, 4, Model.TAG_URL, archive.getUrl()); XMLWriter.XML(writer, 4, Model.TAG_KEY, archive.getKey()); XMLWriter.end(writer, 3, Model.TAG_ARCHIVE); writer.println(); } XMLWriter.end(writer, 2, Model.TAG_PV); writer.println(); } /** * Create PVItem from XML document * * @param model Model to which this item will belong (but doesn't, yet) * @param node XML node with item configuration * @return PVItem * @throws Exception on error */ public static PVItem fromDocument(final Model model, final Element node) throws Exception { final String name = DOMHelper.getSubelementString(node, Model.TAG_NAME); final double period = DOMHelper.getSubelementDouble(node, Model.TAG_SCAN_PERIOD, 0.0); final PVItem item = new PVItem(name, period); final int buffer_size = DOMHelper.getSubelementInt( node, Model.TAG_LIVE_SAMPLE_BUFFER_SIZE, Preferences.getLiveSampleBufferSize()); item.setLiveCapacity(buffer_size); final String req_txt = DOMHelper.getSubelementString(node, Model.TAG_REQUEST, RequestType.OPTIMIZED.name()); try { final RequestType request = RequestType.valueOf(req_txt); item.setRequestType(request); } catch (Throwable ex) { // Ignore } item.configureFromDocument(model, node); // Load archives from saved configuration boolean have_imported_data = false; Element archive = DOMHelper.findFirstElementNode(node.getFirstChild(), Model.TAG_ARCHIVE); while (archive != null) { final String url = DOMHelper.getSubelementString(archive, Model.TAG_URL); final int key = DOMHelper.getSubelementInt(archive, Model.TAG_KEY); final String arch = DOMHelper.getSubelementString(archive, Model.TAG_NAME); if (url.startsWith(ImportArchiveReaderFactory.PREFIX)) have_imported_data = true; item.addArchiveDataSource(new ArchiveDataSource(url, key, arch)); archive = DOMHelper.findNextElementNode(archive, Model.TAG_ARCHIVE); } // When requested, use default archive sources for 'real' archives (RDB, ...) // Do not clobber an imported archive data source, a specific file which was // probably not meant to be replaced by a default. if (Preferences.useDefaultArchives() && !have_imported_data) item.useDefaultArchiveDataSources(); return item; } public PVItem clone() { PVItem ret = (PVItem) super.clone(); ret.samples = samples; ret.archives = archives; ret.pv = pv; ret.current_value = current_value; ret.period = period; ret.scan_timer = scan_timer; ret.request_type = request_type; ret.waveform_index = waveform_index; return ret; } }
/** Replace archives with settings from preferences */ public void useDefaultArchiveDataSources() { archives.clear(); for (ArchiveDataSource arch : Preferences.getArchives()) archives.add(arch); fireItemDataConfigChanged(); }