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