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