package ru.batrdmi.svnplugin;
import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.view.mxGraph;
import com.mxgraph.view.mxStylesheet;
import ru.batrdmi.svnplugin.logic.FileRevisionHistory;
import ru.batrdmi.svnplugin.logic.Revision;
import javax.swing.*;
import java.awt.*;
import java.util.*;
import java.util.List;
import java.util.Queue;
public class RevisionGraphModel extends mxGraph {
private List<Revision> revisions;
private Revision currentRevision;
private LinkedHashMap<String, ArrayList<Revision>> revMap;
private HashMap<Revision, mxCell> cellMap;
private HashMap<Revision, Integer> revisionOrder;
private HashMap<Revision, HashSet<Revision>> linkedRevisions;
public RevisionGraphModel(FileRevisionHistory revisionHistory, boolean collapseRevisions, boolean onlyImpactingRevisions) {
setCellsMovable(false);
setCellsEditable(false);
setCellsResizable(false);
setDropEnabled(false);
if (onlyImpactingRevisions) {
revisionHistory = retainOnlyImpacting(revisionHistory);
}
processHistory(revisionHistory);
getModel().beginUpdate();
createGraph(collapseRevisions);
layoutGraph();
getModel().endUpdate();
}
private FileRevisionHistory retainOnlyImpacting(FileRevisionHistory history) {
class RevInfo {
final Revision thisRev, prevRev;
RevInfo(Revision thisRev, Revision prevRev) {
this.thisRev = thisRev;
this.prevRev = prevRev;
}
}
// preprocess data
List<Revision> sortedRevisions = new ArrayList<Revision>(history.getAllRevisions());
Collections.sort(sortedRevisions, new Revision.RevisionNumberComparator());
Map<String, Revision> pathHeads = new HashMap<String, Revision>();
Map<Revision, RevInfo> preprocessedData = new HashMap<Revision, RevInfo>();
for (Revision r : sortedRevisions) {
preprocessedData.put(r, new RevInfo(r, pathHeads.get(r.getRelPath())));
pathHeads.put(r.getRelPath(), r.isDeleted() ? null : r);
}
// walk the revision graph and collect impacting revisions
Set<Revision> result = new HashSet<Revision>();
Queue<Revision> revsToProcess = new LinkedList<Revision>();
revsToProcess.add(history.getCurrentRevision());
while (!revsToProcess.isEmpty()) {
RevInfo ri = preprocessedData.get(revsToProcess.remove());
if (ri != null && !result.contains(ri.thisRev)) {
result.add(ri.thisRev);
if (!ri.thisRev.getCopiedOrMergedRevisions().isEmpty()) {
revsToProcess.add(ri.thisRev.getCopiedOrMergedRevisions().get(0));
}
if (ri.prevRev != null) {
revsToProcess.add(ri.prevRev);
}
}
}
return new FileRevisionHistory(result, history.getCurrentRevision(),
history.getRepoRoot(), history.getRelPath(), history.isDirectory(), history.getStatus());
}
private void processHistory(FileRevisionHistory revisionHistory) {
revMap = new LinkedHashMap<String, ArrayList<Revision>>();
currentRevision = revisionHistory.getCurrentRevision();
revisions = new ArrayList<Revision>(revisionHistory.getAllRevisions());
revisionOrder = new HashMap<Revision, Integer>();
linkedRevisions = new HashMap<Revision, HashSet<Revision>>();
// Sort revisions based on revision dates
Collections.sort(revisions, new Revision.RevisionNumberComparator());
int i = 0;
for (Revision rev : revisions) {
revisionOrder.put(rev, i++);
String path = rev.getRelPath();
ArrayList<Revision> revsOnPath = revMap.get(path);
if (revsOnPath == null) {
revsOnPath = new ArrayList<Revision>();
revMap.put(path, revsOnPath);
}
revsOnPath.add(rev);
// find linked revisions
for (Revision lr : revisions) {
if (rev.getRelPath().equals(lr.getLinkedRelPath()) && rev.getRevisionNumber() == lr.getLinkedRevisionNumber()
|| lr.getRelPath().equals(rev.getLinkedRelPath()) && lr.getRevisionNumber() == rev.getLinkedRevisionNumber()) {
HashSet<Revision> s = linkedRevisions.get(rev);
if (s == null) {
s = new HashSet<Revision>();
linkedRevisions.put(rev, s);
}
s.add(lr);
}
}
}
}
private void createGraph(boolean collapseRevisions) {
cellMap = new HashMap<Revision, mxCell>();
for (String path : revMap.keySet()) {
mxCell lastCell = null;
ArrayList<Revision> revsOnPath = revMap.get(path);
for (Revision rev : revsOnPath) {
lastCell = createCell(lastCell, rev, collapseRevisions);
}
}
// create merge/copy links
for (mxCell cell : cellMap.values()) {
List<Revision> revs = ((Node) cell.getValue()).revisions;
if (revs.size() == 1) {
mxCell mergeFromCell = getLinkedCell(revs.get(0));
if (mergeFromCell != null) {
createEdge(mergeFromCell, cell, true);
}
}
}
// move all vertices to foreground
orderCells(false, new HashSet<mxCell>(cellMap.values()).toArray());
}
private mxCell createCell(mxCell neighborCell, Revision rev, boolean collapseRevisions) {
mxCell result;
List<Revision> prevList = (neighborCell == null) ? null : ((Node)neighborCell.getValue()).revisions;
Revision prevRev = (prevList == null) ? null : prevList.get(prevList.size() - 1);
int thisOrder = revisionOrder.get(rev);
int prevOrder = (prevRev == null) ? -100 : revisionOrder.get(prevRev);
if (collapseRevisions && prevRev != currentRevision && rev != currentRevision
&& prevRev != null && !linkedRevisions.containsKey(prevRev) && !prevRev.isDeleted()
&& !linkedRevisions.containsKey(rev) && !rev.isDeleted() && (thisOrder - prevOrder) == 1
&& (prevOrder == 0 || revisions.get(prevOrder - 1).getRevisionNumber() < prevRev.getRevisionNumber())
&& (thisOrder == (revisions.size() - 1) || revisions.get(thisOrder + 1).getRevisionNumber() > rev.getRevisionNumber())) {
prevList.add(rev);
result = neighborCell;
} else {
List<Revision> nodeRevisions = new ArrayList<Revision>();
nodeRevisions.add(rev);
result = new mxCell(new Node(nodeRevisions), new mxGeometry(), null);
result.setVertex(true);
result.setConnectable(true);
result.setStyle(SVNRevisionGraph.REVISION_STYLE + ";"
+ mxConstants.STYLE_STROKEWIDTH + "=" + ((rev == currentRevision) ? "3" : "1") + ";"
+ mxConstants.STYLE_DASHED + "=" + rev.isDeleted());
addCell(result);
if (prevRev != null && !prevRev.isDeleted()) {
createEdge(neighborCell, result, false);
}
}
cellMap.put(rev, result);
return result;
}
private void createEdge(mxCell source, mxCell target, boolean arrow) {
insertEdge(null, null, new Node(null), source, target,
arrow ? SVNRevisionGraph.COPY_OR_MERGE_LINK_STYLE : SVNRevisionGraph.IN_BRANCH_LINK_STYLE);
}
private void layoutGraph() {
mxCell lastCell = null;
Double nextY = null;
int i = 0;
for (String path : revMap.keySet()) {
ArrayList<Revision> revsOnPath = revMap.get(path);
if (i != 0) {
Revision firstRev = revsOnPath.get(0);
lastCell = getLinkedCell(firstRev);
}
int relativePosition = SwingConstants.BOTTOM;
double widestY = 0;
for (Revision rev : revsOnPath) {
mxCell cell = cellMap.get(rev);
if (cell != lastCell) {
lastCell = positionCell(lastCell, relativePosition, nextY, cell);
relativePosition = SwingConstants.RIGHT;
mxRectangle rect = lastCell.getGeometry();
widestY = Math.max(widestY, rect.getY() + rect.getHeight());
}
}
nextY = widestY + SVNRevisionGraph.CELL_SPACING;
i++;
}
//Now adjust x position in order of revision numbers
for (i = 1; i < revisions.size(); i++) {
Revision rev = revisions.get(i);
Revision prevRev = revisions.get(i - 1);
mxCell cell = cellMap.get(rev);
mxCell prevCell = cellMap.get(prevRev);
if (cell != prevCell) {
Rectangle rect = cell.getGeometry().getRectangle();
positionCell(prevCell,
rev.getRevisionNumber() > prevRev.getRevisionNumber() ? SwingConstants.RIGHT : SwingConstants.BOTTOM,
rect.getY(), cell);
}
}
}
private mxCell positionCell(mxCell neighborCell, int relativePosition, Double preferredY, mxCell targetCell) {
//Calculate position from neighbor
mxRectangle neighborRect = null;
if (neighborCell == null) {
if (relativePosition == SwingConstants.BOTTOM)
neighborRect = new mxRectangle(SVNRevisionGraph.CELL_SPACING, 0, 0, 0);
else if (relativePosition == SwingConstants.RIGHT)
neighborRect = new mxRectangle(0, SVNRevisionGraph.CELL_SPACING, 0, 0);
} else {
neighborRect = neighborCell.getGeometry();
}
double x = 0, y = 0;
if (relativePosition == SwingConstants.BOTTOM) {
x = neighborRect.getX();
y = neighborRect.getY() + neighborRect.getHeight() + SVNRevisionGraph.CELL_SPACING;
} else if (relativePosition == SwingConstants.RIGHT) {
x = neighborRect.getX() + neighborRect.getWidth() + SVNRevisionGraph.CELL_SPACING;
y = neighborRect.getY();
}
if (preferredY != null) {
y = preferredY;
}
mxRectangle preferredSize = getPreferredSizeForCell(targetCell);
neighborRect = new mxRectangle(x, y, preferredSize.getWidth(), preferredSize.getHeight());
resizeCell(targetCell, neighborRect);
return targetCell;
}
private mxCell getLinkedCell(Revision rev) {
if (rev.getLinkedRelPath() == null) {
return null;
} else {
Revision mergeRev = new Revision(rev.getLinkedRelPath(), rev.getLinkedRevisionNumber());
return cellMap.get(mergeRev);
}
}
public Revision getCurrentRevision() {
return currentRevision;
}
public mxCell getCellForRevision(Revision revision) {
return cellMap.get(revision);
}
public Set<String> getAllPaths() {
return revMap.keySet();
}
public List<Revision> getRevisionsForPath(String path) {
return revMap.get(path);
}
public Set<Revision> getLinkedRevisions(Revision r) {
Set<Revision> result = linkedRevisions.get(r);
return (result == null) ? Collections.<Revision>emptySet() : result;
}
public int getNodeHeight() {
if (cellMap.isEmpty()) {
return 0;
} else {
return (int) cellMap.values().iterator().next().getGeometry().getHeight();
}
}
public List<Node> getSelectedNodes() {
List<Node> nodes = new ArrayList<Node>();
Object[] cells = getSelectionCells();
if (cells != null) {
for (Object cell : cells) {
mxCell c = (mxCell) cell;
if (c.isVertex()) {
nodes.add((RevisionGraphModel.Node) c.getValue());
}
}
}
return nodes;
}
public static class Node {
public final List<Revision> revisions;
public final Map<String, Object> properties = new HashMap<String, Object>();
public Node(List<Revision> revisions) {
this.revisions = revisions;
}
}
@Override
public String getLabel(Object o) {
List<Revision> revisions = ((Node) ((mxCell) o).getValue()).revisions;
if (revisions == null || revisions.isEmpty()) {
return null;
} else if (revisions.size() == 1) {
return Long.toString(revisions.get(0).getRevisionNumber());
} else {
return Long.toString(revisions.get(0).getRevisionNumber()) + "..."
+ Long.toString(revisions.get(revisions.size() - 1).getRevisionNumber());
}
}
@Override
public String getToolTipForCell(Object o) {
List<Revision> revisions = ((Node) ((mxCell) o).getValue()).revisions;
if (revisions == null || revisions.isEmpty()) {
return null;
} else if (revisions.size() == 1) {
return revisions.get(0).getMessage();
} else {
return revisions.size() + " revisions";
}
}
@Override
protected mxStylesheet createStylesheet() {
mxStylesheet s = new mxStylesheet();
Map<String, Object> style = new HashMap<String, Object>();
style.put(mxConstants.STYLE_SHAPE, SVNRevisionGraph.LINK_SHAPE);
style.put(mxConstants.STYLE_ENDARROW, "no arrow");
s.putCellStyle(SVNRevisionGraph.IN_BRANCH_LINK_STYLE, new HashMap<String, Object>(style));
style.put(mxConstants.STYLE_SHAPE, SVNRevisionGraph.LINK_SHAPE);
style.put(mxConstants.STYLE_ENDARROW, mxConstants.ARROW_CLASSIC);
s.putCellStyle(SVNRevisionGraph.COPY_OR_MERGE_LINK_STYLE, new HashMap<String, Object>(style));
style.clear();
style.put(mxConstants.STYLE_SHAPE, SVNRevisionGraph.REVISION_SHAPE);
style.put(mxConstants.STYLE_FONTCOLOR, "black");
style.put(mxConstants.STYLE_SPACING, 1);
style.put(mxConstants.STYLE_SPACING_TOP, 2);
style.put(mxConstants.STYLE_SPACING_LEFT, 1);
s.putCellStyle(SVNRevisionGraph.REVISION_STYLE, new HashMap<String, Object>(style));
return s;
}
}