| /*========================================================================= |
| * 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); |
| } |
| } |
| } |
| } |