| | @@ -153,10 +153,11 @@ |
| 153 | 153 | */ |
| 154 | 154 | #if INTERFACE |
| 155 | 155 | #define TIMELINE_ARTID 0x0001 /* Show artifact IDs on non-check-in lines */ |
| 156 | 156 | #define TIMELINE_LEAFONLY 0x0002 /* Show "Leaf", but not "Merge", "Fork" etc */ |
| 157 | 157 | #define TIMELINE_BRIEF 0x0004 /* Combine adjacent elements of same object */ |
| 158 | +#define TIMELINE_GRAPH 0x0008 /* Compute a graph */ |
| 158 | 159 | #endif |
| 159 | 160 | |
| 160 | 161 | /* |
| 161 | 162 | ** Output a timeline in the web format given a query. The query |
| 162 | 163 | ** should return these columns: |
| | @@ -184,18 +185,23 @@ |
| 184 | 185 | int mxWikiLen; |
| 185 | 186 | Blob comment; |
| 186 | 187 | int prevTagid = 0; |
| 187 | 188 | int suppressCnt = 0; |
| 188 | 189 | char zPrevDate[20]; |
| 190 | + GraphContext *pGraph = 0; |
| 189 | 191 | |
| 190 | 192 | zPrevDate[0] = 0; |
| 191 | 193 | mxWikiLen = db_get_int("timeline-max-comment", 0); |
| 192 | 194 | if( db_get_boolean("timeline-block-markup", 0) ){ |
| 193 | 195 | wikiFlags = WIKI_INLINE; |
| 194 | 196 | }else{ |
| 195 | 197 | wikiFlags = WIKI_INLINE | WIKI_NOBLOCK; |
| 196 | 198 | } |
| 199 | + if( tmFlags & TIMELINE_GRAPH ){ |
| 200 | + pGraph = graph_init(); |
| 201 | + @ <div id="canvas" style="position:relative;width:1px;height:1px;"></div> |
| 202 | + } |
| 197 | 203 | |
| 198 | 204 | db_multi_exec( |
| 199 | 205 | "CREATE TEMP TABLE IF NOT EXISTS seen(rid INTEGER PRIMARY KEY);" |
| 200 | 206 | "DELETE FROM seen;" |
| 201 | 207 | ); |
| | @@ -212,10 +218,11 @@ |
| 212 | 218 | const char *zType = db_column_text(pQuery, 9); |
| 213 | 219 | const char *zUser = db_column_text(pQuery, 4); |
| 214 | 220 | const char *zTagList = db_column_text(pQuery, 10); |
| 215 | 221 | int tagid = db_column_int(pQuery, 11); |
| 216 | 222 | int commentColumn = 3; /* Column containing comment text */ |
| 223 | + char zTime[8]; |
| 217 | 224 | if( tagid ){ |
| 218 | 225 | if( tagid==prevTagid ){ |
| 219 | 226 | if( tmFlags & TIMELINE_BRIEF ){ |
| 220 | 227 | suppressCnt++; |
| 221 | 228 | continue; |
| | @@ -236,27 +243,29 @@ |
| 236 | 243 | continue; |
| 237 | 244 | } |
| 238 | 245 | db_multi_exec("INSERT OR IGNORE INTO seen VALUES(%d)", rid); |
| 239 | 246 | if( memcmp(zDate, zPrevDate, 10) ){ |
| 240 | 247 | sprintf(zPrevDate, "%.10s", zDate); |
| 241 | | - @ <tr><td colspan=3> |
| 242 | | - @ <div class="divider">%s(zPrevDate)</div> |
| 248 | + @ <tr><td> |
| 249 | + @ <div class="divider"><nobr>%s(zPrevDate)</nobr></div> |
| 243 | 250 | @ </td></tr> |
| 244 | 251 | } |
| 252 | + memcpy(zTime, &zDate[11], 5); |
| 253 | + zTime[5] = 0; |
| 245 | 254 | @ <tr> |
| 246 | | - @ <td valign="top">%s(&zDate[11])</td> |
| 255 | + @ <td valign="top" align="right">%s(zTime)</td> |
| 247 | 256 | @ <td width="20" align="center" valign="top"> |
| 248 | | - @ <font id="m%d(rid)" size="+1" color="white">*</font></td> |
| 257 | + @ <div id="m%d(rid)"></div> |
| 249 | 258 | if( zBgClr && zBgClr[0] ){ |
| 250 | 259 | @ <td valign="top" align="left" bgcolor="%h(zBgClr)"> |
| 251 | 260 | }else{ |
| 252 | 261 | @ <td valign="top" align="left"> |
| 253 | 262 | } |
| 254 | 263 | if( zType[0]=='c' ){ |
| 255 | 264 | const char *azTag[5]; |
| 256 | 265 | int nTag = 0; |
| 257 | | - hyperlink_to_uuid_with_mouseover(zUuid, "xin", "xout", rid); |
| 266 | + hyperlink_to_uuid(zUuid); |
| 258 | 267 | if( (tmFlags & TIMELINE_LEAFONLY)==0 ){ |
| 259 | 268 | if( nParent>1 ){ |
| 260 | 269 | azTag[nTag++] = "Merge"; |
| 261 | 270 | } |
| 262 | 271 | if( nPChild>1 ){ |
| | @@ -280,10 +289,37 @@ |
| 280 | 289 | int i; |
| 281 | 290 | for(i=0; i<nTag; i++){ |
| 282 | 291 | @ <b>%s(azTag[i])%s(i==nTag-1?"":",")</b> |
| 283 | 292 | } |
| 284 | 293 | } |
| 294 | + if( pGraph ){ |
| 295 | + int nParent = 0; |
| 296 | + int aParent[32]; |
| 297 | + const char *zBr; |
| 298 | + static Stmt qparent; |
| 299 | + static Stmt qbranch; |
| 300 | + db_static_prepare(&qparent, |
| 301 | + "SELECT pid FROM plink WHERE cid=:rid ORDER BY isprim DESC" |
| 302 | + ); |
| 303 | + db_static_prepare(&qbranch, |
| 304 | + "SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0 AND rid=:rid", |
| 305 | + TAG_BRANCH |
| 306 | + ); |
| 307 | + db_bind_int(&qparent, ":rid", rid); |
| 308 | + while( db_step(&qparent)==SQLITE_ROW && nParent<32 ){ |
| 309 | + aParent[nParent++] = db_column_int(&qparent, 0); |
| 310 | + } |
| 311 | + db_reset(&qparent); |
| 312 | + db_bind_int(&qbranch, ":rid", rid); |
| 313 | + if( db_step(&qbranch)==SQLITE_ROW ){ |
| 314 | + zBr = db_column_text(&qbranch, 0); |
| 315 | + }else{ |
| 316 | + zBr = "trunk"; |
| 317 | + } |
| 318 | + graph_add_row(pGraph, rid, isLeaf, nParent, aParent, zBr); |
| 319 | + db_reset(&qbranch); |
| 320 | + } |
| 285 | 321 | }else if( (tmFlags & TIMELINE_ARTID)!=0 ){ |
| 286 | 322 | hyperlink_to_uuid(zUuid); |
| 287 | 323 | } |
| 288 | 324 | db_column_blob(pQuery, commentColumn, &comment); |
| 289 | 325 | if( mxWikiLen>0 && blob_size(&comment)>mxWikiLen ){ |
| | @@ -310,12 +346,173 @@ |
| 310 | 346 | if( suppressCnt ){ |
| 311 | 347 | @ <tr><td><td><td> |
| 312 | 348 | @ <small><i>... %d(suppressCnt) similar |
| 313 | 349 | @ event%s(suppressCnt>1?"s":"") omitted.</i></small></tr> |
| 314 | 350 | suppressCnt = 0; |
| 351 | + } |
| 352 | + if( pGraph ){ |
| 353 | + graph_finish(pGraph); |
| 354 | + if( pGraph->nErr ){ |
| 355 | + graph_free(pGraph); |
| 356 | + pGraph = 0; |
| 357 | + }else{ |
| 358 | + @ <tr><td><td><div style="width:%d(pGraph->mxRail*20+30)px;"></div> |
| 359 | + } |
| 315 | 360 | } |
| 316 | 361 | @ </table> |
| 362 | + if( pGraph && pGraph->nErr==0 ){ |
| 363 | + GraphRow *pRow; |
| 364 | + int i; |
| 365 | + char cSep; |
| 366 | + @ <script type="text/JavaScript"> |
| 367 | + cgi_printf("var rowinfo = [\n"); |
| 368 | + for(pRow=pGraph->pFirst; pRow; pRow=pRow->pNext){ |
| 369 | + cgi_printf("{id:\"m%d\",r:%d,d:%d,mo:%d,mu:%d,u:%d,au:", |
| 370 | + pRow->rid, |
| 371 | + pRow->iRail, |
| 372 | + pRow->bDescender, |
| 373 | + pRow->mergeOut, |
| 374 | + pRow->mergeUpto, |
| 375 | + pRow->aiRaiser[pRow->iRail] |
| 376 | + ); |
| 377 | + cSep = '['; |
| 378 | + for(i=0; i<GR_MAX_RAIL; i++){ |
| 379 | + if( i==pRow->iRail ) continue; |
| 380 | + if( pRow->aiRaiser[i]>0 ){ |
| 381 | + cgi_printf("%c%d,%d", cSep, pGraph->railMap[i], pRow->aiRaiser[i]); |
| 382 | + cSep = ','; |
| 383 | + } |
| 384 | + } |
| 385 | + if( cSep=='[' ) cgi_printf("["); |
| 386 | + cgi_printf("],mi:"); |
| 387 | + cSep = '['; |
| 388 | + for(i=0; i<GR_MAX_RAIL; i++){ |
| 389 | + if( pRow->mergeIn & (1<<i) ){ |
| 390 | + cgi_printf("%c%d", cSep, pGraph->railMap[i]); |
| 391 | + cSep = ','; |
| 392 | + } |
| 393 | + } |
| 394 | + if( cSep=='[' ) cgi_printf("["); |
| 395 | + cgi_printf("]}%s", pRow->pNext ? ",\n" : "];\n"); |
| 396 | + } |
| 397 | + cgi_printf("var nrail = %d\n", pGraph->mxRail+1); |
| 398 | + graph_free(pGraph); |
| 399 | + @ var canvasDiv = document.getElementById("canvas"); |
| 400 | + @ function drawBox(color,x0,y0,x1,y1){ |
| 401 | + @ var n = document.createElement("div"); |
| 402 | + @ if( x0>x1 ){ var t=x0; x0=x1; x1=t; } |
| 403 | + @ if( y0>y1 ){ var t=y0; y0=y1; y1=t; } |
| 404 | + @ var w = x1-x0+1; |
| 405 | + @ var h = y1-y0+1; |
| 406 | + @ n.setAttribute("style", |
| 407 | + @ "position:absolute;"+ |
| 408 | + @ "left:"+x0+"px;"+ |
| 409 | + @ "top:"+y0+"px;"+ |
| 410 | + @ "width:"+w+"px;"+ |
| 411 | + @ "height:"+h+"px;"+ |
| 412 | + @ "background-color:"+color+";" |
| 413 | + @ ); |
| 414 | + @ canvasDiv.appendChild(n); |
| 415 | + @ } |
| 416 | + @ function absoluteY(id){ |
| 417 | + @ var obj = document.getElementById(id); |
| 418 | + @ if( !obj ) return; |
| 419 | + @ var top = 0; |
| 420 | + @ if( obj.offsetParent ){ |
| 421 | + @ do{ |
| 422 | + @ top += obj.offsetTop; |
| 423 | + @ }while( obj = obj.offsetParent ); |
| 424 | + @ } |
| 425 | + @ return top; |
| 426 | + @ } |
| 427 | + @ function absoluteX(id){ |
| 428 | + @ var obj = document.getElementById(id); |
| 429 | + @ if( !obj ) return; |
| 430 | + @ var left = 0; |
| 431 | + @ if( obj.offsetParent ){ |
| 432 | + @ do{ |
| 433 | + @ left += obj.offsetLeft; |
| 434 | + @ }while( obj = obj.offsetParent ); |
| 435 | + @ } |
| 436 | + @ return left; |
| 437 | + @ } |
| 438 | + @ function drawUpArrow(x,y0,y1){ |
| 439 | + @ drawBox("black",x,y0,x+1,y1); |
| 440 | + @ if( y0+8>=y1 ){ |
| 441 | + @ drawBox("black",x-1,y0+1,x+2,y0+2); |
| 442 | + @ drawBox("black",x-2,y0+3,x+3,y0+4); |
| 443 | + @ }else{ |
| 444 | + @ drawBox("black",x-1,y0+2,x+2,y0+4); |
| 445 | + @ drawBox("black",x-2,y0+5,x+3,y0+7); |
| 446 | + @ } |
| 447 | + @ } |
| 448 | + @ function drawThinArrow(y,xFrom,xTo){ |
| 449 | + @ if( xFrom<xTo ){ |
| 450 | + @ drawBox("black",xFrom,y,xTo,y); |
| 451 | + @ drawBox("black",xTo-4,y-1,xTo-2,y+1); |
| 452 | + @ if( xTo>xFrom-8 ) drawBox("black",xTo-6,y-2,xTo-5,y+2); |
| 453 | + @ }else{ |
| 454 | + @ drawBox("black",xTo,y,xFrom,y); |
| 455 | + @ drawBox("black",xTo+2,y-1,xTo+4,y+1); |
| 456 | + @ if( xTo+8<xFrom ) drawBox("black",xTo+5,y-2,xTo+6,y+2); |
| 457 | + @ } |
| 458 | + @ } |
| 459 | + @ function drawThinLine(x0,y0,x1,y1){ |
| 460 | + @ drawBox("black",x0,y0,x1,y1); |
| 461 | + @ } |
| 462 | + @ function drawNode(p, left, btm){ |
| 463 | + @ drawBox("black",p.x-5,p.y-5,p.x+6,p.y+6); |
| 464 | + @ drawBox("white",p.x-4,p.y-4,p.x+5,p.y+5); |
| 465 | + @ if( p.u>0 ){ |
| 466 | + @ var u = rowinfo[p.u-1]; |
| 467 | + @ drawUpArrow(p.x, u.y+6, p.y-5); |
| 468 | + @ } |
| 469 | + @ if( p.d ){ |
| 470 | + @ drawUpArrow(p.x, p.y+6, btm); |
| 471 | + @ } |
| 472 | + @ if( p.mo>=0 ){ |
| 473 | + @ var x1 = p.mo*20 + left; |
| 474 | + @ var y1 = p.y-3; |
| 475 | + @ var x0 = x1>p.x ? p.x+7 : p.x-6; |
| 476 | + @ var u = rowinfo[p.mu-1]; |
| 477 | + @ var y0 = u.y+5; |
| 478 | + @ drawThinLine(x0,y1,x1,y1); |
| 479 | + @ drawThinLine(x1,y0,x1,y1); |
| 480 | + @ } |
| 481 | + @ var n = p.au.length; |
| 482 | + @ for(var i=0; i<n; i+=2){ |
| 483 | + @ var x1 = p.au[i]*20 + left; |
| 484 | + @ var x0 = x1>p.x ? p.x+7 : p.x-6; |
| 485 | + @ drawBox("black",x0,p.y,x1,p.y+1); |
| 486 | + @ var u = rowinfo[p.au[i+1]-1]; |
| 487 | + @ drawUpArrow(x1, u.y+6, p.y); |
| 488 | + @ } |
| 489 | + @ for(var j in p.mi){ |
| 490 | + @ var y0 = p.y+5; |
| 491 | + @ var mx = p.mi[j]*20 + left; |
| 492 | + @ if( mx>p.x ){ |
| 493 | + @ drawThinArrow(y0,mx,p.x+5); |
| 494 | + @ }else{ |
| 495 | + @ drawThinArrow(y0,mx,p.x-5); |
| 496 | + @ } |
| 497 | + @ } |
| 498 | + @ } |
| 499 | + @ function renderGraph(){ |
| 500 | + @ var canvasY = absoluteY("canvas"); |
| 501 | + @ var left = absoluteX(rowinfo[0].id) - absoluteX("canvas") + 15; |
| 502 | + @ for(var i in rowinfo){ |
| 503 | + @ rowinfo[i].y = absoluteY(rowinfo[i].id) + 10 - canvasY; |
| 504 | + @ rowinfo[i].x = left + rowinfo[i].r*20; |
| 505 | + @ } |
| 506 | + @ var btm = rowinfo[rowinfo.length-1].y + 20; |
| 507 | + @ for(var i in rowinfo){ |
| 508 | + @ drawNode(rowinfo[i], left, btm); |
| 509 | + @ } |
| 510 | + @ } |
| 511 | + @ renderGraph(); |
| 512 | + @ </script> |
| 513 | + } |
| 317 | 514 | } |
| 318 | 515 | |
| 319 | 516 | /* |
| 320 | 517 | ** Create a temporary table suitable for storing timeline data. |
| 321 | 518 | */ |
| | @@ -461,13 +658,13 @@ |
| 461 | 658 | tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname='sym-%q'", zTagName); |
| 462 | 659 | }else{ |
| 463 | 660 | tagid = 0; |
| 464 | 661 | } |
| 465 | 662 | if( zType[0]=='a' ){ |
| 466 | | - tmFlags = TIMELINE_BRIEF; |
| 663 | + tmFlags = TIMELINE_BRIEF | TIMELINE_GRAPH; |
| 467 | 664 | }else{ |
| 468 | | - tmFlags = 0; |
| 665 | + tmFlags = TIMELINE_GRAPH; |
| 469 | 666 | } |
| 470 | 667 | |
| 471 | 668 | style_header("Timeline"); |
| 472 | 669 | login_anonymous_available(); |
| 473 | 670 | timeline_temp_table(); |
| | @@ -645,10 +842,11 @@ |
| 645 | 842 | if( zUser ){ |
| 646 | 843 | blob_appendf(&desc, " by user %h", zUser); |
| 647 | 844 | } |
| 648 | 845 | if( tagid>0 ){ |
| 649 | 846 | blob_appendf(&desc, " tagged with \"%h\"", zTagName); |
| 847 | + tmFlags &= ~TIMELINE_GRAPH; |
| 650 | 848 | } |
| 651 | 849 | if( zAfter ){ |
| 652 | 850 | blob_appendf(&desc, " occurring on or after %h.<br>", zAfter); |
| 653 | 851 | }else if( zBefore ){ |
| 654 | 852 | blob_appendf(&desc, " occurring on or before %h.<br>", zBefore); |
| | @@ -691,103 +889,10 @@ |
| 691 | 889 | db_prepare(&q, "SELECT * FROM timeline ORDER BY timestamp DESC"); |
| 692 | 890 | @ <h2>%b(&desc)</h2> |
| 693 | 891 | blob_reset(&desc); |
| 694 | 892 | www_print_timeline(&q, tmFlags, 0); |
| 695 | 893 | db_finalize(&q); |
| 696 | | - |
| 697 | | - @ <script> |
| 698 | | - @ var parentof = new Object(); |
| 699 | | - @ var childof = new Object(); |
| 700 | | - db_prepare(&q, "SELECT rid FROM seen"); |
| 701 | | - while( db_step(&q)==SQLITE_ROW ){ |
| 702 | | - int rid = db_column_int(&q, 0); |
| 703 | | - Stmt q2; |
| 704 | | - const char *zSep; |
| 705 | | - Blob *pOut = cgi_output_blob(); |
| 706 | | - |
| 707 | | - db_prepare(&q2, "SELECT pid FROM plink WHERE cid=%d", rid); |
| 708 | | - zSep = ""; |
| 709 | | - blob_appendf(pOut, "parentof[\"m%d\"] = [", rid); |
| 710 | | - while( db_step(&q2)==SQLITE_ROW ){ |
| 711 | | - int pid = db_column_int(&q2, 0); |
| 712 | | - blob_appendf(pOut, "%s\"m%d\"", zSep, pid); |
| 713 | | - zSep = ","; |
| 714 | | - } |
| 715 | | - db_finalize(&q2); |
| 716 | | - blob_appendf(pOut, "];\n"); |
| 717 | | - db_prepare(&q2, "SELECT cid FROM plink WHERE pid=%d", rid); |
| 718 | | - zSep = ""; |
| 719 | | - blob_appendf(pOut, "childof[\"m%d\"] = [", rid); |
| 720 | | - while( db_step(&q2)==SQLITE_ROW ){ |
| 721 | | - int pid = db_column_int(&q2, 0); |
| 722 | | - blob_appendf(pOut, "%s\"m%d\"", zSep, pid); |
| 723 | | - zSep = ","; |
| 724 | | - } |
| 725 | | - db_finalize(&q2); |
| 726 | | - blob_appendf(pOut, "];\n"); |
| 727 | | - } |
| 728 | | - db_finalize(&q); |
| 729 | | - @ function setall(value){ |
| 730 | | - @ for(var x in parentof){ |
| 731 | | - @ setone(x,value); |
| 732 | | - @ } |
| 733 | | - @ } |
| 734 | | - @ setall("#ffffff"); |
| 735 | | - @ function setone(id, clr){ |
| 736 | | - @ if( parentof[id]==null ) return 0; |
| 737 | | - @ var w = document.getElementById(id); |
| 738 | | - @ if( w.style.color==clr ){ |
| 739 | | - @ return 0 |
| 740 | | - @ }else{ |
| 741 | | - @ w.style.color = clr |
| 742 | | - @ return 1 |
| 743 | | - @ } |
| 744 | | - @ } |
| 745 | | - @ function xin(id) { |
| 746 | | - @ setall("#ffffff"); |
| 747 | | - @ setone(id,"#ff0000"); |
| 748 | | - @ set_children(id, "#b0b0b0"); |
| 749 | | - @ set_parents(id, "#b0b0b0"); |
| 750 | | - @ for(var x in parentof[id]){ |
| 751 | | - @ var pid = parentof[id][x] |
| 752 | | - @ var w = document.getElementById(pid); |
| 753 | | - @ if( w!=null ){ |
| 754 | | - @ w.style.color = "#000000"; |
| 755 | | - @ } |
| 756 | | - @ } |
| 757 | | - @ for(var x in childof[id]){ |
| 758 | | - @ var cid = childof[id][x] |
| 759 | | - @ var w = document.getElementById(cid); |
| 760 | | - @ if( w!=null ){ |
| 761 | | - @ w.style.color = "#000000"; |
| 762 | | - @ } |
| 763 | | - @ } |
| 764 | | - @ } |
| 765 | | - @ function xout(id) { |
| 766 | | - @ /* setall("#000000"); */ |
| 767 | | - @ } |
| 768 | | - @ function set_parents(id, clr){ |
| 769 | | - @ var plist = parentof[id]; |
| 770 | | - @ if( plist==null ) return; |
| 771 | | - @ for(var x in plist){ |
| 772 | | - @ var pid = plist[x]; |
| 773 | | - @ if( setone(pid,clr)==1 ){ |
| 774 | | - @ set_parents(pid,clr); |
| 775 | | - @ } |
| 776 | | - @ } |
| 777 | | - @ } |
| 778 | | - @ function set_children(id,clr){ |
| 779 | | - @ var clist = childof[id]; |
| 780 | | - @ if( clist==null ) return; |
| 781 | | - @ for(var x in clist){ |
| 782 | | - @ var cid = clist[x]; |
| 783 | | - @ if( setone(cid,clr)==1 ){ |
| 784 | | - @ set_children(cid,clr); |
| 785 | | - @ } |
| 786 | | - @ } |
| 787 | | - @ } |
| 788 | | - @ </script> |
| 789 | 894 | style_footer(); |
| 790 | 895 | } |
| 791 | 896 | |
| 792 | 897 | /* |
| 793 | 898 | ** The input query q selects various records. Print a human-readable |
| 794 | 899 | |