Last active
July 17, 2024 21:38
-
-
Save steveroush/eea511daa2eecfd8709127f4bff12315 to your computer and use it in GitHub Desktop.
a GVPR program that adds a swimlane feature to Graphviz (dot)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/************************************************************************* | |
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 | |
} | |
} | |
*************************************************************************/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.