blob: 583f570d94454ff5885add1e046cbb27b23c2e31 [file] [log] [blame]
/*=========================================================================
* Copyright (c) 2010-2014 Pivotal Software, Inc. All Rights Reserved.
* This product is protected by U.S. and international copyright
* and intellectual property laws. Pivotal products are covered by
* one or more patents listed at http://www.pivotal.io/patents.
*=========================================================================
*/
package com.gemstone.sequence;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by IntelliJ IDEA.
* User: dsmith
* Date: Oct 29, 2010
* Time: 4:18:40 PM
* To change this template use File | Settings | File Templates.
*/
public class SequenceDiagram extends JPanel {
/**
*
*/
private static final Color HIGHLIGHT_COLOR = Color.RED;
private final List<String> lineNames;
private final Map<String, List<String>> shortLineNames;
private final SortedMap<Comparable, SubDiagram> subDiagrams = new TreeMap<Comparable, SubDiagram>();
private final StateColorMap colorMap = new StateColorMap();
private final long minTime;
private final long maxTime;
private static int PADDING_BETWEEN_LINES = 100;
private static int Y_PADDING = 20;
private static final int STATE_WIDTH = 20;
private static final int LINE_LABEL_BOUNDARY = 5;
private static final int AXIS_SIZE=35;
private int lineStep;
private int lineWidth;
private Popup mouseover;
private LifelineState mouseoverState;
private LifelineState selectedState;
public SequenceDiagram(long minTime, long maxTime, List<String> lineNames, LineMapper lineMapper) {
this.lineNames = lineNames;
this.shortLineNames = parseShortNames(lineNames, lineMapper);
this.minTime = minTime;
this.maxTime = maxTime;
int width = getInitialWidth();
int height = 500;
super.setPreferredSize(new Dimension(width, height));
resizeMe(width, height);
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
Component source = (Component) e.getSource();
resizeMe(source.getWidth(), source.getHeight());
}
});
setBackground(Color.WHITE);
}
private Map<String, List<String>> parseShortNames(List<String> lineNames, LineMapper lineMapper) {
Map<String, List<String>> shortNames = new LinkedHashMap<String, List<String>>(lineNames.size());
for(String name : lineNames) {
String shortName = lineMapper.getShortNameForLine(name);
List<String> list = shortNames.get(shortName);
if(list == null) {
list = new ArrayList<String>();
shortNames.put(shortName, list);
}
list.add(name);
}
return shortNames;
}
public List<Comparable> getSubDiagramsNames() {
return new ArrayList<Comparable>(subDiagrams.keySet());
}
public void addSubDiagram(Comparable name, Map<String, Lifeline> lines, List<Arrow> arrows) {
subDiagrams.put(name, new SubDiagram(name, lines, arrows));
}
public void removeSubDiagram(Comparable name) {
this.subDiagrams.remove(name);
}
private int getInitialWidth() {
return (this.shortLineNames.size()) * PADDING_BETWEEN_LINES;
}
public void resizeMe(int width, int height) {
this.setPreferredSize(new Dimension(width, height));
float xZoom = width / (float) getInitialWidth();
long elapsedTime = maxTime - minTime;
double yScale = height / (double) elapsedTime;
// long yBase = (long) (minTime - ((long) Y_PADDING / yScale));
long yBase = minTime;
lineStep = (int) (PADDING_BETWEEN_LINES * xZoom);
lineWidth = (int) (STATE_WIDTH * xZoom);
if(subDiagrams.size() <= 0) {
return;
}
int sublineWidth = lineWidth / subDiagrams.size();
int sublineIndex = 0;
for(SubDiagram diagram : subDiagrams.values()) {
int lineIndex = 0;
for(List<String> fullNames : shortLineNames.values()) {
for(String name : fullNames) {
Lifeline line = diagram.lifelines.get(name);
if(line != null) {
int lineX = lineIndex * lineStep + sublineIndex * sublineWidth;
line.resize(lineX, sublineWidth, yBase, yScale);
}
}
lineIndex++;
}
sublineIndex++;
}
}
public void showPopupText(int x, int y, int xOnScreen, int yOnScreen) {
LifelineState state = getStateAt(x, y);
if(state == mouseoverState) {
return;
}
if(mouseover != null) {
mouseover.hide();
}
if(state == null) {
mouseover = null;
mouseoverState = null;
} else {
Component popupContents = state.getPopup();
mouseoverState = state;
mouseover = PopupFactory.getSharedInstance().getPopup(this, popupContents, xOnScreen + 20, yOnScreen);
mouseover.show();
}
}
public void selectState(int x, int y) {
LifelineState state = getStateAt(x, y);
if(state == selectedState) {
return;
}
if(selectedState != null) {
fireRepaintOfDependencies(selectedState);
}
selectedState = state;
if(state != null) {
fireRepaintOfDependencies(state);
}
}
private LifelineState getStateAt(int x, int y) {
//TODO - this needs some
//serious optimization to go straight to the right state
// I think we need a tree map of of lines, keyed by x offset
//and a keymap of states keyed by y offset.
//That could make painting faster as well.
List<String> reverseList = new ArrayList<String>(lineNames);
Collections.reverse(reverseList);
for(SubDiagram diagram : subDiagrams.values()) {
for(String name : reverseList) {
Lifeline line = diagram.lifelines.get(name);
if(line != null) {
if(line.getX() < x && line.getX() + line.getWidth() > x) {
LifelineState state = line.getStateAt(y);
if(state != null) {
return state;
}
}
}
}
}
return null;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
//TODO - we should clip these to the visible lines
for(SubDiagram subDiagram : subDiagrams.values()) {
subDiagram.paintStates(g2, colorMap);
}
for(SubDiagram subDiagram : subDiagrams.values()) {
subDiagram.paintArrows(g2, colorMap);
}
paintHighlightedComponents(g2, selectedState, new HashSet<LifelineState>());
}
private void fireRepaintOfDependencies(LifelineState state) {
//TODO - it might be more efficient to repaint just the changed
//areas, but right now this will do.
repaint();
}
private void paintHighlightedComponents(Graphics2D g2, LifelineState endingState, Set<LifelineState> visited) {
if(!visited.add(endingState)) {
//Prevent cycles
return;
}
g2.setColor(HIGHLIGHT_COLOR);
if(endingState != null) {
endingState.highlight(g2);
for(Arrow arrow : endingState.getInboundArrows()) {
arrow.paint(g2);
paintHighlightedComponents(g2, arrow.getStartingState(), visited);
}
}
}
public long getMinTime() {
return minTime;
}
public long getMaxTime() {
return maxTime;
}
public JComponent createMemberAxis() {
return new MemberAxis();
}
private class MemberAxis extends JComponent {
public MemberAxis() {
setPreferredSize(new Dimension(getWidth(), AXIS_SIZE));
SequenceDiagram.this.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
int newWidth = e.getComponent().getWidth();
setPreferredSize(new Dimension(newWidth, AXIS_SIZE));
revalidate();
}
});
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Rectangle bounds = g.getClipBounds();
int index = 0;
for(String name : shortLineNames.keySet()) {
int nameWidth = g.getFontMetrics().stringWidth(name);
int lineX = lineStep * index;
index++;
if(bounds.getMaxX() < lineX
|| bounds.getMinX() > lineX + nameWidth) {
continue;
}
g.setClip(lineX + LINE_LABEL_BOUNDARY, 0, lineStep - + LINE_LABEL_BOUNDARY * 2, getHeight());
g.drawString(name, lineX + LINE_LABEL_BOUNDARY, AXIS_SIZE / 3);
g.setClip(null);
}
}
}
public static class SubDiagram {
private final Map<String, Lifeline> lifelines;
private final List<Arrow> arrows;
private final Comparable name;
public SubDiagram(Comparable name, Map<String, Lifeline> lines, List<Arrow> arrows) {
this.name = name;
this.lifelines = lines;
this.arrows = arrows;
}
public void paintStates(Graphics2D g2, StateColorMap colorMap) {
for(Lifeline line: lifelines.values()) {
line.paint(g2, colorMap);
}
}
public void paintArrows(Graphics2D g2, StateColorMap colorMap) {
Color lineColor = colorMap.getColor(name);
g2.setColor(lineColor);
for(Arrow arrow: arrows) {
arrow.paint(g2);
}
}
}
}