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