1 // VIEW 2 3 /** 4 * @class 5 */ 6 function FeatureTrack( config, refSeq, browserParams ) { 7 //config: object with: 8 // key: display text track name 9 // label: internal track name (no spaces, odd characters) 10 // baseUrl: base URL to use for resolving relative URLs 11 // contained in the track's configuration 12 // config: configuration info for this track 13 //refSeq: object with: 14 // name: refseq name 15 // start: refseq start 16 // end: refseq end 17 //browserParams: object with: 18 // changeCallback: function to call once JSON is loaded 19 // trackPadding: distance in px between tracks 20 // baseUrl: base URL for the URL in config 21 22 Track.call(this, config.label, config.key, 23 false, browserParams.changeCallback); 24 this.fields = {}; 25 this.refSeq = refSeq; 26 27 // TODO: this featureStore object should eventuallly be 28 // instantiated by Browser and passed into this constructor, not 29 // constructed here. 30 var storeclass = config.backendVersion == 0 ? SeqFeatureStore.NCList_v0 : SeqFeatureStore.NCList; 31 this.featureStore = new storeclass({ 32 urlTemplate: config.urlTemplate, 33 baseUrl: config.baseUrl, 34 refSeq: refSeq, 35 track: this 36 }); 37 38 // connect the store and track loadSuccess and loadFailed events 39 // to eachother 40 dojo.connect( this.featureStore, 'loadSuccess', this, 'loadSuccess' ); 41 dojo.connect( this.featureStore, 'loadFail', this, 'loadFail' ); 42 43 //number of histogram bins per block 44 this.numBins = 25; 45 this.histLabel = false; 46 this.padding = 5; 47 this.trackPadding = browserParams.trackPadding; 48 49 this.config = config; 50 } 51 52 FeatureTrack.prototype = new Track(""); 53 54 /** 55 * Request that the track load its data. The track will call its own 56 * loadSuccess() function when it is loaded. 57 */ 58 FeatureTrack.prototype.load = function() { 59 this.featureStore.load(); 60 }; 61 62 /** 63 * Mixin: Track.YScaleMixin. 64 */ 65 dojo.mixin( FeatureTrack.prototype, Track.YScaleMixin ); 66 67 FeatureTrack.prototype.loadSuccess = function(trackInfo, url) { 68 69 var defaultConfig = { 70 style: { 71 className: "feature2" 72 }, 73 scaleThresh: { 74 hist: 4, 75 label: 50, 76 subfeature: 80 77 }, 78 hooks: { 79 create: function(track, feat ) { 80 var featDiv; 81 var featUrl = track.featureUrl(feat); 82 if (featUrl) { 83 featDiv = document.createElement("a"); 84 featDiv.href = featUrl; 85 featDiv.target = "_new"; 86 } else { 87 featDiv = document.createElement("div"); 88 } 89 return featDiv; 90 } 91 }, 92 events: { 93 } 94 }; 95 96 if (! this.config.style.linkTemplate) { 97 defaultConfig.events.click = 98 function(track, elem, feat, event) { 99 alert( "clicked on feature\n" + 100 "start: " + (Number( feat.get('start') )+1) + 101 ", end: " + Number( feat.get('end') ) + 102 ", strand: " + feat.get('strand') + 103 ", label: " + feat.get('name') + 104 ", ID: " + feat.get('id') ); 105 }; 106 } 107 108 Util.deepUpdate(defaultConfig, this.config); 109 this.config = defaultConfig; 110 111 this.config.hooks.create = this.evalHook(this.config.hooks.create); 112 this.config.hooks.modify = this.evalHook(this.config.hooks.modify); 113 114 this.eventHandlers = {}; 115 for (var event in this.config.events) { 116 this.eventHandlers[event] = 117 this.wrapHandler(this.evalHook(this.config.events[event])); 118 } 119 120 this.labelScale = this.featureStore.density * this.config.scaleThresh.label; 121 122 this.setLoaded(); 123 }; 124 125 FeatureTrack.prototype.evalHook = function(hook) { 126 if (! ("string" == typeof hook)) return hook; 127 var result; 128 try { 129 result = eval("(" + hook + ")"); 130 } catch (e) { 131 console.log("eval failed for hook on track " 132 + this.name + ": " + hook); 133 } 134 return result; 135 }; 136 137 /** 138 * Make life easier for event handlers by handing them some things 139 */ 140 FeatureTrack.prototype.wrapHandler = function(handler) { 141 var track = this; 142 return function(event) { 143 event = event || window.event; 144 if (event.shiftKey) return; 145 var elem = (event.currentTarget || event.srcElement); 146 //depending on bubbling, we might get the subfeature here 147 //instead of the parent feature 148 if (!elem.feature) elem = elem.parentElement; 149 if (!elem.feature) return; //shouldn't happen; just bail if it does 150 handler(track, elem, elem.feature, event); 151 }; 152 }; 153 154 FeatureTrack.prototype.setViewInfo = function(genomeView, numBlocks, 155 trackDiv, labelDiv, 156 widthPct, widthPx, scale) { 157 Track.prototype.setViewInfo.apply(this, arguments ); 158 this.setLabel(this.key); 159 }; 160 161 /** 162 * Return an object with some statistics about the histograms we will 163 * draw for a given block size in base pairs. 164 * @private 165 */ 166 FeatureTrack.prototype._histDimensions = function( blockSizeBp ) { 167 168 // bases in each histogram bin that we're currently rendering 169 var bpPerBin = blockSizeBp / this.numBins; 170 var pxPerCount = 2; 171 var logScale = false; 172 var stats = this.featureStore.histograms.stats; 173 var statEntry; 174 for (var i = 0; i < stats.length; i++) { 175 if (stats[i].basesPerBin >= bpPerBin) { 176 //console.log("bpPerBin: " + bpPerBin + ", histStats bases: " + this.histStats[i].bases + ", mean/max: " + (this.histStats[i].mean / this.histStats[i].max)); 177 logScale = ((stats[i].mean / stats[i].max) < .01); 178 pxPerCount = 100 / (logScale ? 179 Math.log(stats[i].max) : 180 stats[i].max); 181 statEntry = stats[i]; 182 break; 183 } 184 } 185 186 return { 187 bpPerBin: bpPerBin, 188 pxPerCount: pxPerCount, 189 logScale: logScale, 190 stats: statEntry 191 }; 192 }; 193 194 FeatureTrack.prototype.fillHist = function(blockIndex, block, 195 leftBase, rightBase, 196 stripeWidth) { 197 198 var dims = this._histDimensions( Math.abs( rightBase - leftBase ) ); 199 200 var track = this; 201 var makeHistBlock = function(hist) { 202 var maxBin = 0; 203 for (var bin = 0; bin < track.numBins; bin++) { 204 if (typeof hist[bin] == 'number' && isFinite(hist[bin])) { 205 maxBin = Math.max(maxBin, hist[bin]); 206 } 207 } 208 var binDiv; 209 for (var bin = 0; bin < track.numBins; bin++) { 210 if (!(typeof hist[bin] == 'number' && isFinite(hist[bin]))) 211 continue; 212 binDiv = document.createElement("div"); 213 binDiv.className = track.config.style.className + "-hist";; 214 binDiv.style.cssText = 215 "left: " + ((bin / track.numBins) * 100) + "%; " 216 + "height: " 217 + (dims.pxPerCount * ( dims.logScale ? Math.log(hist[bin]) : hist[bin])) 218 + "px;" 219 + "bottom: " + track.trackPadding + "px;" 220 + "width: " + (((1 / track.numBins) * 100) - (100 / stripeWidth)) + "%;" 221 + (track.config.style.histCss ? 222 track.config.style.histCss : ""); 223 if (Util.is_ie6) binDiv.appendChild(document.createComment()); 224 block.appendChild(binDiv); 225 } 226 227 track.heightUpdate( dims.pxPerCount * ( dims.logScale ? Math.log(maxBin) : maxBin ), 228 blockIndex ); 229 track.makeHistogramYScale( Math.abs(rightBase-leftBase) ); 230 }; 231 232 // The histogramMeta array describes multiple levels of histogram detail, 233 // going from the finest (smallest number of bases per bin) to the 234 // coarsest (largest number of bases per bin). 235 // We want to use coarsest histogramMeta that's at least as fine as the 236 // one we're currently rendering. 237 // TODO: take into account that the histogramMeta chosen here might not 238 // fit neatly into the current histogram (e.g., if the current histogram 239 // is at 50,000 bases/bin, and we have server histograms at 20,000 240 // and 2,000 bases/bin, then we should choose the 2,000 histogramMeta 241 // rather than the 20,000) 242 var histogramMeta = this.featureStore.histograms.meta[0]; 243 for (var i = 0; i < this.featureStore.histograms.meta.length; i++) { 244 if (dims.bpPerBin >= this.featureStore.histograms.meta[i].basesPerBin) 245 histogramMeta = this.featureStore.histograms.meta[i]; 246 } 247 248 // number of bins in the server-supplied histogram for each current bin 249 var binCount = dims.bpPerBin / histogramMeta.basesPerBin; 250 // if the server-supplied histogram fits neatly into our current histogram, 251 if ((binCount > .9) 252 && 253 (Math.abs(binCount - Math.round(binCount)) < .0001)) { 254 // we can use the server-supplied counts 255 var firstServerBin = Math.floor(leftBase / histogramMeta.basesPerBin); 256 binCount = Math.round(binCount); 257 var histogram = []; 258 for (var bin = 0; bin < this.numBins; bin++) 259 histogram[bin] = 0; 260 261 histogramMeta.lazyArray.range( 262 firstServerBin, 263 firstServerBin + (binCount * this.numBins), 264 function(i, val) { 265 // this will count features that span the boundaries of 266 // the original histogram multiple times, so it's not 267 // perfectly quantitative. Hopefully it's still useful, though. 268 histogram[Math.floor((i - firstServerBin) / binCount)] += val; 269 }, 270 function() { 271 makeHistBlock(histogram); 272 } 273 ); 274 } else { 275 // make our own counts 276 this.featureStore.histogram( leftBase, rightBase, 277 this.numBins, makeHistBlock); 278 } 279 }; 280 281 FeatureTrack.prototype.endZoom = function(destScale, destBlockBases) { 282 this.clear(); 283 }; 284 285 FeatureTrack.prototype.updateStaticElements = function( coords ) { 286 Track.prototype.updateStaticElements.apply( this, arguments ); 287 this.updateYScaleFromViewDimensions( coords ); 288 this.updateFeatureLabelPositions( coords ); 289 }; 290 291 FeatureTrack.prototype.updateFeatureLabelPositions = function( coords ) { 292 if( ! 'x' in coords || this.scale < this.labelScale ) 293 return; 294 295 dojo.query( '.block', this.div ) 296 .forEach( function(block) { 297 // calculate the view left coord relative to the 298 // block left coord in units of pct of the block 299 // width 300 var viewLeft = 100 * ( coords.x - block.offsetLeft ) / block.offsetWidth + 2; 301 302 // if the view start is unknown, or is to the 303 // left of this block, we don't have to worry 304 // about adjusting the feature labels 305 if( ! viewLeft ) 306 return; 307 308 var blockWidth = block.endBase - block.startBase; 309 310 dojo.query('.feature',block) 311 .forEach( function(featDiv) { 312 if( ! featDiv.label ) return; 313 var labelDiv = featDiv.label; 314 var feature = featDiv.feature; 315 316 // get the feature start and end in terms of block width pct 317 var minLeft = parseInt( feature.get('start') ); 318 minLeft = 100 * (minLeft - block.startBase) / blockWidth; 319 var maxLeft = parseInt( feature.get('end') ); 320 maxLeft = 100 * ( (maxLeft - block.startBase) / blockWidth - labelDiv.offsetWidth / block.offsetWidth); 321 322 // move our label div to the view start if the start is between the feature start and end 323 labelDiv.style.left = Math.max( minLeft, Math.min( viewLeft, maxLeft ) ) + '%'; 324 325 },this); 326 },this); 327 }; 328 329 FeatureTrack.prototype.fillBlock = function(blockIndex, block, 330 leftBlock, rightBlock, 331 leftBase, rightBase, 332 scale, stripeWidth, 333 containerStart, containerEnd) { 334 335 // only update the label once for each block size 336 var blockBases = Math.abs( leftBase-rightBase ); 337 if( this._updatedLabelForBlockSize != blockBases ){ 338 if ( scale < (this.featureStore.density * this.config.scaleThresh.hist)) { 339 this.setLabel(this.key + "<br>per " + Util.addCommas( Math.round( blockBases / this.numBins)) + " bp"); 340 } else { 341 this.setLabel(this.key); 342 } 343 this._updatedLabelForBlockSize = blockBases; 344 } 345 346 //console.log("scale: %d, histScale: %d", scale, this.histScale); 347 if (this.featureStore.histograms && 348 (scale < (this.featureStore.density * this.config.scaleThresh.hist)) ) { 349 this.fillHist(blockIndex, block, leftBase, rightBase, stripeWidth, 350 containerStart, containerEnd); 351 } else { 352 353 // if we have transitioned to viewing features, delete the 354 // y-scale used for the histograms 355 if( this.yscale ) { 356 this._removeYScale(); 357 } 358 359 this.fillFeatures(blockIndex, block, leftBlock, rightBlock, 360 leftBase, rightBase, scale, 361 containerStart, containerEnd); 362 } 363 }; 364 365 /** 366 * Creates a Y-axis scale for the feature histogram. Must be run after 367 * the histogram bars are drawn, because it sometimes must use the 368 * track height to calculate the max value if there are no explicit 369 * histogram stats. 370 * @param {Number} blockSizeBp the size of the blocks in base pairs. 371 * Necessary for calculating histogram stats. 372 */ 373 FeatureTrack.prototype.makeHistogramYScale = function( blockSizeBp ) { 374 var dims = this._histDimensions( blockSizeBp); 375 if( dims.logScale ) { 376 console.error("Log histogram scale axis labels not yet implemented."); 377 return; 378 } 379 var maxval = dims.stats ? dims.stats.max : this.height/dims.pxPerCount; 380 maxval = dims.logScale ? log(maxval) : maxval; 381 382 // if we have a scale, and it has the same characteristics 383 // (including pixel height), don't redraw it. 384 if( this.yscale && this.yscale_params 385 && this.yscale_params.maxval == maxval 386 && this.yscale_params.height == this.height 387 && this.yscale_params.blockbp == blockSizeBp 388 ) { 389 return; 390 } else { 391 this._removeYScale(); 392 this.makeYScale({ min: 0, max: maxval }); 393 this.yscale_params = { 394 height: this.height, 395 blockbp: blockSizeBp, 396 maxval: maxval 397 }; 398 } 399 }; 400 401 /** 402 * Delete the Y-axis scale if present. 403 * @private 404 */ 405 FeatureTrack.prototype._removeYScale = function() { 406 if( !this.yscale ) 407 return; 408 this.yscale.parentNode.removeChild( this.yscale ); 409 delete this.yscale_params; 410 delete this.yscale; 411 }; 412 413 FeatureTrack.prototype.cleanupBlock = function(block) { 414 if (block && block.featureLayout) block.featureLayout.cleanup(); 415 }; 416 417 /** 418 * Called when sourceBlock gets deleted. Any child features of 419 * sourceBlock that extend onto destBlock should get moved onto 420 * destBlock. 421 */ 422 FeatureTrack.prototype.transfer = function(sourceBlock, destBlock, scale, 423 containerStart, containerEnd) { 424 425 if (!(sourceBlock && destBlock)) return; 426 if (!sourceBlock.featureLayout) return; 427 428 var destLeft = destBlock.startBase; 429 var destRight = destBlock.endBase; 430 var blockWidth = destRight - destLeft; 431 var sourceSlot; 432 433 var overlaps = (sourceBlock.startBase < destBlock.startBase) 434 ? sourceBlock.featureLayout.rightOverlaps 435 : sourceBlock.featureLayout.leftOverlaps; 436 437 for (var i = 0; i < overlaps.length; i++) { 438 //if the feature overlaps destBlock, 439 //move to destBlock & re-position 440 sourceSlot = sourceBlock.featureNodes[overlaps[i].id]; 441 if (sourceSlot && ("label" in sourceSlot)) { 442 sourceSlot.label.parentNode.removeChild(sourceSlot.label); 443 } 444 if (sourceSlot && sourceSlot.feature) { 445 if ( sourceSlot.layoutEnd > destLeft 446 && sourceSlot.feature.get('start') < destRight ) { 447 448 sourceBlock.removeChild(sourceSlot); 449 delete sourceBlock.featureNodes[overlaps[i].id]; 450 451 var featDiv = 452 this.renderFeature(sourceSlot.feature, overlaps[i].id, 453 destBlock, scale, 454 containerStart, containerEnd); 455 destBlock.appendChild(featDiv); 456 } 457 } 458 } 459 }; 460 461 FeatureTrack.prototype.fillFeatures = function(blockIndex, block, 462 leftBlock, rightBlock, 463 leftBase, rightBase, scale, 464 containerStart, containerEnd) { 465 //arguments: 466 //block: div to be filled with info 467 //leftBlock: div to the left of the block to be filled 468 //rightBlock: div to the right of the block to be filled 469 //leftBase: starting base of the block 470 //rightBase: ending base of the block 471 //scale: pixels per base at the current zoom level 472 //containerStart: don't make HTML elements extend further left than this 473 //containerEnd: don't make HTML elements extend further right than this 474 //0-based 475 476 this.scale = scale; 477 478 var layouter = new Layout(leftBase, rightBase); 479 block.featureLayout = layouter; 480 block.featureNodes = {}; 481 block.style.backgroundColor = "#ddd"; 482 483 //are we filling right-to-left (true) or left-to-right (false)? 484 var goLeft = false; 485 if (leftBlock && leftBlock.featureLayout) { 486 leftBlock.featureLayout.setRightLayout(layouter); 487 layouter.setLeftLayout(leftBlock.featureLayout); 488 } 489 if (rightBlock && rightBlock.featureLayout) { 490 rightBlock.featureLayout.setLeftLayout(layouter); 491 layouter.setRightLayout(rightBlock.featureLayout); 492 goLeft = true; 493 } 494 495 //determine the glyph height, arrowhead width, label text dimensions, etc. 496 if (!this.haveMeasurements) { 497 this.measureStyles(); 498 this.haveMeasurements = true; 499 } 500 501 var curTrack = this; 502 var featCallback = function(feature, path) { 503 //uniqueId is a stringification of the path in the NCList where 504 //the feature lives; it's unique across the top-level NCList 505 //(the top-level NCList covers a track/chromosome combination) 506 var uniqueId = path.join(","); 507 //console.log("ID " + uniqueId + (layouter.hasSeen(uniqueId) ? " (seen)" : " (new)")); 508 if (layouter.hasSeen(uniqueId)) { 509 //console.log("this layouter has seen " + uniqueId); 510 return; 511 } 512 var featDiv = 513 curTrack.renderFeature(feature, uniqueId, block, scale, 514 containerStart, containerEnd); 515 block.appendChild(featDiv); 516 }; 517 518 var startBase = goLeft ? rightBase : leftBase; 519 var endBase = goLeft ? leftBase : rightBase; 520 521 this.featureStore.iterate(startBase, endBase, featCallback, 522 function () { 523 block.style.backgroundColor = ""; 524 curTrack.heightUpdate(layouter.totalHeight, 525 blockIndex); 526 }); 527 }; 528 529 FeatureTrack.prototype.measureStyles = function() { 530 //determine dimensions of labels (height, per-character width) 531 var heightTest = document.createElement("div"); 532 heightTest.className = "feature-label"; 533 heightTest.style.height = "auto"; 534 heightTest.style.visibility = "hidden"; 535 heightTest.appendChild(document.createTextNode("1234567890")); 536 document.body.appendChild(heightTest); 537 this.nameHeight = heightTest.clientHeight; 538 this.nameWidth = heightTest.clientWidth / 10; 539 document.body.removeChild(heightTest); 540 541 //measure the height of glyphs 542 var glyphBox; 543 heightTest = document.createElement("div"); 544 //cover all the bases: stranded or not, phase or not 545 heightTest.className = 546 this.config.style.className 547 + " plus-" + this.config.style.className 548 + " plus-" + this.config.style.className + "1"; 549 if (this.config.style.featureCss) 550 heightTest.style.cssText = this.config.style.featureCss; 551 heightTest.style.visibility = "hidden"; 552 if (Util.is_ie6) heightTest.appendChild(document.createComment("foo")); 553 document.body.appendChild(heightTest); 554 glyphBox = dojo.marginBox(heightTest); 555 this.glyphHeight = Math.round(glyphBox.h + 2); 556 this.padding += glyphBox.w; 557 document.body.removeChild(heightTest); 558 559 //determine the width of the arrowhead, if any 560 if (this.config.style.arrowheadClass) { 561 var ah = document.createElement("div"); 562 ah.className = "plus-" + this.config.style.arrowheadClass; 563 if (Util.is_ie6) ah.appendChild(document.createComment("foo")); 564 document.body.appendChild(ah); 565 glyphBox = dojo.marginBox(ah); 566 this.plusArrowWidth = glyphBox.w; 567 ah.className = "minus-" + this.config.style.arrowheadClass; 568 glyphBox = dojo.marginBox(ah); 569 this.minusArrowWidth = glyphBox.w; 570 document.body.removeChild(ah); 571 } 572 }; 573 574 FeatureTrack.prototype.renderFeature = function(feature, uniqueId, block, scale, 575 containerStart, containerEnd) { 576 //featureStart and featureEnd indicate how far left or right 577 //the feature extends in bp space, including labels 578 //and arrowheads if applicable 579 580 var featureEnd = feature.get('end'); 581 var featureStart = feature.get('start'); 582 if( typeof featureEnd == 'string' ) 583 featureEnd = parseInt(featureEnd); 584 if( typeof featureStart == 'string' ) 585 featureStart = parseInt(featureStart); 586 587 588 var levelHeight = this.glyphHeight + 2; 589 590 // if the label extends beyond the feature, use the 591 // label end position as the end position for layout 592 var name = feature.get('name'); 593 if (name && (scale > this.labelScale)) { 594 featureEnd = Math.max(featureEnd, 595 featureStart + ((name ? name.length : 0) 596 * (this.nameWidth / scale) ) ); 597 levelHeight += this.nameHeight; 598 } 599 featureEnd += Math.max(1, this.padding / scale); 600 601 var top = block.featureLayout.addRect(uniqueId, 602 featureStart, 603 featureEnd, 604 levelHeight); 605 606 var featDiv = this.config.hooks.create(this, feature ); 607 for (var event in this.eventHandlers) { 608 featDiv["on" + event] = this.eventHandlers[event]; 609 } 610 featDiv.feature = feature; 611 featDiv.layoutEnd = featureEnd; 612 featDiv.className = (featDiv.className ? featDiv.className + " " : "") + "feature"; 613 614 block.featureNodes[uniqueId] = featDiv; 615 616 var strand = feature.get('strand'); 617 switch (strand) { 618 case 1: 619 case '+': 620 featDiv.className = featDiv.className + " plus-" + this.config.style.className; break; 621 case -1: 622 case '-': 623 featDiv.className = featDiv.className + " minus-" + this.config.style.className; break; 624 default: 625 featDiv.className = featDiv.className + " " + this.config.style.className; break; 626 } 627 628 var phase = feature.get('phase'); 629 if ((phase !== null) && (phase !== undefined)) 630 featDiv.className = featDiv.className + " " + featDiv.className + "_phase" + phase; 631 632 // Since some browsers don't deal well with the situation where 633 // the feature goes way, way offscreen, we truncate the feature 634 // to exist betwen containerStart and containerEnd. 635 // To make sure the truncated end of the feature never gets shown, 636 // we'll destroy and re-create the feature (with updated truncated 637 // boundaries) in the transfer method. 638 var displayStart = Math.max( feature.get('start'), containerStart ); 639 var displayEnd = Math.min( feature.get('end'), containerEnd ); 640 var minFeatWidth = 1; 641 var blockWidth = block.endBase - block.startBase; 642 var featwidth = Math.max(minFeatWidth, (100 * ((displayEnd - displayStart) / blockWidth))); 643 featDiv.style.cssText = 644 "left:" + (100 * (displayStart - block.startBase) / blockWidth) + "%;" 645 + "top:" + top + "px;" 646 + " width:" + featwidth + "%;" 647 + (this.config.style.featureCss ? this.config.style.featureCss : ""); 648 649 if ( this.config.style.arrowheadClass ) { 650 var ah = document.createElement("div"); 651 var featwidth_px = featwidth/100*blockWidth*scale; 652 switch (strand) { 653 case 1: 654 case '+': 655 if( featwidth_px > this.plusArrowWidth*1.1 ) { 656 ah.className = "plus-" + this.config.style.arrowheadClass; 657 ah.style.cssText = "position: absolute; right: 0px; top: 0px; z-index: 100;"; 658 featDiv.appendChild(ah); 659 } 660 break; 661 case -1: 662 case '-': 663 if( featwidth_px > this.minusArrowWidth*1.1 ) { 664 ah.className = "minus-" + this.config.style.arrowheadClass; 665 ah.style.cssText = 666 "position: absolute; left: 0px; top: 0px; z-index: 100;"; 667 featDiv.appendChild(ah); 668 } 669 break; 670 } 671 } 672 673 if (name && (scale > this.labelScale)) { 674 var labelDiv; 675 var featUrl = this.featureUrl(feature); 676 if (featUrl) { 677 labelDiv = document.createElement("a"); 678 labelDiv.href = featUrl; 679 labelDiv.target = featDiv.target; 680 } else { 681 labelDiv = document.createElement("div"); 682 } 683 for (event in this.eventHandlers) { 684 labelDiv["on" + event] = this.eventHandlers[event]; 685 } 686 687 labelDiv.className = "feature-label"; 688 labelDiv.appendChild(document.createTextNode(name)); 689 labelDiv.style.top = (top + this.glyphHeight) + "px"; 690 labelDiv.style.left = (100 * (featureStart - block.startBase) / blockWidth)+'%'; 691 featDiv.label = labelDiv; 692 labelDiv.feature = feature; 693 block.appendChild(labelDiv); 694 } 695 696 if( featwidth > minFeatWidth ) { 697 var subfeatures = feature.get('subfeatures'); 698 if( subfeatures ) { 699 for (var i = 0; i < subfeatures.length; i++) { 700 this.renderSubfeature(feature, featDiv, 701 subfeatures[i], 702 displayStart, displayEnd); 703 } 704 } 705 } 706 707 if (this.config.hooks.modify) { 708 this.config.hooks.modify(this, feature, featDiv); 709 } 710 711 //ie6 doesn't respect the height style if the div is empty 712 if (Util.is_ie6) featDiv.appendChild(document.createComment()); 713 //TODO: handle event-handler-related IE leaks 714 return featDiv; 715 }; 716 717 FeatureTrack.prototype.featureUrl = function(feature) { 718 var urlValid = true; 719 if (this.config.style.linkTemplate) { 720 var href = this.config.style.linkTemplate.replace( 721 /\{([^}]+)\}/g, 722 function(match, group) { 723 var val = feature.get( group.toLowerCase() ); 724 if (val !== undefined) 725 return val; 726 else 727 urlValid = false; 728 return 0; 729 }); 730 if( urlValid ) 731 return href; 732 } 733 return undefined; 734 }; 735 736 FeatureTrack.prototype.renderSubfeature = function(feature, featDiv, subfeature, 737 displayStart, displayEnd) { 738 var subStart = subfeature.get('start'); 739 var subEnd = subfeature.get('end'); 740 var featLength = displayEnd - displayStart; 741 742 var subDiv = document.createElement("div"); 743 744 if( this.config.style.subfeatureClasses ) { 745 var type = subfeature.get('type'); 746 subDiv.className = this.config.style.subfeatureClasses[type] || this.config.style.className + '-' + type; 747 switch ( subfeature.get('strand') ) { 748 case 1: 749 case '+': 750 subDiv.className += " plus-" + subDiv.className; break; 751 case -1: 752 case '-': 753 subDiv.className += " minus-" + subDiv.className; break; 754 } 755 } 756 757 // if the feature has been truncated to where it doesn't cover 758 // this subfeature anymore, just skip this subfeature 759 if ((subEnd <= displayStart) || (subStart >= displayEnd)) return; 760 761 if (Util.is_ie6) subDiv.appendChild(document.createComment()); 762 subDiv.style.cssText = 763 "left: " + (100 * ((subStart - displayStart) / featLength)) + "%;" 764 + "top: 0px;" 765 + "width: " + (100 * ((subEnd - subStart) / featLength)) + "%;"; 766 featDiv.appendChild(subDiv); 767 }; 768 769 /* 770 771 Copyright (c) 2007-2010 The Evolutionary Software Foundation 772 773 Created by Mitchell Skinner <mitch_skinner@berkeley.edu> 774 775 This package and its accompanying libraries are free software; you can 776 redistribute it and/or modify it under the terms of the LGPL (either 777 version 2.1, or at your option, any later version) or the Artistic 778 License 2.0. Refer to LICENSE for the full license text. 779 780 */ 781