package org.yamcs.ui.archivebrowser;
import org.yamcs.TimeInterval;
import org.yamcs.protobuf.Yamcs.ArchiveRecord;
import org.yamcs.protobuf.Yamcs.ArchiveTag;
import org.yamcs.protobuf.Yamcs.IndexResult;
import org.yamcs.ui.UiColors;
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.awt.event.AWTEventListener;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryType;
import java.util.*;
import java.util.List;
/**
* Main panel of the ArchiveBrowser
* @author nm
*
*/
public class ArchivePanel extends JPanel implements PropertyChangeListener {
private static final long serialVersionUID = 1L;
ProgressMonitor progressMonitor;
ArchiveBrowser archiveBrowser;
JLabel totalRangeLabel;
JLabel statusInfoLabel;
JLabel instanceLabel;
private LinkedHashMap<String,NavigatorItem> itemsByName=new LinkedHashMap<String, NavigatorItem>();
SideNavigator sideNavigator;
JToolBar archiveToolbar;
protected PrefsToolbar prefs;
private JPanel insetPanel; // Contains switchable navigator insets (depends on open item)
private JPanel navigatorItemPanel; // Contains switchable items
private NavigatorItem activeItem; // Currently opened item from SideNav
public ReplayPanel replayPanel;
int loadCount, recCount;
boolean passiveUpdate = false;
private TimeInterval receivedDataInterval = new TimeInterval();
volatile boolean lowOnMemoryReported=false;
//used to check for out of memory errors that may happen when receiving too many archive records
MemoryPoolMXBean heapMemoryPoolBean = null;
public ArchivePanel(ArchiveBrowser archiveBrowser, boolean replayEnabled) {
super(new BorderLayout());
this.archiveBrowser=archiveBrowser;
/*
* Upper fixed content
*/
Box fixedTop = Box.createVerticalBox();
fixedTop.setBorder(BorderFactory.createMatteBorder(0, 0, 2, 0, UiColors.BORDER_COLOR));
prefs = new PrefsToolbar();
prefs.setAlignmentX(Component.LEFT_ALIGNMENT);
fixedTop.add(prefs);
archiveToolbar = new JToolBar();
archiveToolbar.setFloatable(false);
archiveToolbar.setAlignmentX(Component.LEFT_ALIGNMENT);
fixedTop.add(archiveToolbar);
//
// transport control panel (only enabled when a HRDP data channel is selected)
//
if (replayEnabled) {
replayPanel = new ReplayPanel();
replayPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), "Replay Control"));
replayPanel.setToolTipText("Doubleclick between the start/stop locators to reposition the replay.");
replayPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
fixedTop.add(replayPanel);
}
// This is a bit clumsy right now with the inner classes, but will make more
// sense once we add custom components to different DataViewers.
DataViewer allViewer = new DataViewer(archiveBrowser.yconnector, archiveBrowser.indexReceiver, this, replayEnabled) {
@Override
public String getLabelName() {
return "Archive";
}
@Override
public JComponent createContentPanel() {
JComponent component = super.createContentPanel();
addIndex("completeness", "completeness index");
addIndex("tm", "tm histogram", 1000);
addIndex("pp", "pp histogram", 1000);
addIndex("cmdhist", "cmdhist histogram", 1000);
addVerticalGlue();
return component;
}
};
itemsByName.put(allViewer.getLabelName(), allViewer);
DataViewer completenessViewer = new DataViewer(archiveBrowser.yconnector, archiveBrowser.indexReceiver, this, false) {
@Override
public String getLabelName() {
return "Completeness";
}
@Override
public int getIndent() {
return 1;
}
@Override
public JComponent createContentPanel() {
JComponent component = super.createContentPanel();
addIndex("completeness", "completeness index");
addVerticalGlue();
return component;
}
};
itemsByName.put(completenessViewer.getLabelName(), completenessViewer);
DataViewer tmViewer = new DataViewer(archiveBrowser.yconnector, archiveBrowser.indexReceiver, this, replayEnabled) {
@Override
public String getLabelName() {
return "Telemetry";
}
@Override
public int getIndent() {
return 1;
}
@Override
public JComponent createContentPanel() {
JComponent component = super.createContentPanel();
addIndex("tm", "tm histogram", 1000);
addVerticalGlue();
return component;
}
};
itemsByName.put(tmViewer.getLabelName(), tmViewer);
DataViewer ppViewer = new DataViewer(archiveBrowser.yconnector, archiveBrowser.indexReceiver, this, false) {
@Override
public String getLabelName() {
return "Processed Parameters";
}
@Override
public int getIndent() {
return 1;
}
@Override
public JComponent createContentPanel() {
JComponent component = super.createContentPanel();
addIndex("pp", "pp histogram", 1000);
addVerticalGlue();
return component;
}
};
itemsByName.put(ppViewer.getLabelName(), ppViewer);
DataViewer cmdViewer = new DataViewer(archiveBrowser.yconnector, archiveBrowser.indexReceiver, this, false) {
@Override
public String getLabelName() {
return "Command History";
}
@Override
public int getIndent() {
return 1;
}
@Override
public JComponent createContentPanel() {
JComponent component = super.createContentPanel();
addIndex("cmdhist", "cmdhist histogram", 1000);
addVerticalGlue();
return component;
}
};
itemsByName.put(cmdViewer.getLabelName(), cmdViewer);
add(fixedTop, BorderLayout.NORTH);
add(createStatusBar(), BorderLayout.SOUTH);
sideNavigator = new SideNavigator(this);
add(sideNavigator, BorderLayout.WEST);
navigatorItemPanel = new JPanel(new CardLayout());
add(navigatorItemPanel, BorderLayout.CENTER);
insetPanel = new JPanel(new CardLayout());
insetPanel.setVisible(false);
sideNavigator.add(insetPanel, BorderLayout.SOUTH);
for(Map.Entry<String, NavigatorItem> entry:itemsByName.entrySet()) {
String name = entry.getKey();
NavigatorItem navigatorItem = entry.getValue();
navigatorItemPanel.add(navigatorItem.getContentPanel(), name);
sideNavigator.addItem(name, navigatorItem.getIndent(), navigatorItem);
JComponent navInset = navigatorItem.getNavigatorInset();
if(navInset!=null) {
insetPanel.add(navInset, name);
}
}
openItem("Archive");
for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {
if (pool.getType() == MemoryType.HEAP && pool.isCollectionUsageThresholdSupported()) {
heapMemoryPoolBean = pool;
heapMemoryPoolBean.setCollectionUsageThreshold((int)Math.floor(heapMemoryPoolBean.getUsage().getMax()*0.95));
}
}
// Catch mouse events globally, to deal more easily with events on child components
Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
@Override
public void eventDispatched(AWTEvent event) { // EDT
if(activeItem instanceof DataViewer) {
DataView dataView = ((DataViewer)activeItem).getDataView();
if (!(event.getSource() instanceof JScrollBar)
&& !(event.getSource() instanceof TagTimeline)
&& SwingUtilities.isDescendingFrom((Component)event.getSource(), dataView)) {
MouseEvent me = SwingUtilities.convertMouseEvent((Component)event.getSource(), (MouseEvent) event, dataView.indexPanel);
if(event.getID()==MouseEvent.MOUSE_DRAGGED) {
dataView.doMouseDragged(me);
} else if(event.getID()==MouseEvent.MOUSE_PRESSED) {
dataView.doMousePressed(me);
} else if(event.getID()==MouseEvent.MOUSE_RELEASED) {
dataView.doMouseReleased(me);
} else if(event.getID()==MouseEvent.MOUSE_MOVED) {
dataView.doMouseMoved(me);
} else if(event.getID()==MouseEvent.MOUSE_EXITED) {
dataView.doMouseExited(me);
}
}
}
}
}, AWTEvent.MOUSE_EVENT_MASK + AWTEvent.MOUSE_MOTION_EVENT_MASK);
}
public void openItem(String name) {
NavigatorItem item=getItemByName(name);
fireIntentionToSwitchActiveItem(item);
}
public NavigatorItem getItemByName(String name) {
return itemsByName.get(name);
}
void fireIntentionToSwitchActiveItem(NavigatorItem sourceItem) {
if(activeItem == sourceItem) return;
sideNavigator.updateActiveItem(sourceItem); // UI-only
CardLayout ncl = (CardLayout) navigatorItemPanel.getLayout();
ncl.show(navigatorItemPanel, sourceItem.getLabelName());
if(sourceItem.getNavigatorInset()!=null) {
CardLayout icl = (CardLayout) insetPanel.getLayout();
icl.show(insetPanel, sourceItem.getLabelName());
insetPanel.setVisible(true);
} else {
insetPanel.setVisible(false);
}
if(activeItem!=null) {
activeItem.onClose();
}
sourceItem.onOpen();
activeItem = sourceItem;
}
private Box createStatusBar() {
Box bar = Box.createHorizontalBox();
Border outsideBorder = BorderFactory.createMatteBorder(2, 0, 0, 0, UiColors.BORDER_COLOR);
Border insideBorder = BorderFactory.createEmptyBorder(5, 10, 5, 10);
bar.setBorder(BorderFactory.createCompoundBorder(outsideBorder, insideBorder));
bar.add(Box.createHorizontalGlue());
bar.add(createLabelForStatusBar(" Instance: ")); // Front space serves as left padding
instanceLabel = createLabelForStatusBar(null);
bar.add(instanceLabel);
bar.add(createLabelForStatusBar(", Data Range: "));
totalRangeLabel = createLabelForStatusBar(null);
bar.add(totalRangeLabel);
bar.add(Box.createHorizontalGlue());
statusInfoLabel = createLabelForStatusBar(null);
bar.add(statusInfoLabel);
return bar;
}
private JLabel createLabelForStatusBar(String text) {
JLabel lbl = new JLabel();
if(text != null) {
lbl.setText(text);
}
lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN, lbl.getFont().getSize2D()-2));
return lbl;
}
void updateStatusBar() {
passiveUpdate = true;
if (loadCount == 0) {
statusInfoLabel.setText("(no data loaded) ");
} else {
statusInfoLabel.setText("Loading Data ... " + loadCount + " ");
statusInfoLabel.repaint();
}
totalRangeLabel.setText(receivedDataInterval.toStringEncoded());
totalRangeLabel.repaint();
passiveUpdate = false;
}
public synchronized void startReloading() {
recCount=0;
archiveBrowser.setInstance(prefs.getInstance());
setBusyPointer();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
prefs.reloadButton.setEnabled(false);
instanceLabel.setText(archiveBrowser.getInstance());
}
});
for(NavigatorItem item:itemsByName.values()) {
item.startReloading();
}
if(lowOnMemoryReported) {
System.gc();
lowOnMemoryReported=false;
}
receivedDataInterval = new TimeInterval();
}
public static ImageIcon getIcon(String imagename) {
return new ImageIcon(ArchivePanel.class.getResource("/org/yamcs/images/" + imagename));
}
static protected void debugLog(String s) {
System.out.println(s);
}
static protected void debugLogComponent(String name, JComponent c) {
Insets in = c.getInsets();
debugLog("component " + name + ": "
+ "min(" + c.getMinimumSize().width + "," + c.getMinimumSize().height + ") "
+ "pref(" + c.getPreferredSize().width + "," + c.getPreferredSize().height + ") "
+ "max(" + c.getMaximumSize().width + "," + c.getMaximumSize().height + ") "
+ "size(" + c.getSize().width + "," + c.getSize().height + ") "
+ "insets(" + in.top + "," + in.left + "," +in.bottom + "," +in.right + ")");
}
void playOrStopPressed() {
// to be reimplemented by subclass ArchiveReplay in YamcsMonitor
}
@Override
public void propertyChange(PropertyChangeEvent e) {
debugLog(e.getPropertyName()+"/"+e.getOldValue()+"/"+e.getNewValue());
}
/**
* Called when the connection to yamcs is (re)established, (re)populates the list of hrdp instances
* @param archiveInstances
*/
public void setInstances(final List<String> archiveInstances) {
prefs.setInstances(archiveInstances);
}
static class IndexChunkSpec implements Comparable<IndexChunkSpec>{
long startInstant, stopInstant;
int tmcount;
String info;
IndexChunkSpec(long start, long stop, int tmcount, String info) {
this.startInstant = start;
this.stopInstant = stop;
this.tmcount = tmcount;
this.info=info;
}
/**
*
* @return frequency in Hz
*/
float getFrequency() {
float freq = (float)(tmcount-1) / ((stopInstant - startInstant) / 1000.0f);
freq = Math.round(freq * 1000) / 1000.0f;
return freq;
}
@Override
public int compareTo(IndexChunkSpec a) {
return Long.signum(startInstant-a.startInstant);
}
//merge two records if close enough to eachother
public boolean merge(IndexChunkSpec t, long mergeTime) {
boolean merge=false;
if(tmcount==1) {
if(t.startInstant-stopInstant<mergeTime){
merge=true;
}
} else {
float dist=(stopInstant-startInstant)/((float)(tmcount-1));
if(t.startInstant-stopInstant<dist+mergeTime) {
merge=true;
}
}
if(merge) {
stopInstant=t.stopInstant;
tmcount+=t.tmcount;
}
return merge;
}
@Override
public String toString() {
return "start: "+startInstant+" stop: "+stopInstant+" count:"+tmcount;
}
}
public void connected() {
prefs.reloadButton.setEnabled(true);
}
public void disconnected() {
prefs.reloadButton.setEnabled(false);
}
public void setBusyPointer() {
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
}
public void setNormalPointer() {
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
public synchronized TimeInterval getRequestedDataInterval() {
return prefs.getInterval();
}
public synchronized void receiveArchiveRecords(IndexResult ir) {
if((heapMemoryPoolBean!=null) && (heapMemoryPoolBean.isCollectionUsageThresholdExceeded())) {
if(!lowOnMemoryReported) {
lowOnMemoryReported=true;
receiveArchiveRecordsError("The memory is almost exhausted, ignoring received Archive Records. Consider increasing the maximum heap size -Xmx parameter");
}
return;
}
for(NavigatorItem navigatorItem:itemsByName.values()) {
navigatorItem.receiveArchiveRecords(ir);
}
long start, stop;
for (ArchiveRecord r:ir.getRecordsList()) {
//debugLog(r.packet+"\t"+r.num+"\t"+new Date(r.first)+"\t"+new Date(r.last));
start = r.getFirst();
stop = r.getLast();
if ((!receivedDataInterval.hasStart()) || (start<receivedDataInterval.getStart())) {
receivedDataInterval.setStart(start);
}
if ((!receivedDataInterval.hasStop()) || (stop>receivedDataInterval.getStop())) {
receivedDataInterval.setStop(stop);
}
recCount++;
loadCount++;
updateStatusBar();
}
}
public synchronized void receiveArchiveRecordsError(final String errorMessage) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
for(NavigatorItem navigatorItem:itemsByName.values()) {
navigatorItem.receiveArchiveRecordsError(errorMessage);
}
JOptionPane.showMessageDialog(ArchivePanel.this, "Error when receiving archive records: "+errorMessage, "error receiving archive records", JOptionPane.ERROR_MESSAGE);
prefs.reloadButton.setEnabled(true);
setNormalPointer();
}
});
}
void seekReplay(long newPosition) {
replayPanel.seekReplay(newPosition);
}
public synchronized void archiveLoadFinished() {
loadCount = 0;
if (receivedDataInterval.hasStart() && receivedDataInterval.hasStop()) {
for(NavigatorItem item:itemsByName.values()) {
item.archiveLoadFinished();
}
prefs.savePreferences();
}
SwingUtilities.invokeLater(() -> {
statusInfoLabel.setText("");
prefs.reloadButton.setEnabled(true);
setNormalPointer();
});
}
public void tagAdded(ArchiveTag ntag) {
for(NavigatorItem navigatorItem:itemsByName.values()) {
navigatorItem.tagAdded(ntag);
}
}
public void tagRemoved(ArchiveTag rtag) {
for(NavigatorItem navigatorItem:itemsByName.values()) {
navigatorItem.tagRemoved(rtag);
}
}
public void tagChanged(ArchiveTag oldTag, ArchiveTag newTag) {
for(NavigatorItem navigatorItem:itemsByName.values()) {
navigatorItem.tagChanged(oldTag, newTag);
}
}
public void tagsAdded(List<ArchiveTag> tagList) {
for(NavigatorItem navigatorItem:itemsByName.values()) {
navigatorItem.receiveTags(tagList);
}
}
// TODO only used by selector. Rework maybe in custom replay launcher
public Selection getSelection() {
DataViewer dataViewer = (DataViewer) activeItem;
return dataViewer.getDataView().getSelection();
}
// TODO only used by selector. Rework maybe in custom replay launcher
public List<String> getSelectedPackets(String tableName) {
DataViewer dataViewer = (DataViewer) activeItem;
if(dataViewer.getDataView().indexBoxes.containsKey(tableName)) {
return dataViewer.getDataView().getSelectedPackets("tm");
}
return Collections.emptyList();
}
public void onWindowResizing() {
activeItem.windowResized();
}
public void onWindowResized() {
for (NavigatorItem navigatorItem : itemsByName.values()) {
navigatorItem.windowResized();
}
}
public synchronized TimeInterval getReceivedDataInterval() {
return receivedDataInterval;
}
}