Skip to content

Instantly share code, notes, and snippets.

@steveroush
Last active July 17, 2024 21:38
Show Gist options
  • Save steveroush/eea511daa2eecfd8709127f4bff12315 to your computer and use it in GitHub Desktop.
Save steveroush/eea511daa2eecfd8709127f4bff12315 to your computer and use it in GitHub Desktop.
a GVPR program that adds a swimlane feature to Graphviz (dot)
/*************************************************************************
swimlanes
- for more info on swim lanes: https://en.wikipedia.org/wiki/Swim_lane
- new attributes, all at the parent cluster-level:
- swimlanePool=true/false/[0-9]+ - turns on swimlane algorithm
- if having nested swimlanes is desirable, true/false is
insufficient. Instead, use swimlane=someuniquename
- swimlaneequalSize=true/false all lanes given same width, or width determined "naturally" by dot
- swimlanePos = graphname:(n|s|e|w|before|after|post|pre) - where to position a non-swimlane cluster
Future attribute:
- swimlaneInputSequence=true/false - lanes ordered "naturally" by dot, or as determined by the inputfile
- lane2laneMargin - assigned at pool-level
- pool2laneMargin - assigned at pool-level
- pool2poolMargin - assigned to Root or cluster
usage:
dot myFile.gv | gvpr -cf swimlane.gvpr | neato -n -Tpng ...
- implementation:
- each lane is represented by a cluster
- all lanes are contained by a (parent) cluster
- all clusters directly contained by the parent cluster will be treated as lanes
- all nodes within the parent cluster must be contained by a lane/cluster (no "free" nodes)
- algorithm (assumes rankdir=TB. adjust as necessary):
- run std Graphviz algorithm on clusters within the parent
- adjust cluster/lane heights to make all equal (at this stage clusters would overlap wildly)
- adjust cluster/lane positions (centers) so clusters no longer overlap (increase X)
- either based on input sequence (if so desired by user)
- or based on Graphviz starting position for the clusters - order based on left-to-right, top-to-bottom
- consider node and cluster positions nailed
- continue positioning all nodes and clusters outside the swimlane parent as normal
To do:
- lots of tests
- allow clusters & nodes OUTSIDE of pools
- what dot attributes do NOT apply?
- what happens with other engines (neato, fdp, ...) (not dot)?
answer: ?? (fdp & neato kind-of do clusters)
- document new attributes
- add "margin" attribute for between lanes
- add "margin" attribute for between pools
- make functions as general purpose as possible (e.g. moveClusters
- look for a way to support reusing names from one pool to the next
as in two alternatives, with identcal names
- add "rankdir" code to support horizontal swim lanes
*************************************************************************/
BEGIN {
float laneWidth[], laneHeight[];
float poolMinX, poolMinY, poolMaxX, poolMaxY, maxWidth, maxHeight;
float startingX, startingY, labelMargin, topMargin[], bottomMargin[];
float lane2nodeMarginX, lane2nodeMarginY, tmpMarginX, tmpMarginY;
float num;
int i, n, TBranked, equalSize, dotSequence, inputSequence, poolCnt, p2pMargin;
int firstPoolMinMax, poolIndx, laneSeqNo=-1;
int tmpO, fileNumber=10000;
int isPool[], isParent[], isCluster[]; //, poolList[];
int outsideCluster[graph_t]; // array of clusters NOT in pool(s)
graph_t Root, Parent, thatGraph, relativeGraph;
graph_t aGraph; // make local
graph_t swimlaneSeq[], order[], swimlanePool[];
node_t thisNode;
string _ERR; // bug workaround
////////////////////////////////////////////////////////////////////////////
void snapShot(graph_t snapG, string lbl) {
return; // turn off for now
string saveLbl="", filePrefix="__snapshot_";
tmpO=openF(filePrefix+(string)(++fileNumber),"w");
if (hasAttr(snapG, "label") && snapG.label != "")
saveLbl=snapG.label;
snapG.label=lbl;
fwriteG(snapG,tmpO);
closeF(tmpO);
snapG.label=saveLbl;
}
///////////////////////////////////////////////////////////////////////
// do NOT call with direct output from sprintf
// OR a concatenated string "xxx" + "yyy" ...
// - there is a bug - string will be empty
void doErr(string errString) {
printf(2, "Error:: %s\n", errString);
}
////////////////////////////////////////////////////////////////////////
// gvpr seems to have a bug if "bogus" attribute names are "too" long
// seemingly, no problem if the "bogus" name starts with "_"
// so we try aget
////////////////////////////////////////////////////////////////////////
int myHasAttr(obj_t thisObj, string thisAtt) {
if (hasAttr(thisObj, thisAtt) && aget(thisObj, thisAtt) != "") {
//print("// myHasAttr: ", thisObj.name, " ", thisAtt, " returning 1");
return 1;
} else {
//print("// myHasAttr: ", thisObj.name, " ", thisAtt, " returning 0");
return 0;
}
}
////////////////////////////////////////////////////////////////////////////
// validate a binary attribute and return a corresponding variable
int isTrue(obj_t thisObj, string thisAtt) {
string tmpStr, rslt;
int rc;
if (!myHasAttr(thisObj, thisAtt)) {
_ERR=thisObj.name + " has no attribute named " + thisAtt;
doErr(_ERR);
rc=0;
} else {
tmpStr = aget(thisObj, thisAtt);
tmpStr = tolower(tmpStr);
//print("// tmpStr: ", tmpStr);
if (tmpStr == "@(1|yes|true)") { // ksh syntax
//print("// TRUE: ", tmpStr);
rc = 1;
} else if (tmpStr == "@(0|no|false)") { // ksh syntax
//print("// FALSE: ", tmpStr);
rc = 0;
} else {
_ERR=" value must be true or false (" + tmpStr + ")";
doErr(_ERR);
rc=0;
}
}
print("// isTrue - rc: ", rc);
return rc;
}
////////////////////////////////////////////////////////////////////////
int ExistsAndTrue(obj_t thisObj, string thisAtt) {
if (myHasAttr(thisObj, thisAtt)==0)
return 0;
return isTrue(thisObj, thisAtt);
}
////////////////////////////////////////////////////////////////////////
float MinX(graph_t thisG) {
return (float) xOf(llOf(thisG.bb));
}
float MinY(graph_t thisG) {
return (float) yOf(llOf(thisG.bb));
}
float MaxX(graph_t thisG) {
return (float) xOf(urOf(thisG.bb));
}
float MaxY(graph_t thisG) {
return (float) yOf(urOf(thisG.bb));
}
float CenterX(graph_t thisG) {
return (float) xOf(llOf(thisG.bb)) + ((float) xOf(urOf(thisG.bb)) - (float) xOf(llOf(thisG.bb))) / 2.;
}
float CenterY(graph_t thisG) {
return (float) yOf(llOf(thisG.bb)) + ((float) yOf(urOf(thisG.bb)) - (float) yOf(llOf(thisG.bb))) / 2.;
}
/////////////////////////////////////////////////////////////////////////////////////
void changeBBto(graph_t thisGraph, float newMinX, float newMinY, float newMaxX, float newMaxY) {
print("// changeBBto before ", thisGraph.name, " ", thisGraph.bb, " ", newMinX, " ", newMinY, " ", newMaxX, " ", newMaxY);
thisGraph.bb = sprintf("%.2f,%.2f,%.2f,%.2f", newMinX, newMinY, newMaxX, newMaxY);
print("// changeBBto after ", thisGraph.bb);
snapShot(Root, "after changeBBto");
}
///////////////////////////////////////////////////////////////////////////////////
void changeBBby(graph_t thisGraph, float dMinX, float dMinY, float dMaxX, float dMaxY) {
print("// changedBBby before ", thisGraph.name, " ",thisGraph.bb, " ", dMinX, " ", dMinY, " ", dMaxX, " ", dMaxY);
thisGraph.bb = sprintf("%.2f,%.2f,%.2f,%.2f", MinX(thisGraph) + dMinX, MinY(thisGraph) + dMinY, MaxX(thisGraph) + dMaxX, MaxY(thisGraph) + dMaxY);
print("// changedBBby after ", thisGraph.bb);
snapShot(Root, "After changeBBby");
}
/////////////////////////////////////////////////////////////////////////////////////
graph_t recordClusters(graph_t tGraph) {
graph_t thisG, aG;
//print("// isCluster!");
for (thisG = fstsubg(tGraph); thisG; thisG = nxtsubg(thisG)) {
if ((thisG.name == "cluster*") || ExistsAndTrue(thisG, "cluster")) {
isCluster[thisG]=1;
} else {
isCluster[thisG]=0;
}
print("// isCluster: ", thisG.name, " is: ",isCluster[thisG]);
//////////////////////////////////////////////////////////////////////////////
// grab lp information (label position) before we start moving clusters around
// store as relative (margin), so only need to get it once
//////////////////////////////////////////////////////////////////////////////
if (hasAttr(thisG, "lp") && thisG.lp != "" && thisG.parent!=NULL) {
aG=thisG.parent;
if (thisG.lp!=aG.lp) {
print("// computing labelMargin for ", thisG.name);
if (hasAttr(thisG, "labelloc")) {
if (thisG.labelloc == "[Bb]") {
print("// bottom label ",(float)yOf(thisG.lp)," ",MinY(thisG));
labelMargin = 2. * ((float) yOf(thisG.lp) - MinY(thisG));
bottomMargin[thisG] = labelMargin;
} else { // at the top
print("// top/unset label ",(float)yOf(thisG.lp)," ",MaxY(thisG));
labelMargin = 2. * (MaxY(thisG) - (float) yOf(thisG.lp));
topMargin[thisG] = labelMargin;
}
print("// labelloc: ", thisG.labelloc, " lp: ", thisG.lp, " bb: ", thisG.bb, " labelMargin: ", labelMargin);
}
}
}
thisG = recordClusters(thisG);
}
return tGraph;
}
/***********************************************************************************
set min & max X & Y for the current pool, one lane at a time
is this useful?? reality changes, this does not track
maybe real-time calculate for all children clusters??
current plan-ish: calc entire pool with one call
but still have to call repeatedly, hmm
*************************************************************************************/
///////////////////////////////////////////////////////////////////////////////
void calcPoolMinMax(graph_t thisGraph) {
print("// calcPoolMinMax - firstPoolMinMax: ", firstPoolMinMax, " Graph: ", thisGraph, " dX: ", MaxX(thisGraph)-MinX(thisGraph), " dY: ", MaxY(thisGraph)-MinY(thisGraph));
if (firstPoolMinMax || MinX(thisGraph) < poolMinX)
poolMinX = MinX(thisGraph);
if (firstPoolMinMax || MaxX(thisGraph) > poolMaxX)
poolMaxX = MaxX(thisGraph);
if (firstPoolMinMax || MinY(thisGraph) < poolMinY)
poolMinY = MinY(thisGraph);
if (firstPoolMinMax || MaxY(thisGraph) > poolMaxY)
poolMaxY = MaxY(thisGraph);
firstPoolMinMax = 0;
print("// pool ", thisGraph.name, " minX/Y & max X/Y: ", poolMinX, " ", poolMinY, " ", poolMaxX, " ", poolMaxY);
if (maxWidth < laneWidth[thisGraph])
maxWidth = laneWidth[thisGraph];
if (maxHeight < laneHeight[thisGraph])
maxHeight = laneHeight[thisGraph];
print("// maxWidth: ", maxWidth);
}
///////////////////////////////////////////////////////////////////////////////////
// this will be called once to set values
// hmm, called twice
// plus width & height usually change
// two lines of code, called once?? (move to in-line)
////////////////////////////////////////////////////////////////////
void setClusterHeightWidth(graph_t thisGraph) {
print("// setClusterHeightWidth - ", thisGraph.name, " bb: ", thisGraph.bb);
laneWidth[thisGraph] = MaxX(thisGraph) - MinX(thisGraph);
laneHeight[thisGraph] = MaxY(thisGraph) - MinY(thisGraph);
}
///////////////////////////////////////////////////////////////////////////////////////
// recursivly move cluster(s) by delta_X (dX), delta_Y (dY)
// to make general purpose: use bb values
//
graph_t moveClusterBy(graph_t mGraph, float dX, float dY) {
graph_t thisGraph;
print("// moveClusterBy MOVING: ", mGraph.name, " from ", mGraph.bb, " by ", dX, " ", dY);
mGraph.bb = sprintf("%.2f,%.2f,%.2f,%.2f", dX + (float) xOf(llOf(mGraph.bb)), dY + (float) yOf(llOf(mGraph.bb)), dX + (float) xOf(urOf(mGraph.bb)), dY + (float) yOf(urOf(mGraph.bb)));
printf("// SHIFTED cluster bb (%s) %.2f %.2f %.2f %.2f \n", mGraph.name, (float) xOf(llOf(mGraph.bb)), (float) yOf(llOf(mGraph.bb)), (float) xOf(urOf(mGraph.bb)), (float) yOf(urOf(mGraph.bb)));
for (thisGraph = fstsubg(mGraph); thisGraph; thisGraph = nxtsubg(thisGraph)) {
if (isCluster[thisGraph]) {
print("// cluster MOVING: ", thisGraph.name, " ", thisGraph.bb, " ", dX, " ", dY);
thisGraph = moveClusterBy(thisGraph, dX, dY);
}
}
// snapShot(Root, "After moveClusterBy");
return mGraph;
}
////////////////////////////////////////////////////////////////////////////////
// move one node
// this is general purpose - it changes pos
void moveThisNode(node_t thisNode, float dX, float dY) {
float posX, posY;
print("// NODE before : ", thisNode.name, " ", thisNode.pos);
sscanf(thisNode.pos, "%lf,%lf", &posX, &posY);
posX += dX;
posY += dY;
thisNode.pos = sprintf("%.2f,%.2f", posX, posY);
print("// NODE after : ", thisNode.name, " ", thisNode.pos);
}
//////////////////////////////////////////////////////////////////////////////////////
// this is general purpose
void moveClusterAndContentsBy(graph_t thisGraph, float dX, float dY) {
print("// content MOVING: ", thisGraph.name, " ", dX, " ", dY);
// move clusters
moveClusterBy(thisGraph, dX, dY);
// move nodes
for (thisNode = fstnode(thisGraph); thisNode; thisNode = nxtnode_sg(thisGraph, thisNode)) {
moveThisNode(thisNode, dX, dY);
//thisNode.Xpin = "true"; /// play with pinning nodes
}
snapShot(Root, "After moveClusterandContentsBy");
}
//////////////////////////////////////////////////////////////////////////
graph_t findPools(graph_t thisGraph) {
float TmpMinX, TmpMinY, TmpMaxX, TmpMaxY;
graph_t thisG;
print("// +++ findPools - (sub)graph: ", thisGraph.name);
if (isCluster[thisGraph]) {
if (!(myHasAttr(thisGraph, "bb"))) {
_ERR="Graph " + thisGraph.name + " does not have a bb (boundingBox) value";
doErr(_ERR);
exit(9);
}
setClusterHeightWidth(thisGraph);
if (ExistsAndTrue(thisGraph, "swimlanePool")) {
print("// SWIMLANEPOOL is set "+ thisGraph.name);
if (thisGraph.parent==NULL || !myHasAttr(thisGraph.parent, "swimlanePool") || !isTrue(thisGraph.parent, "swimlanePool")) {
print("// BINGO - new pool !!!!!!!! "+ thisGraph.name);
isPool[thisGraph]=1;
swimlanePool[++poolIndx]=thisGraph;
poolCnt++;
}
}
}
for (thisG = fstsubg(thisGraph); thisG; thisG = nxtsubg(thisG)) {
if(isCluster[thisG]) // ???? why this test ????
thisG = findPools(thisG);
}
return thisGraph;
} // end of findPools
///////////////////////////////////////////////////////////////////////
// endPool is too big
// find a way to reduce size
///////////////////////////////////////////////////////////////////////
void endPool() {
graph_t thisLane, parentGraph, tmpSeq[];
float tmpX = 0., tmpY = 0., deltaX, deltaY;
unset(tmpSeq);
snapShot(Root, "top of endPool");
print("////////////// start endPool ///////////////////////////");
thisLane = swimlaneSeq[1];
parentGraph = swimlanePool[poolIndx];
print("// swimlaneSeq[1]: ", thisLane.name, " ", thisLane.bb);
startingX = MinX(parentGraph) + lane2nodeMarginX; // + p2pMargin;
startingY = MinY(parentGraph) + lane2nodeMarginY; // + p2pMargin;
print("// startingX: ",startingX," startingY: ", startingY, " lane2nodeMarginX: ",lane2nodeMarginX, " lane2nodeMarginY: ", lane2nodeMarginX, " p2pMargin: ", p2pMargin);
print("// parentGraph : ", parentGraph.name, " ", parentGraph.bb);
if (dotSequence == 1) {
// reorder based on center of each swimlane/cluster, as positioned by dot
unset(swimlaneSeq);
laneSeqNo = 0;
forr (order[num]) {
print("// ordering: ", num, " ", order[num]);
swimlaneSeq[++laneSeqNo] = order[num];
print("// after reverse: ", swimlaneSeq[laneSeqNo]);
}
}
// calculate left-to-right sequence (default), unless user want input sequence
// reverse swimllane (cluster) order if rankdir=LR or RL ???
if (TBranked == 0) {
for (swimlaneSeq[i]) {
print("// reversing: ",i);
tmpSeq[laneSeqNo + 1 - i] = swimlaneSeq[i];
print("// before reverse: ", swimlaneSeq[i]);
}
for (tmpSeq[i]) {
swimlaneSeq[i] = tmpSeq[i];
print("// after reverse: ", swimlaneSeq[i]);
}
}
for (swimlaneSeq[i]) {
print("// swimlane: ", i, " ", swimlaneSeq[i].name);
thisLane = swimlaneSeq[i];
//////// move/shift this swimlane
print("// shifting lane: ", thisLane.name);
if (TBranked) {
if (equalSize == 1) {
if (laneWidth[thisLane] < maxWidth) {
//snapShot(Root, "!! endPool - equalSize !!!!!!!!");
changeBBby(thisLane, -(maxWidth - laneWidth[thisLane]) / 2., 0, (maxWidth - laneWidth[thisLane]) / 2.);
laneWidth[thisLane] = maxWidth;
}
}
moveClusterAndContentsBy(thisLane, startingX - MinX(thisLane), 0);
startingX = MaxX(thisLane);
} else { // LR
if (equalSize) {
print("// equalsize & LR ranked");
if (laneWidth[thisLane] < maxWidth) {
//snapShot(Root, "!! endPool - equalSize !!!!!!!!");
changeBBby(thisLane, -(maxWidth - laneWidth[thisLane]) / 2., 0, (maxWidth - laneWidth[thisLane]) / 2.);
laneWidth[thisLane] = maxWidth;
}
}
moveClusterAndContentsBy(thisLane, 0, startingY - MinY(thisLane));
startingY = MaxY(thisLane);
}
//////// expand height (or width) of the lanes so all lanes "line up"
if (TBranked == 1) {
changeBBto(thisLane, MinX(thisLane), poolMinY, MaxX(thisLane), poolMaxY);
} else {
changeBBto(thisLane, poolMinX, MinY(thisLane), poolMaxX, MaxY(thisLane));
}
}
// adjust parent/pool cluster
// pool2pool
if (TBranked == 1) {
startingX += lane2nodeMarginX;
} else {
startingY -= lane2nodeMarginY;
}
///////////////////////// adjust for possible margins !!!!!!!!!!!!!!!!!!!
firstPoolMinMax = 1;
for (swimlaneSeq[i]) {
thisLane = swimlaneSeq[i];
if (isCluster[thisLane])
calcPoolMinMax(thisLane);
}
tmpMarginX=lane2nodeMarginX; // +p2pMargin;
tmpMarginY=lane2nodeMarginY; // +p2pMargin;
//snapShot(Root, "!! endPool - ADJUSTING FOR LABELS - " + (string)bottomMargin[parentGraph] + " " +(string)topMargin[parentGraph] + " !!!!!!!!");
while (1==1) {
isParent[parentGraph]=1;
changeBBto(parentGraph, poolMinX-tmpMarginX, poolMinY-tmpMarginY, poolMaxX+tmpMarginX, poolMaxY+tmpMarginY);
// adjust poolCluster by top & bottom labelmargin
changeBBby(parentGraph, 0, -bottomMargin[parentGraph], 0, topMargin[parentGraph]);
// not thrilled how this is done, but a top label has pushed the bb up
// so, now we slide it back down
if (topMargin[parentGraph]>0)
moveClusterAndContentsBy(parentGraph, 0, -topMargin[parentGraph]);
if (parentGraph==Root) break;
parentGraph=parentGraph.parent;
tmpMarginX+=lane2nodeMarginX;
tmpMarginY+=lane2nodeMarginY;
}
////// shift the rest of the pools - better for videos
snapShot(Root, "!! endPool - before shifting");
if (poolIndx<poolCnt) {
if (TBranked) {
deltaX=MaxX(swimlanePool[poolIndx]) - MinX(swimlanePool[poolIndx+1]);
deltaY=0;
} else {
deltaX=0;
deltaY=MaxY(swimlanePool[poolIndx]) - MinY(swimlanePool[poolIndx+1]);
}
}
for (i=poolIndx+1; i<=poolCnt; i++) {
moveClusterAndContentsBy(swimlanePool[i], deltaX, deltaY);
}
snapShot(Root, "!!!!!! endPool - the end");
// to do
// - find any loose nodes inside the pool - not "legal"
// - adjust any nodes (and clusters) outside the pool
// and to the right (greater X) of the pool
} // end of endPool
/////////////////////////////////////////////////////////////////////////
void startNewPool(graph_t newSwimlanePool) {
string lblloc;
print("///////////////////////// startNewPool ///////////////");
print("// Starting new pool ", newSwimlanePool.name, " ", newSwimlanePool);
//isPool[newSwimlanePool] = 1;
swimlanePool[++poolIndx] = newSwimlanePool;
print("// swimlanePool: ", swimlanePool[poolIndx], " poolIndx: ", poolIndx);
unset(swimlaneSeq);
unset(laneWidth);
unset(laneHeight);
unset(order);
maxWidth = 0;
maxHeight = 0;
laneSeqNo = 0;
// redundant?? firstPoolMinMax = 1;
dotSequence = 1; // add test for user sequence
//// if lp is set, determine margin required by label
// from top/bottom of cluster
// if labelloc exists & =="[Bb]", margin from bottom, else from top
// note: this is done BEFORE moving anything (I hope)
// labelMargin = 0; /// <<< or not zero ???
Parent=newSwimlanePool.parent;
} // end of startNewPool
//////////////////////////////////////////////////////////////////////////
// recursive routine, processing from the top (root) down << ????????
// looking for pools, swimlanes (and non-swimlanes)
// too big, find a way to make smaller
// also find a better name clusterCheck -> processPools
//////////////////////////////////////////////////////////////////////////
graph_t processPools(graph_t thisGraph) {
float TmpMinX, TmpMinY, TmpMaxX, TmpMaxY;
graph_t thisG;
print("// +++ processPools - (sub)graph: ", thisGraph.name);
setClusterHeightWidth(thisGraph);
if (isPool[thisGraph]) {
print("// SWIMLANEPOOL is set "+ thisGraph.name);
print("// BINGO - new pool !!!!!!!! "+ thisGraph.name);
// new pool cluster (not a swimlane cluster)
// do end-of-pool processing for previous pool
if (laneSeqNo != -1) // not first pool
endPool();
startNewPool(thisGraph);
print("// NEW POOL: ", thisGraph.name);
equalSize = ExistsAndTrue(thisGraph, "swimlaneEqualSize");
inputSequence = ExistsAndTrue(thisGraph, "swimlaneInputSequence");
firstPoolMinMax = 1; // !!! NOT redundant !!!
if (myHasAttr(thisGraph, "margin") && thisGraph.margin != "") {
c = sscanf(thisGraph.margin, "%lf,%lf", &lane2nodeMarginX, &lane2nodeMarginY);
if (c == 1)
lane2nodeMarginY = lane2nodeMarginX;
} else {
lane2nodeMarginX = lane2nodeMarginY = 8;
}
print("// lane2nodeMarginX/Y: ", lane2nodeMarginX, " ", lane2nodeMarginY);
} else {
// not a new pool
Parent = thisGraph.parent;
if ((Parent != NULL) && (myHasAttr(Parent, "swimlanePool")) ) {
// not a new pool
// - a swimlane cluster OR sub-cluster (internal cluster)
print("// lane or subcluster: ", thisGraph.name);
if (isPool[Parent]) { // a lane (not embedded cluster)
swimlaneSeq[++laneSeqNo] = thisGraph;
print("// NEW LANE: ", laneSeqNo, " ",thisGraph.name);
// calc order, based on dot layout
if (TBranked) {
num = MaxY(thisGraph) - (MinX(thisGraph) / 100000);
} else {
num = MinX(thisGraph) - (MaxY(thisGraph) / 100000);
}
order[num] = thisGraph;
print("// order ", thisGraph.name, " ", num);
print("// SwimLane !!: ", thisGraph.name);
// set max & min X/Y values for entire
calcPoolMinMax(thisGraph);
}
} else { // cluster/root not involved in swimlanes/pools
if (thisGraph!=Root) {
outsideCluster[thisGraph] = 1;
print("// +++++++++ setting outsideCluster: ", thisGraph.name);
}
}
}
///////////////////////////////////////////////////////////
// labels & lp are inherited, especially after running dot -Tdot (it rearranges things)
// if lp=Parent.lp, we need to unset labels & lps
// !!!!! only delete values AFTER CHECKING children clusters
////////////////////////////////////////////////////////////
//print("// do we delete: ", thisGraph.name, " label values?");
Parent=thisGraph.parent;
if (hasAttr(thisGraph, "lp") && thisGraph.lp != "" && Parent!=NULL && thisGraph.lp==Parent.lp) {
//print("// YES!! We do delete: ", thisGraph.name, " label values");
thisGraph.lp="";
thisGraph.label="";
}
for (thisG = fstsubg(thisGraph); thisG; thisG = nxtsubg(thisG)) {
if(isCluster[thisG])
thisG = processPools(thisG);
}
return thisGraph;
} // end of processPools
} // end of BEGIN
/////////////////////////////////////////////////////////////////////////
BEG_G {
poolIndx = 0;
Root = $G;
// fix other tests for rankdir, use TBranked
if (myHasAttr(Root, "rankdir") && Root.rankdir == "[Ll][Rr]") {
TBranked = 0;
} else {
TBranked = 1;
}
print ("// TBranked: ", TBranked);
if (!(hasAttr($G, "splines") && $G.splines!=""))
$G.splines="true";
if (hasAttr($G, "pool2poolMargin") && $G.pool2poolMargin!="")
p2pMargin=(int)$G.pool2poolMargin;
else
p2pMargin=8;
print("// p2pMargin: ", p2pMargin);
isCluster[$G]=1;
recordClusters($G);
findPools($G);
poolIndx=0; // reset, is this necessary ??????????????????????????
for (thisNode = fstnode($G); thisNode; thisNode = nxtnode(thisNode)) {
// gather positional data to determine above/below/left/right
/*
if (TBranked){
rank[thisNode]=thisNode.Y;
// actually in reverse order
}else{
rank[thisNode]=thisNode.X;
}
*/
}
processPools($G);
if (poolCnt==0) {
doErr("Did not find any Pools");
exit(9);
} else
endPool();
print("// done with BEG_G");
}
END_G {
string fld[int];
unset(fld);
print("// start of END_G");
snapShot(Root, "****** before (last) shifting set");
// for each larger cluster, find other embedded clusters
// (north, south, east or west) and reposition
// if they exist, position multiple pools (swimlane pools)
print("// poolIndx count: ", poolIndx);
for (i = 2; i <= poolCnt; i++) {
print("// shifting swimlanepool: ", swimlanePool[i], " from: ", swimlanePool[i - 1]);
/// keep it simple
/// add (maybe) margin from one pool (swimlanepool) to the next
if (TBranked) {
moveClusterAndContentsBy(swimlanePool[i], p2pMargin + MaxX(swimlanePool[i-1]) - MinX(swimlanePool[i]), 0);
} else {
moveClusterAndContentsBy(swimlanePool[i], 0, p2pMargin + MaxY(swimlanePool[i-1]) - MinY(swimlanePool[i]));
}
}
snapShot(Root, "after (last) shifting set");
/////////////
// try to reposition any/all clusters that are outside of the pool(s)
/////////////
for (outsideCluster[aGraph]) {
print("////// outsideCluster: ", aGraph.name);
if (myHasAttr(aGraph, "swimlanePos") && aGraph.swimlanePos != "") {
n = split(aGraph.swimlanePos, fld, ":");
print("// outsideCluster: ", aGraph.name, " ", fld[0], " ", fld[1]);
relativeGraph = isSubg(Root, fld[0]);
if (relativeGraph != NULL) {
if (fld[1] == "n" || ((fld[1] == "(pre|before)" && TBranked == 1))) {
print("// n or pre");
moveClusterAndContentsBy(aGraph, CenterX(relativeGraph) - CenterX(aGraph),
MaxY(relativeGraph) - MinY(aGraph) + 50);
} else if (fld[1] == "s" || ((fld[1] == "(post|after)" && TBranked == 1))) {
print("// s or post");
moveClusterAndContentsBy(aGraph, CenterX(relativeGraph) - CenterX(aGraph), MinY(relativeGraph) - MaxY(aGraph) - 50);
} else if (fld[1] == "e" || ((fld[1] == "(post|after)" && TBranked == 0))) {
print("// e or post");
moveClusterAndContentsBy(aGraph, MaxX(relativeGraph) - MinX(aGraph) + 50, CenterY(relativeGraph) - CenterY(aGraph));
} else if (fld[1] == "w" || ((fld[1] == "(pre|before)" && TBranked == 0))) {
print("// w or pre");
moveClusterAndContentsBy(aGraph, MinX(relativeGraph)
- MaxX(aGraph) - 50, CenterY(relativeGraph) - CenterY(aGraph));
} else {
_ERR="The positional string (" + fld[1] + ") is invalid";
doErr(_ERR);
}
} else { // unknown pool
_ERR="No such pool/cluster (" + fld[0] + ")";
doErr(_ERR);
}
} else {
if (isParent[aGraph]!=1) {
_ERR="The cluster (" + aGraph.name + ") will not be repositioned";
doErr(_ERR);
} else {
print("// Ahhh! The cluster " + substr(aGraph.name,0) + " is a parent (wraps around)");
changeBBto(aGraph, MinX(swimlanePool[1])-8, MinY(swimlanePool[1])-8, MaxX(swimlanePool[poolCnt])+8, MaxY(swimlanePool[poolCnt])+8);
}
}
print("////// end of outsideCluster: ", aGraph.name);
}
$G.bb="";
//// try to move lp removal to earlier in the process, nut not a big deal
for (isCluster[aGraph])
aGraph.lp="";
snapShot(Root, "The End");
print("// end of END_G");
}
/*
// we want to find all "loose" nodes - nodes that are in a pool (pool) but not contained in a swimlane
// it is annoying that there is no simple way to determine the immediate graph parent of a node
// (*.parent applies to graphs, not nodes)
// * for each pool
// * list ALL nodes with pool as parent (any level)
// * for each swimlane
// * remove ALL nodes with swimlane (or any cluster/root) as parent <<< x levels deep???
*/
/*
look for uncontained (un-laned) nodes within a pool
find youngest/lowest pool
analize & re-arrange the pool
repeat for all pools
re-arrange the pools (and other parts/clusters)
*/
//////////////////////////////////////////////////////////////////////////////////////
/* GVPR COMMENTS
- gvpr uses ksh pattern matching - !!!
- almost all variables are global, what a pain!
- two-dimensional arrays would be nice (multi-dimensional)
- hasAttr - reports true (even) if value is inherited!! (ouch)
- it woud be nice to be able to turn inheritance off
- it would be nice to have node.parent (not just graph.parent)
- node height/with is increased by (# of peripheries -1)
REMEMBER:
- does not work: myFunction(sprintf(...)) - need separate sprintf
- may also apply to other built-ins
- (almost) all variables are GLOBAL
- split (& tokens) require array xxx[int]
- graph traversal has to be RECURSIVE !!!!!
- peripheries show up in node width & height << a bug (though helpful in some ways
- 5 is an integer, 5. is floating point
*/
/*******************************************************************************
// an example:
digraph G {
node [label="\N"];
subgraph clusterSurround2 {
swimlanePool=true
subgraph cluster21 {
graph [color=green]
x10
y10
z10
z10
x10 -> y10
x10 -> z10
x10 -> z10
}
subgraph cluster22 {
graph [color=red]
x11 y11 z11 ZZ11
x11 -> y11
x11 -> z11
x11 -> ZZ11
}
subgraph cluster23 {
graph [color=purple]
x12 y12 z12 ZZ12
x12 -> y12 -> z12 -> ZZ12
}
z11 ->x10
x10-> y12
}
}
*************************************************************************/
@steveroush
Copy link
Author

This is a work-in-progress. It passes many, but not all of my test cases.
There is a test case appended to the bottom of the file.
Feel free to ask questions, and please report successes and failures.

@steveroush
Copy link
Author

Improved version

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment