package org.yamcs.ui.archivebrowser;
import org.yamcs.TimeInterval;
import org.yamcs.protobuf.Yamcs.ArchiveTag;
import org.yamcs.utils.TimeEncoding;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.List;
/**
* Shows a number of IndexBoxes together. A timeline and a tag bar is shared
* among all included IndexBoxes.
* Range selections can be made which visually span all included IndexBoxes
*/
public class DataView extends JScrollPane {
private static final long serialVersionUID = 1L;
HeaderPanel headerPanel;
IndexPanel indexPanel;
Map<String,IndexBox> indexBoxes = new HashMap<String,IndexBox>();
private boolean showTagBox = true;
Stack<ZoomSpec> zoomStack = new Stack<ZoomSpec>();
private List<ActionListener> actionListeners=new ArrayList<ActionListener>();
private DataViewer dataViewer;
ArchivePanel archivePanel;
boolean hideResponsePackets=true;
long lastStartTimestamp = -1;
long lastEndTimestamp = -1;
boolean startColor = false;
int startX, stopX, deltaX;
boolean drawPreviewLocator;
float previewLocatorAlpha;
int dragButton, previewLocatorX, mouseLocatorX;
SelectionImpl currentSelection;
long startLocator, stopLocator, currentLocator, previewLocator, seekLocator;
final long DO_NOT_DRAW = Long.MIN_VALUE;
final int handleWidth = 6;
final int handleHeight = 12;
final int handleWidth2 = 9; // current position locator
final int cursorSnap = 2; // epsilon between mouse position and handle line to detect cursor change
final Color handleFill = Color.YELLOW; // start/stop locator colour
final Color currentFill = Color.GREEN; // current replay position locator colour
int antsOffset = 0;
final int antsLength = 8;
public DataView(ArchivePanel archivePanel, DataViewer dataViewer) {
super(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
this.dataViewer = dataViewer;
this.archivePanel = archivePanel;
setBorder(BorderFactory.createEmptyBorder());
getViewport().setOpaque(false);
setPreferredSize(new Dimension(850, 400));
headerPanel = new HeaderPanel();
indexPanel = new IndexPanel();
setColumnHeaderView(headerPanel);
setViewportView(indexPanel);
getColumnHeader().setOpaque(false);
startLocator = stopLocator = currentLocator = DO_NOT_DRAW;
drawPreviewLocator = false;
resetSelection();
new java.util.Timer().schedule(new TimerTask() {
@Override
public void run() {
synchronized (this) {
if (getComponentCount() > 0) {
final int y = getComponent(0).getLocation().y;
final int h = getSize().height - y;
int x, y2;
if (currentSelection != null) {
if ( ++antsOffset >= antsLength ) {
antsOffset = 0;
startColor = !startColor;
}
final int x1 = currentSelection.getStartX(), x2 = currentSelection.getStopX();
repaint(0, x1, y, x2 - x1 + 1, h);
}
}
if (drawPreviewLocator) {
repaint();
previewLocatorAlpha -= 0.05f;
drawPreviewLocator = previewLocatorAlpha > 0.0f;
}
}
}
}, 1000, 150);
ToolTipManager ttmgr = ToolTipManager.sharedInstance();
ttmgr.setInitialDelay(0);
ttmgr.setReshowDelay(0);
ttmgr.setDismissDelay(Integer.MAX_VALUE);
setOpaque(false);
}
public void addIndex(String tableName, String name, long mergeTime) {
IndexBox indexBox = new IndexBox(this, name);
indexBox.setAlignmentX(Component.LEFT_ALIGNMENT);
indexBox.setMergeTime(mergeTime);
indexBoxes.put(tableName, indexBox);
indexPanel.add(indexBox);
}
public void addVerticalGlue() {
indexPanel.add(Box.createVerticalGlue());
}
public void refreshDisplay() {
refreshDisplay(false);
}
/**
* @param force whether it should also adapt its width when it's shrinking
*/
public void refreshDisplay(boolean force) {
int panelw = getViewport().getExtentSize().width;
if ( !zoomStack.empty() ) {
ZoomSpec zoom = zoomStack.peek();
if (panelw > zoom.getPixels() || force) {
zoom.setPixels(panelw);
}
panelw = zoom.getPixels();
headerPanel.scale.setToZoom(zoom);
if(showTagBox) {
headerPanel.tagBox.setToZoom(zoom);
} else {
headerPanel.tagBox.removeAll();
}
for(IndexBox ib:indexBoxes.values()) {
ib.setToZoom(zoom);
}
}
headerPanel.scale.setMaximumSize(new Dimension(panelw, headerPanel.scale.getPreferredSize().height));
headerPanel.scale.setMinimumSize(headerPanel.scale.getMaximumSize());
headerPanel.scale.setPreferredSize(headerPanel.scale.getMaximumSize());
headerPanel.scale.setSize(headerPanel.scale.getMaximumSize());
}
void setPointer(MouseEvent e) {
if (archivePanel.prefs.reloadButton.isEnabled()) {
setNormalPointer();
if (currentSelection != null) {
if (Math.abs(e.getX() - currentSelection.getStartX()) <= cursorSnap) {
setMoveLeftPointer();
}
if (Math.abs(e.getX() - currentSelection.getStopX()) <= cursorSnap) {
setMoveRightPointer();
}
}
}
}
@Override
public Point getToolTipLocation(MouseEvent event) {
return new Point(event.getX() - 94, event.getY() + 20);
}
long getMouseInstant(MouseEvent e) {
if (zoomStack.isEmpty()) {
return TimeEncoding.INVALID_INSTANT;
}
previewLocator = zoomStack.peek().convertPixelToInstant(e.getX());
return previewLocator;
}
void setMouseLabel(MouseEvent e) {
long instant = getMouseInstant(e);
if (instant==TimeEncoding.INVALID_INSTANT) {
setToolTipText(null);
} else {
setToolTipText(TimeEncoding.toCombinedFormat(instant));
}
dataViewer.signalMousePosition(instant);
}
public void zoomIn() {
ZoomSpec zoom = zoomStack.peek();
final JViewport vp = getViewport();
// save current location in current zoom spec
zoom.viewLocation = zoom.convertPixelToInstant(vp.getViewPosition().x);
// create new zoom spec and add it to the stack
long startInstant;
long stopInstant;
if(currentSelection==null) {
// Zoom in on center 3rd of current view extent
int extentWidth=vp.getExtentSize().width;
int nextStartX = vp.getViewPosition().x + (extentWidth/3);
int nextStopX = nextStartX + (extentWidth/3);
startInstant=zoom.convertPixelToInstant(nextStartX);
stopInstant=zoom.convertPixelToInstant(nextStopX);
zoom.setSelectedRange(TimeEncoding.INVALID_INSTANT, TimeEncoding.INVALID_INSTANT);
} else {
startInstant = currentSelection.getStartInstant();
stopInstant = currentSelection.getStopInstant();
zoom.setSelectedRange(startInstant, stopInstant);
}
long range = stopInstant - startInstant;
TimeInterval requestedInterval = archivePanel.getRequestedDataInterval();
long zstart=startInstant - range * 2;
if(requestedInterval.hasStart()) {
zstart=Math.max(zstart, requestedInterval.getStart());
}
long zstop=stopInstant + range * 2;
if(requestedInterval.hasStop()) {
zstop=Math.min(zstop, requestedInterval.getStop());
}
zoom = new ZoomSpec(zstart, zstop, vp.getExtentSize().width, range);
zoom.viewLocation = startInstant;
zoomStack.push(zoom);
resetSelection();
refreshDisplay();
// set the view to the previously selected region
setViewLocationFromZoomstack();
}
void setViewLocationFromZoomstack() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if(zoomStack.isEmpty()) return;
final ZoomSpec currentZoom = zoomStack.peek();
final JViewport vp = getViewport();
int x = (int)((currentZoom.viewLocation - currentZoom.startInstant) / currentZoom.pixelRatio);
vp.setViewPosition(new Point(x, vp.getViewPosition().y));
//debugLog("zoom out, view width " + vp.getView().getSize().width + " location " + x + " = " + currentZoom.viewLocation);
}
});
}
public void archiveLoadFinished() {
for(IndexBox ib:indexBoxes.values()) {
ib.dataLoadFinished();
}
if (zoomStack.isEmpty() ||
((archivePanel.prefs.getStartTimestamp() != lastStartTimestamp) ||
(archivePanel.prefs.getEndTimestamp() != lastEndTimestamp)
)) {
int w = getViewport().getExtentSize().width;
zoomStack.clear();
TimeInterval receivedDataInterval = archivePanel.getReceivedDataInterval();
TimeInterval requestedDataInterval = archivePanel.getRequestedDataInterval();
long zstart = receivedDataInterval.getStart();
if(requestedDataInterval.hasStart()) {
zstart=Math.min(requestedDataInterval.getStart(), zstart);
}
long zstop = receivedDataInterval.getStop();
if(requestedDataInterval.hasStop()) {
zstop=Math.max(requestedDataInterval.getStop(), zstop);
}
long range=zstop - zstart;
zstart-=range/100;
zstop+=range/100;
zoomStack.push(new ZoomSpec(zstart, zstop, w, zstop - zstart));
}
lastStartTimestamp = archivePanel.prefs.getStartTimestamp();
lastEndTimestamp = archivePanel.prefs.getEndTimestamp();
SwingUtilities.invokeLater(() -> {
//debugLog("receiveHrdpRecords() mark 1");
dataViewer.zoomInButton.setEnabled(true);
dataViewer.zoomOutButton.setEnabled(false);
dataViewer.showAllButton.setEnabled(true);
refreshDisplay();
});
}
void emitActionEvent(String cmd) {
ActionEvent ae=new ActionEvent(this, ActionEvent.ACTION_PERFORMED, cmd);
for(ActionListener al:actionListeners) {
al.actionPerformed(ae);
}
}
void emitActionEvent(ActionEvent ae) {
for(ActionListener al:actionListeners) {
al.actionPerformed(ae);
}
}
void showAll() {
while (zoomStack.size() > 1) {
zoomStack.pop();
}
resetSelection();
refreshDisplay(true);
setViewLocationFromZoomstack();
}
void zoomOut() {
if (zoomStack.size() > 1) {
zoomStack.pop();
ZoomSpec zoom = zoomStack.peek();
if(zoom.selectionStart!=TimeEncoding.INVALID_INSTANT && zoom.selectionStop!=TimeEncoding.INVALID_INSTANT) {
// Restore selection as it was made before zoom in (to make it easier to go back and forth between zoom in/out)
updateSelection(zoom.selectionStart, zoom.selectionStop);
dataViewer.signalSelectionChange(currentSelection);
}
refreshDisplay();
// place the view where it was
setViewLocationFromZoomstack();
}
}
public void doMouseDragged(MouseEvent e) {
indexPanel.doMouseDragged(e);
// TTM does not show the tooltip in mouseDragged() so we send a MOUSE_MOVED event
dispatchEvent(new MouseEvent(e.getComponent(), MouseEvent.MOUSE_MOVED, e.getWhen(), e.getModifiers(),
e.getX(), e.getY(), e.getClickCount(), e.isPopupTrigger(), e.getButton()));
}
public void doMouseMoved(MouseEvent e) {
headerPanel.doMouseMoved(e);
setMouseLabel(e);
setPointer(e);
}
public void doMousePressed(MouseEvent e) {
indexPanel.doMousePressed(e);
}
public void doMouseReleased(MouseEvent e) {
indexPanel.doMouseReleased(e);
}
public void doMouseExited(MouseEvent e) {
dataViewer.signalMousePosition(TimeEncoding.INVALID_INSTANT);
mouseLocatorX = -1;
repaint(); // Force removal of needle in paint()
}
public void updateSelection(Long selectionStart, Long selectionStop) {
if(!archivePanel.passiveUpdate) {
if ((selectionStart!=null) && (selectionStop!=null)) {
long start = selectionStart;
long stop = selectionStop;
if ((start != TimeEncoding.INVALID_INSTANT) && (stop != TimeEncoding.INVALID_INSTANT)) {
updateSelection(start, stop);
emitActionEvent("histo_selection_finished");
}
}
}
}
public void selectionFinished() {
for(String name : indexBoxes.keySet()) {
emitActionEvent(name+"_selection_finished");
}
}
/**
* Called after the mouse dragging selection is updated on the boxes to update the selectionStart/Stop fields
* We use the passiveUpdate to avoid a ping pong effect
*/
void updateSelectionFields() {
archivePanel.passiveUpdate=true;
dataViewer.signalSelectionChange(currentSelection);
archivePanel.passiveUpdate=false;
}
public List<String> getSelectedPackets(String type) {
return indexBoxes.get(type).getPacketsForSelection(getSelection());
}
/**called from the tagBox when a tag is selected. Update tmBox selection to this*/
public void selectedTag(ArchiveTag tag) {
archivePanel.passiveUpdate=true;
if(tag.hasStart()) {
dataViewer.signalSelectionStartChange(tag.getStart());
} else {
dataViewer.signalSelectionStartChange(archivePanel.getReceivedDataInterval().getStart());
}
archivePanel.passiveUpdate=false;
if(tag.hasStop()) {
dataViewer.signalSelectionStopChange(tag.getStop());
} else {
dataViewer.signalSelectionStopChange(archivePanel.getReceivedDataInterval().getStart());
}
}
public void setMoveLeftPointer() {
setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
}
public void setMoveRightPointer() {
setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
}
public void setBusyPointer() {
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
}
public void setNormalPointer() {
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
void setStartLocator(long position) {
startLocator = position;
}
void setStopLocator(long position) {
stopLocator = position;
}
void setCurrentLocator(long position) {
currentLocator = position;
repaint();
}
public void addActionListener(ActionListener al) {
actionListeners.add(al);
}
public void resetSelection() {
startX = -1;
currentSelection = null;
dataViewer.signalSelectionChange(null);
repaint();
emitActionEvent("selection_reset");
}
public SelectionImpl getSelection() {
return currentSelection;
}
void updateSelection(long start, long stop) {
if (currentSelection == null) {
currentSelection = new SelectionImpl(start, stop);
} else {
currentSelection.set(start, stop);
}
// set last mouse x/y coords
startX = currentSelection.getStartX();
stopX = currentSelection.getStopX();
repaint();
}
class SelectionImpl implements Selection {
long start, stop;
SelectionImpl(int x1, int x2) {
set(x1, x2);
}
SelectionImpl(long start1, long stop1) {
set(start1, stop1);
}
@Override
public long getStartInstant() {
return start;
}
@Override
public long getStopInstant() {
return stop;
}
int getStartX() {
final ZoomSpec zoom = zoomStack.peek();
return zoom.convertInstantToPixel(start);
}
int getStopX() {
final ZoomSpec zoom = zoomStack.peek();
return zoom.convertInstantToPixel(stop);
}
public void set(long start1, long stop1) {
if (start1>stop1) {
start = stop1;
stop = start1;
} else {
start = start1;
stop = stop1;
}
}
public void set(int x1, int x2) {
if(x1>x2) {
int xt=x1;
x1=x2;
x2=xt;
}
final ZoomSpec zoom = zoomStack.peek();
start = zoom.convertPixelToInstant(x1);
stop = zoom.convertPixelToInstant(x2);
}
}
public class HeaderPanel extends Box {
TagBox tagBox;
TMScale scale;
public HeaderPanel() {
super(BoxLayout.Y_AXIS);
setBorder(BorderFactory.createEmptyBorder());
setOpaque(false);
tagBox=new TagBox(DataView.this);
tagBox.setAlignmentX(Component.LEFT_ALIGNMENT);
add(tagBox);
scale = new TMScale();
scale.setAlignmentX(Component.LEFT_ALIGNMENT);
add(scale);
}
public void doMouseMoved(MouseEvent e) {
if(zoomStack.isEmpty()) return;
mouseLocatorX = e.getX();
repaint();
}
@Override
public void paint(Graphics g) {
super.paint(g);
if(mouseLocatorX > 0) {
// follow mouse for better timeline positioning
g.setColor(Color.DARK_GRAY);
int tagBoxHeight = tagBox.getHeight();
g.drawLine(mouseLocatorX, tagBoxHeight, mouseLocatorX, getHeight());
}
}
}
public class IndexPanel extends Box {
public IndexPanel() {
super(BoxLayout.Y_AXIS);
setBorder(BorderFactory.createEmptyBorder());
setOpaque(false);
}
public void doMousePressed(MouseEvent e) {
if(zoomStack.isEmpty()) return;
dragButton = e.getButton();
if (dragButton == MouseEvent.BUTTON1) {
if (dataViewer.replayEnabled && (e.getClickCount() == 2)) {
drawPreviewLocator = true;
previewLocatorAlpha = 0.8f;
previewLocatorX = e.getX();
archivePanel.seekReplay(previewLocator);
repaint();
} else {
if ((currentSelection != null) && (Math.abs(e.getX() - currentSelection.getStartX()) <= cursorSnap)) {
deltaX = e.getX() - currentSelection.getStartX();
startX = currentSelection.getStopX();
doMouseDragged(e);
} else if ((currentSelection != null) && (Math.abs(e.getX() - currentSelection.getStopX()) <= cursorSnap)) {
deltaX = e.getX() - currentSelection.getStopX();
startX = currentSelection.getStartX();
doMouseDragged(e);
} else {
resetSelection();
doMouseDragged(e);
}
}
}
}
public void doMouseReleased(MouseEvent e) {
if (!zoomStack.isEmpty()) {
if (e.getButton() == MouseEvent.BUTTON1) {
if (currentSelection != null) {
// if only one line was selected, the user wants to deselect
if (startX == stopX) {
resetSelection();
updateSelectionFields();
} else {
// selection finished
selectionFinished();
}
}
}
}
}
void doMouseDragged(MouseEvent e) {
if (!zoomStack.isEmpty()) {
if (dragButton == MouseEvent.BUTTON1) {
//final JViewport vp = tmscrollpane.getViewport();
//stopX = Math.max(e.getX(), vp.getViewPosition().x);
//stopX = Math.min(stopX, vp.getViewPosition().x + vp.getExtentSize().width - 1);
stopX = e.getX() - deltaX;
if (startX == -1) startX = stopX;
if (currentSelection == null) {
currentSelection = new SelectionImpl(startX, stopX);
} else {
currentSelection.set(startX, stopX);
}
setMouseLabel(e);
setPointer(e);
repaint();
// this will trigger an update of selection start/stop text fields
updateSelectionFields();
}
}
}
@Override
public void paint(Graphics g) {
super.paint(g);
// draw the selection rectangle over all the TM panels
if ( (getComponentCount() > 0) && !zoomStack.isEmpty() ) {
ZoomSpec zoom = zoomStack.peek();
final int h = getHeight(); ///
int x, y2;
if (currentSelection != null) {
final int offset = antsOffset - antsLength;
final int x1 = currentSelection.getStartX(), x2 = currentSelection.getStopX();
int y1, yend = h;
boolean color = startColor;
g.setColor(new Color(224, 234, 242, 64));
g.fillRect(x1, 0, x2 - x1 + 1, h);
for ( y1 = offset; y1 < yend; y1 += antsLength ) {
y2 = y1 + (antsLength - 1);
if ( y2 >= yend ) y2 = yend - 1;
g.setColor((color = !color) ? Color.BLACK : Color.WHITE);
g.drawLine(x1, y1, x1, y2);
g.drawLine(x2, y1, x2, y2);
}
}
if ((startLocator != DO_NOT_DRAW) || (stopLocator != DO_NOT_DRAW) || (currentLocator != DO_NOT_DRAW) ||
drawPreviewLocator) {
int xmax = getSize().width - handleWidth;
if ( startLocator != DO_NOT_DRAW ) {
x = zoom.convertInstantToPixel(startLocator);
//debugLog("startLocator (" + x + "," + y + ") box width " + getSize().width);
if ( (x >= 0) && (x < xmax) ) {
final int[] px = { x, x + handleWidth, x };
final int[] py = { handleHeight, handleHeight / 2, 0 };
g.setColor(handleFill);
g.fillPolygon(px, py, px.length);
g.setColor(Color.BLACK);
g.drawPolygon(px, py, px.length);
g.drawLine(x, 0, x, h - 1);
}
}
if ( stopLocator != DO_NOT_DRAW ) {
x = zoom.convertInstantToPixel(stopLocator);
//debugLog("stopLocator (" + x + "," + y + ") box width " + getSize().width);
if ( (x >= 0) && (x < xmax) ) {
final int[] px = { x, x - handleWidth, x };
final int[] py = { handleHeight, handleHeight / 2, 0 };
g.setColor(handleFill);
g.fillPolygon(px, py, px.length);
g.setColor(Color.BLACK);
g.drawPolygon(px, py, px.length);
g.drawLine(x, 0, x, 0 + h - 1);
}
}
// draw the current position
if ( currentLocator != DO_NOT_DRAW ) {
x = zoom.convertInstantToPixel(currentLocator);
//debugLog("currentLocator (" + x + "," + y + ") box width " + getSize().width);
if ( (x >= 0) && (x < xmax) ) {
final int[] px = { x - handleWidth2 / 2, x, x + handleWidth2 / 2 };
final int[] py = { handleHeight, 0, handleHeight };
g.setColor(currentFill);
g.fillPolygon(px, py, px.length);
g.setColor(Color.BLACK);
g.drawPolygon(px, py, px.length);
g.drawLine(x, handleHeight, x, h - 1);
}
}
// draw the preview replay position line
if ( drawPreviewLocator ) {
final int[] px = { previewLocatorX - handleWidth2 / 2, previewLocatorX, previewLocatorX + handleWidth2 / 2 };
final int[] py = { handleHeight, 0, handleHeight };
float[] c = currentFill.getRGBColorComponents(null);
g.setColor(new Color(c[0], c[1], c[2], previewLocatorAlpha));
g.fillPolygon(px, py, px.length);
c = Color.BLACK.getRGBColorComponents(c);
g.setColor(new Color(c[0], c[1], c[2], previewLocatorAlpha));
g.drawPolygon(px, py, px.length);
g.drawLine(previewLocatorX, handleHeight, previewLocatorX, h - 1);
}
}
}
}
}
}