Fossil

Check-in [d7a0240f]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Change the graph layout to include ascenders for nodes that have off-screen children. Fix a bug in the leaf-tracker that became evident by this enhancement. (A rebuild is required to rebuild the leaf table after updating, or else some timeline graphs will not display correctly.)
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1:d7a0240f3963be22136155b857d52a60c28e11c5
User & Date: drh 2011-03-15 01:34:04
Context
2011-03-15
01:44
A merge riser is allowed to ride along side a riser to an off-screen node. check-in: 52ea66df user: drh tags: trunk
01:34
Change the graph layout to include ascenders for nodes that have off-screen children. Fix a bug in the leaf-tracker that became evident by this enhancement. (A rebuild is required to rebuild the leaf table after updating, or else some timeline graphs will not display correctly.) check-in: d7a0240f user: drh tags: trunk
2011-03-14
21:36
Fix an issue with the leaf-tracker that comes up if a check-in specifies a shunned check-in as its parent or merge parent. Ticket [398afa58def78f5]. check-in: aa6abc5e user: drh tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to src/finfo.c.

244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
...
299
300
301
302
303
304
305
306
307
308
    const char *zBgClr = db_column_text(&q, 8);
    const char *zBr = db_column_text(&q, 9);
    int gidx;
    char zTime[10];
    char zShort[20];
    char zShortCkin[20];
    if( zBr==0 ) zBr = "trunk";
    gidx = graph_add_row(pGraph, frid, fpid>0 ? 1 : 0, &fpid, zBr, zBgClr);
    if( memcmp(zDate, zPrevDate, 10) ){
      sqlite3_snprintf(sizeof(zPrevDate), zPrevDate, "%.10s", zDate);
      @ <tr><td>
      @   <div class="divider">%s(zPrevDate)</div>
      @ </td></tr>
    }
    memcpy(zTime, &zDate[11], 5);
................................................................................
      pGraph = 0;
    }else{
      @ <tr><td></td><td><div style="width:%d(pGraph->mxRail*20+30)px;"></div>
      @     </td></tr>
    }
  }
  @ </table>
  timeline_output_graph_javascript(pGraph);
  style_footer();
}







|







 







|


244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
...
299
300
301
302
303
304
305
306
307
308
    const char *zBgClr = db_column_text(&q, 8);
    const char *zBr = db_column_text(&q, 9);
    int gidx;
    char zTime[10];
    char zShort[20];
    char zShortCkin[20];
    if( zBr==0 ) zBr = "trunk";
    gidx = graph_add_row(pGraph, frid, fpid>0 ? 1 : 0, &fpid, zBr, zBgClr, 1);
    if( memcmp(zDate, zPrevDate, 10) ){
      sqlite3_snprintf(sizeof(zPrevDate), zPrevDate, "%.10s", zDate);
      @ <tr><td>
      @   <div class="divider">%s(zPrevDate)</div>
      @ </td></tr>
    }
    memcpy(zTime, &zDate[11], 5);
................................................................................
      pGraph = 0;
    }else{
      @ <tr><td></td><td><div style="width:%d(pGraph->mxRail*20+30)px;"></div>
      @     </td></tr>
    }
  }
  @ </table>
  timeline_output_graph_javascript(pGraph, 1);
  style_footer();
}

Changes to src/graph.c.

40
41
42
43
44
45
46


47
48
49
50
51
52
53
...
161
162
163
164
165
166
167
168

169
170
171
172
173
174
175
176
177
178
179
180


181
182
183
184
185
186
187
...
222
223
224
225
226
227
228

229
230
231
232
233
234
235
...
354
355
356
357
358
359
360
361



362
363
364
365
366
367
368
...
399
400
401
402
403
404
405


406








407
408
409
410
411
412
413
...
429
430
431
432
433
434
435

436
437
438
439
440
441
442
  GraphRow *pNext;            /* Next row down in the list of all rows */
  GraphRow *pPrev;            /* Previous row */
  
  int idx;                    /* Row index.  First is 1.  0 used for "none" */
  int idxTop;                 /* Direct descendent highest up on the graph */
  GraphRow *pChild;           /* Child immediately above this node */
  u8 isDup;                   /* True if this is duplicate of a prior entry */


  u8 bDescender;              /* True if riser from bottom of graph to here. */
  i8 iRail;                   /* Which rail this check-in appears on. 0-based.*/
  i8 mergeOut;                /* Merge out to this rail.  -1 if no merge-out */
  u8 mergeIn[GR_MAX_RAIL];    /* Merge in from non-zero rails */
  int aiRiser[GR_MAX_RAIL];   /* Risers from this node to a higher row. */
  int mergeUpto;              /* Draw the mergeOut rail up to this level */
  u32 mergeDown;              /* Draw merge lines up from bottom of graph */
................................................................................
*/
int graph_add_row(
  GraphContext *p,     /* The context to which the row is added */
  int rid,             /* RID for the check-in */
  int nParent,         /* Number of parents */
  int *aParent,        /* Array of parents */
  const char *zBranch, /* Branch for this check-in */
  const char *zBgClr   /* Background color. NULL or "" for white. */

){
  GraphRow *pRow;
  int nByte;

  if( p->nErr ) return 0;
  nByte = sizeof(GraphRow);
  nByte += sizeof(pRow->aParent[0])*nParent;
  pRow = (GraphRow*)safeMalloc( nByte );
  pRow->aParent = (int*)&pRow[1];
  pRow->rid = rid;
  pRow->nParent = nParent;
  pRow->zBranch = persistBranchName(p, zBranch);


  if( zBgClr==0 || zBgClr[0]==0 ) zBgClr = "white";
  pRow->zBgClr = persistBranchName(p, zBgClr);
  memcpy(pRow->aParent, aParent, sizeof(aParent[0])*nParent);
  if( p->pFirst==0 ){
    p->pFirst = pRow;
  }else{
    p->pLast->pNext = pRow;
................................................................................
      if( dist<iBestDist ){
        iBestDist = dist;
        iBest = i;
      }
    }
  }
  if( iBestDist>1000 ) p->nErr++;

  return iBest;
}

/*
** Assign all children of node pBottom to the same rail as pBottom.
*/
static void assignChildrenToRail(GraphRow *pBottom){
................................................................................
  */
  for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
    if( pRow->isDup ) continue;
    if( pRow->nParent==0 ) continue;                   /* Root node */
    pParent = hashFind(p, pRow->aParent[0]);
    if( pParent==0 ) continue;                         /* Parent off-screen */
    if( pParent->zBranch!=pRow->zBranch ) continue;    /* Different branch */
    if( pParent->idx <= pRow->idx ) continue;          /* Time-warp */



    if( pRow->idxTop < pParent->idxTop ){
      pParent->pChild = pRow;
      pParent->idxTop = pRow->idxTop;
    }
  }

  /* Identify rows where the primary parent is off screen.  Assign
................................................................................
  /* Assign rails to all rows that are still unassigned.
  */
  inUse = (1<<(p->mxRail+1))-1;
  for(pRow=p->pLast; pRow; pRow=pRow->pPrev){
    int parentRid;

    if( pRow->iRail>=0 ){


      if( pRow->pChild==0 ) inUse &= ~(1<<pRow->iRail);








      continue;
    }
    if( pRow->isDup ){
      pRow->iRail = findFreeRail(p, pRow->idx, pRow->idx, inUse, 0);
      pDesc = pRow;
      pParent = 0;
    }else{
................................................................................
        ** appears below the parent in the timeline. */
        int iDownRail = ++p->mxRail;
        if( iDownRail<1 ) iDownRail = ++p->mxRail;
        pRow->iRail = ++p->mxRail;
        pRow->railInUse = 1<<pRow->iRail;
        pParent->aiRiser[iDownRail] = pRow->idx;
        mask = 1<<iDownRail;

        for(pLoop=p->pFirst; pLoop; pLoop=pLoop->pNext){
          pLoop->railInUse |= mask;
        }
      }
    }
    mask = 1<<pRow->iRail;
    pRow->railInUse |= mask;







>
>







 







|
>












>
>







 







>







 







|
>
>
>







 







>
>
|
>
>
>
>
>
>
>
>







 







>







40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
...
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
...
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
...
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
...
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
...
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
  GraphRow *pNext;            /* Next row down in the list of all rows */
  GraphRow *pPrev;            /* Previous row */
  
  int idx;                    /* Row index.  First is 1.  0 used for "none" */
  int idxTop;                 /* Direct descendent highest up on the graph */
  GraphRow *pChild;           /* Child immediately above this node */
  u8 isDup;                   /* True if this is duplicate of a prior entry */
  u8 isLeaf;                  /* True if this is a leaf node */
  u8 timeWarp;                /* Child is earlier in time */
  u8 bDescender;              /* True if riser from bottom of graph to here. */
  i8 iRail;                   /* Which rail this check-in appears on. 0-based.*/
  i8 mergeOut;                /* Merge out to this rail.  -1 if no merge-out */
  u8 mergeIn[GR_MAX_RAIL];    /* Merge in from non-zero rails */
  int aiRiser[GR_MAX_RAIL];   /* Risers from this node to a higher row. */
  int mergeUpto;              /* Draw the mergeOut rail up to this level */
  u32 mergeDown;              /* Draw merge lines up from bottom of graph */
................................................................................
*/
int graph_add_row(
  GraphContext *p,     /* The context to which the row is added */
  int rid,             /* RID for the check-in */
  int nParent,         /* Number of parents */
  int *aParent,        /* Array of parents */
  const char *zBranch, /* Branch for this check-in */
  const char *zBgClr,  /* Background color. NULL or "" for white. */
  int isLeaf           /* True if this row is a leaf */
){
  GraphRow *pRow;
  int nByte;

  if( p->nErr ) return 0;
  nByte = sizeof(GraphRow);
  nByte += sizeof(pRow->aParent[0])*nParent;
  pRow = (GraphRow*)safeMalloc( nByte );
  pRow->aParent = (int*)&pRow[1];
  pRow->rid = rid;
  pRow->nParent = nParent;
  pRow->zBranch = persistBranchName(p, zBranch);
  pRow->isLeaf = isLeaf;
  memset(pRow->aiRiser, -1, sizeof(pRow->aiRiser));
  if( zBgClr==0 || zBgClr[0]==0 ) zBgClr = "white";
  pRow->zBgClr = persistBranchName(p, zBgClr);
  memcpy(pRow->aParent, aParent, sizeof(aParent[0])*nParent);
  if( p->pFirst==0 ){
    p->pFirst = pRow;
  }else{
    p->pLast->pNext = pRow;
................................................................................
      if( dist<iBestDist ){
        iBestDist = dist;
        iBest = i;
      }
    }
  }
  if( iBestDist>1000 ) p->nErr++;
  if( iBest>p->mxRail ) p->mxRail = iBest;
  return iBest;
}

/*
** Assign all children of node pBottom to the same rail as pBottom.
*/
static void assignChildrenToRail(GraphRow *pBottom){
................................................................................
  */
  for(pRow=p->pFirst; pRow; pRow=pRow->pNext){
    if( pRow->isDup ) continue;
    if( pRow->nParent==0 ) continue;                   /* Root node */
    pParent = hashFind(p, pRow->aParent[0]);
    if( pParent==0 ) continue;                         /* Parent off-screen */
    if( pParent->zBranch!=pRow->zBranch ) continue;    /* Different branch */
    if( pParent->idx <= pRow->idx ){
       pParent->timeWarp = 1;
       continue;                                       /* Time-warp */
    }
    if( pRow->idxTop < pParent->idxTop ){
      pParent->pChild = pRow;
      pParent->idxTop = pRow->idxTop;
    }
  }

  /* Identify rows where the primary parent is off screen.  Assign
................................................................................
  /* Assign rails to all rows that are still unassigned.
  */
  inUse = (1<<(p->mxRail+1))-1;
  for(pRow=p->pLast; pRow; pRow=pRow->pPrev){
    int parentRid;

    if( pRow->iRail>=0 ){
      if( pRow->pChild==0 && !pRow->timeWarp ){
        if( pRow->isLeaf || omitDescenders ){
          inUse &= ~(1<<pRow->iRail);
        }else{
          pRow->aiRiser[pRow->iRail] = 0;
          mask = 1<<pRow->iRail;
          for(pLoop=pRow; pLoop; pLoop=pLoop->pPrev){
            pLoop->railInUse |= mask;
          }
        }
      }
      continue;
    }
    if( pRow->isDup ){
      pRow->iRail = findFreeRail(p, pRow->idx, pRow->idx, inUse, 0);
      pDesc = pRow;
      pParent = 0;
    }else{
................................................................................
        ** appears below the parent in the timeline. */
        int iDownRail = ++p->mxRail;
        if( iDownRail<1 ) iDownRail = ++p->mxRail;
        pRow->iRail = ++p->mxRail;
        pRow->railInUse = 1<<pRow->iRail;
        pParent->aiRiser[iDownRail] = pRow->idx;
        mask = 1<<iDownRail;
        inUse |= mask;
        for(pLoop=p->pFirst; pLoop; pLoop=pLoop->pNext){
          pLoop->railInUse |= mask;
        }
      }
    }
    mask = 1<<pRow->iRail;
    pRow->railInUse |= mask;

Changes to src/leaf.c.

38
39
40
41
42
43
44
45

46
47
48
49
50
51
52
..
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
    "INSERT OR IGNORE INTO leaf"
    "  SELECT cid FROM plink"
    "  EXCEPT"
    "  SELECT pid FROM plink"
    "   WHERE coalesce((SELECT value FROM tagxref"
                       " WHERE tagid=%d AND rid=plink.pid),'trunk')"
         " == coalesce((SELECT value FROM tagxref"
                       " WHERE tagid=%d AND rid=plink.cid),'trunk');",

    TAG_BRANCH, TAG_BRANCH
  );
}

/*
** A bag of checkins whose leaf status needs to be checked.
*/
................................................................................
  static Stmt checkIfLeaf;
  static Stmt addLeaf;
  static Stmt removeLeaf;
  int rc;

  db_static_prepare(&checkIfLeaf,
    "SELECT 1 FROM plink"
    " WHERE pid=:rid"
    "   AND coalesce((SELECT value FROM tagxref"
                    " WHERE tagid=%d AND rid=:rid),'trunk')"
       " == coalesce((SELECT value FROM tagxref"
                    " WHERE tagid=%d AND rid=plink.cid),'trunk');",
    TAG_BRANCH, TAG_BRANCH
  );
  db_bind_int(&checkIfLeaf, ":rid", rid);







|
>







 







|







38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
..
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
    "INSERT OR IGNORE INTO leaf"
    "  SELECT cid FROM plink"
    "  EXCEPT"
    "  SELECT pid FROM plink"
    "   WHERE coalesce((SELECT value FROM tagxref"
                       " WHERE tagid=%d AND rid=plink.pid),'trunk')"
         " == coalesce((SELECT value FROM tagxref"
                       " WHERE tagid=%d AND rid=plink.cid),'trunk')"
      "   AND isprim",
    TAG_BRANCH, TAG_BRANCH
  );
}

/*
** A bag of checkins whose leaf status needs to be checked.
*/
................................................................................
  static Stmt checkIfLeaf;
  static Stmt addLeaf;
  static Stmt removeLeaf;
  int rc;

  db_static_prepare(&checkIfLeaf,
    "SELECT 1 FROM plink"
    " WHERE pid=:rid AND isprim"
    "   AND coalesce((SELECT value FROM tagxref"
                    " WHERE tagid=%d AND rid=:rid),'trunk')"
       " == coalesce((SELECT value FROM tagxref"
                    " WHERE tagid=%d AND rid=plink.cid),'trunk');",
    TAG_BRANCH, TAG_BRANCH
  );
  db_bind_int(&checkIfLeaf, ":rid", rid);

Changes to src/timeline.c.

252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
...
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
...
394
395
396
397
398
399
400

401
402
403
404
405
406
407
...
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426

427
428
429
430
431
432
433
...
513
514
515
516
517
518
519
520
521
522
523
524
525
526

527
528
529
530
531
532
533
      db_reset(&qparent);
      db_bind_int(&qbranch, ":rid", rid);
      if( db_step(&qbranch)==SQLITE_ROW ){
        zBr = db_column_text(&qbranch, 0);
      }else{
        zBr = "trunk";
      }
      gidx = graph_add_row(pGraph, rid, nParent, aParent, zBr, zBgClr);
      db_reset(&qbranch);
      @ <div id="m%d(gidx)"></div>
    }
    @</td>
    if( zBgClr && zBgClr[0] ){
      @ <td class="timelineTableCell" style="background-color: %h(zBgClr);">
    }else{
................................................................................
      */
      @ <tr><td /><td>
      @ <div id="grbtm" style="width:%d(pGraph->mxRail*20+30)px;"></div>
      @ </td></tr>
    }
  }
  @ </table>
  timeline_output_graph_javascript(pGraph);
}

/*
** Generate all of the necessary javascript to generate a timeline
** graph.
*/
void timeline_output_graph_javascript(GraphContext *pGraph){
  if( pGraph && pGraph->nErr==0 ){
    GraphRow *pRow;
    int i;
    char cSep;
    @ <script  type="text/JavaScript">
    @ /* <![CDATA[ */

................................................................................
    **        for the upward portion of a merge arrow.  The merge arrow goes up
    **        to the row identified by mu:.  If this value is zero then
    **        node has no merge children and no merge-out line is drawn.
    **   mu:  The id of the row which is the top of the merge-out arrow.
    **    u:  Draw a thick child-line out of the top of this node and up to
    **        the node with an id equal to this value.  0 if there is no
    **        thick-line riser.

    **   au:  An array of integers that define thick-line risers for branches.
    **        The integers are in pairs.  For each pair, the first integer is
    **        is the rail on which the riser should run and the second integer
    **        is the id of the node upto which the riser should run.
    **   mi:  "merge-in".  An array of integer x-coordinates from which
    **        merge arrows should be drawn into this node.  If the value is
    **        negative, then the x-coordinate is the absolute value of mi[]
................................................................................
    for(pRow=pGraph->pFirst; pRow; pRow=pRow->pNext){
      int mo = pRow->mergeOut;
      if( mo<0 ){
        mo = 0;
      }else{
        mo = (mo/4)*20 - 3 + 4*(mo&3);
      }
      cgi_printf("{id:%d,bg:\"%s\",r:%d,d:%d,mo:%d,mu:%d,u:%d,au:",
        pRow->idx,                      /* id */
        pRow->zBgClr,                   /* bg */
        pRow->iRail,                    /* r */
        pRow->bDescender,               /* d */
        mo,                             /* mo */
        pRow->mergeUpto,                /* mu */
        pRow->aiRiser[pRow->iRail]      /* u */

      );
      /* u */
      cSep = '[';
      for(i=0; i<GR_MAX_RAIL; i++){
        if( i==pRow->iRail ) continue;
        if( pRow->aiRiser[i]>0 ){
          cgi_printf("%c%d,%d", cSep, i, pRow->aiRiser[i]);
................................................................................
    @ }
    @ function drawThinLine(x0,y0,x1,y1){
    @   drawBox("black",x0,y0,x1,y1);
    @ }
    @ function drawNode(p, left, btm){
    @   drawBox("black",p.x-5,p.y-5,p.x+6,p.y+6);
    @   drawBox(p.bg,p.x-4,p.y-4,p.x+5,p.y+5);
    @   if( p.u>0 ){
    @     var u = rowinfo[p.u-1];
    @     drawUpArrow(p.x, u.y+6, p.y-5);
    @   }
    @   if( p.d ){
    @     drawUpArrow(p.x, p.y+6, btm);
    @   } 

    @   if( p.mo>0 ){
    @     var x1 = p.mo + left - 1;
    @     var y1 = p.y-3;
    @     var x0 = x1>p.x ? p.x+7 : p.x-6;
    @     var u = rowinfo[p.mu-1];
    @     var y0 = u.y+5;
    @     if( x1>=p.x-5 && x1<=p.x+5 ){







|







 







|






|







 







>







 







|






|
>







 







|
|
|
|
<
|
<
>







252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
...
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
...
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
...
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
...
515
516
517
518
519
520
521
522
523
524
525

526

527
528
529
530
531
532
533
534
      db_reset(&qparent);
      db_bind_int(&qbranch, ":rid", rid);
      if( db_step(&qbranch)==SQLITE_ROW ){
        zBr = db_column_text(&qbranch, 0);
      }else{
        zBr = "trunk";
      }
      gidx = graph_add_row(pGraph, rid, nParent, aParent, zBr, zBgClr, isLeaf);
      db_reset(&qbranch);
      @ <div id="m%d(gidx)"></div>
    }
    @</td>
    if( zBgClr && zBgClr[0] ){
      @ <td class="timelineTableCell" style="background-color: %h(zBgClr);">
    }else{
................................................................................
      */
      @ <tr><td /><td>
      @ <div id="grbtm" style="width:%d(pGraph->mxRail*20+30)px;"></div>
      @ </td></tr>
    }
  }
  @ </table>
  timeline_output_graph_javascript(pGraph, (tmFlags & TIMELINE_DISJOINT)!=0);
}

/*
** Generate all of the necessary javascript to generate a timeline
** graph.
*/
void timeline_output_graph_javascript(GraphContext *pGraph, int omitDescenders){
  if( pGraph && pGraph->nErr==0 ){
    GraphRow *pRow;
    int i;
    char cSep;
    @ <script  type="text/JavaScript">
    @ /* <![CDATA[ */

................................................................................
    **        for the upward portion of a merge arrow.  The merge arrow goes up
    **        to the row identified by mu:.  If this value is zero then
    **        node has no merge children and no merge-out line is drawn.
    **   mu:  The id of the row which is the top of the merge-out arrow.
    **    u:  Draw a thick child-line out of the top of this node and up to
    **        the node with an id equal to this value.  0 if there is no
    **        thick-line riser.
    **    f:  0x01: a leaf node.
    **   au:  An array of integers that define thick-line risers for branches.
    **        The integers are in pairs.  For each pair, the first integer is
    **        is the rail on which the riser should run and the second integer
    **        is the id of the node upto which the riser should run.
    **   mi:  "merge-in".  An array of integer x-coordinates from which
    **        merge arrows should be drawn into this node.  If the value is
    **        negative, then the x-coordinate is the absolute value of mi[]
................................................................................
    for(pRow=pGraph->pFirst; pRow; pRow=pRow->pNext){
      int mo = pRow->mergeOut;
      if( mo<0 ){
        mo = 0;
      }else{
        mo = (mo/4)*20 - 3 + 4*(mo&3);
      }
      cgi_printf("{id:%d,bg:\"%s\",r:%d,d:%d,mo:%d,mu:%d,u:%d,f:%d,au:",
        pRow->idx,                      /* id */
        pRow->zBgClr,                   /* bg */
        pRow->iRail,                    /* r */
        pRow->bDescender,               /* d */
        mo,                             /* mo */
        pRow->mergeUpto,                /* mu */
        pRow->aiRiser[pRow->iRail],     /* u */
        pRow->isLeaf ? 1 : 0            /* f */
      );
      /* u */
      cSep = '[';
      for(i=0; i<GR_MAX_RAIL; i++){
        if( i==pRow->iRail ) continue;
        if( pRow->aiRiser[i]>0 ){
          cgi_printf("%c%d,%d", cSep, i, pRow->aiRiser[i]);
................................................................................
    @ }
    @ function drawThinLine(x0,y0,x1,y1){
    @   drawBox("black",x0,y0,x1,y1);
    @ }
    @ function drawNode(p, left, btm){
    @   drawBox("black",p.x-5,p.y-5,p.x+6,p.y+6);
    @   drawBox(p.bg,p.x-4,p.y-4,p.x+5,p.y+5);
    @   if( p.u>0 ) drawUpArrow(p.x, rowinfo[p.u-1].y+6, p.y-5);
    if( !omitDescenders ){
      @   if( p.u==0 ) drawUpArrow(p.x, 0, p.y-5);
      @   if( p.f&1 ) drawBox("black",p.x-1,p.y-1,p.x+2,p.y+2);

      @   if( p.d ) drawUpArrow(p.x, p.y+6, btm);

    } 
    @   if( p.mo>0 ){
    @     var x1 = p.mo + left - 1;
    @     var y1 = p.y-3;
    @     var x0 = x1>p.x ? p.x+7 : p.x-6;
    @     var u = rowinfo[p.mu-1];
    @     var y0 = u.y+5;
    @     if( x1>=p.x-5 && x1<=p.x+5 ){