001package com.hfg.graphics;
002
003
004import java.awt.*;
005import java.awt.Frame;
006import java.awt.font.FontRenderContext;
007import java.awt.font.TextLayout;
008import java.awt.image.BufferedImage;
009import java.awt.geom.AffineTransform;
010import java.util.*;
011import java.util.List;
012import java.io.*;
013
014import com.hfg.bio.Strand;
015import com.hfg.html.*;
016import com.hfg.html.attribute.Shape;
017import com.hfg.javascript.PopupMenuJS;
018import com.hfg.javascript.TooltipJS;
019import com.hfg.util.collection.CollectionUtil;
020import com.hfg.image.ImageIO_Util;
021import com.hfg.util.mime.MimeType;
022import com.hfg.svg.*;
023import com.hfg.xml.XMLNode;
024
025import javax.imageio.ImageIO;
026
027//------------------------------------------------------------------------------
028/**
029 * An evidence Track of data to display on genomic coordinates.
030 *
031 * @author J. Alex Taylor, hairyfatguy.com
032 */
033//------------------------------------------------------------------------------
034// com.hfg XML/HTML Coding Library
035//
036// This library is free software; you can redistribute it and/or
037// modify it under the terms of the GNU Lesser General Public
038// License as published by the Free Software Foundation; either
039// version 2.1 of the License, or (at your option) any later version.
040//
041// This library is distributed in the hope that it will be useful,
042// but WITHOUT ANY WARRANTY; without even the implied warranty of
043// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
044// Lesser General Public License for more details.
045//
046// You should have received a copy of the GNU Lesser General Public
047// License along with this library; if not, write to the Free Software
048// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
049//
050// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
051// jataylor@hairyfatguy.com
052//------------------------------------------------------------------------------
053// TODO:
054//  - Vertically center in the track.
055
056public class Track2D extends Rectangle implements Comparable<Track2D>
057{
058
059   //##########################################################################
060   // PRIVATE FIELDS
061   //##########################################################################
062
063   private String                mName;
064   private String                mDescription;
065   private String                mURL;
066   private List<Gene2D>          mForwardGenes;
067   private List<Gene2D>          mReverseGenes;
068   private List<HighlightRegion> mHighlightRegions;
069   private Boolean               mShowId;
070   private boolean               mShowSpeciesInTooltip  = true;
071   private boolean               mShowLocationInTooltip = true;
072   private ColorScale            mPctIdColorScale;
073   
074   private Color  mBackgroundColor = Color.white;
075//   private Color  mForwardStrandColor = Color.blue;
076//   private Color  mReverseStrandColor = new Color(150, 150, 255);
077   private Color  mAxisColor  = Color.gray;
078   private Color  mLabelColor = Color.black;
079   private Color  mHighlightColor = new Color(150, 250, 150);
080   private Font    mLabelFont = sDefaultLabelFont;
081
082   private int    mXAxisStartValue;
083   private int    mXAxisEndValue;
084   private double mXScalingFactor;
085   private Map<Gene2D, String>    mForwardLineMap;
086   private Map<Gene2D, String>    mReverseLineMap;
087   private int    mNumForwardLines;
088   private int    mNumReverseLines;
089   private List<Integer> mForwardLineHeights;
090   private List<Integer> mReverseLineHeights;
091
092   private int    mImageWidthInPixels = 700;
093   private int    mForwardImageHeightInPixels;
094   private int    mReverseImageHeightInPixels;
095   private int    mLineHeightInPixels = 10;
096   private int    mLinePaddingInPixels = 3;
097   private int    mDescriptionWidthInPixels = 120;
098   private int    mMinimumBufferInPixels = 3;
099   private int    mRightBufferInPixels = 60;
100
101   private int    mXAxisImageHeightInPixels;
102
103//   private Font      mFont       = Font.decode("Arial-PLAIN-10");
104
105
106   // Calculated
107   private int    mMajorTickStep;
108   private int    mMinorTickStep;
109
110   private ImageMap mForwardImageMap = new ImageMap();
111   private ImageMap mReverseImageMap = new ImageMap();
112   private Script   mJavascript = new Script(MimeType.TEXT_JAVASCRIPT);
113
114   private static FontRenderContext sFRC;
115//   private static Font sDefaultLabelFont = Font.decode("Courier-PLAIN-9");
116   private static Font sDefaultLabelFont = new Font("Monospaced", Font.PLAIN, 8);
117   static
118   {
119      Frame frame = new Frame();
120      frame.addNotify();
121      Image image =frame.createImage(1, 1);
122      sFRC = ((Graphics2D) image.getGraphics()).getFontRenderContext();
123   }
124
125
126   //##########################################################################
127   // CONSTRUCTORS
128   //##########################################################################
129
130   //--------------------------------------------------------------------------
131   public Track2D()
132   {
133   }
134
135
136   //--------------------------------------------------------------------------
137   public Track2D(String inName)
138   {
139      this();
140      mName = inName;
141   }
142
143
144   //##########################################################################
145   // PUBLIC METHODS
146   //##########################################################################
147
148
149   //--------------------------------------------------------------------------
150   @Override
151   public boolean equals(Object o2)
152   {
153      return (o2 instanceof Track2D && compareTo((Track2D)o2) == 0);
154   }
155
156   //--------------------------------------------------------------------------
157   public int compareTo(Track2D inTrack2)
158   {
159      int returnValue = 0;
160
161      if (this != inTrack2)
162      {
163         returnValue = this.getName().compareTo(inTrack2.getName());
164      }
165
166      return returnValue;
167   }
168
169
170   //--------------------------------------------------------------------------
171   public boolean hasGenesOnStrand(Strand inStrand)
172   {
173      return areAnyGenesInDisplayRegion(Strand.FORWARD == inStrand ? mForwardGenes : mReverseGenes);
174   }
175
176
177   //--------------------------------------------------------------------------
178   public void addHighlightRegion(int start, int end)
179   {
180      if (null == mHighlightRegions)
181      {
182         mHighlightRegions = new ArrayList<HighlightRegion>();
183      }
184
185      mHighlightRegions.add(new HighlightRegion(start, end));
186   }
187
188   //--------------------------------------------------------------------------
189   public void setGenes(Collection<Gene2D> inValues)
190   {
191      if (mForwardGenes != null) mForwardGenes.clear();
192      if (mReverseGenes != null) mReverseGenes.clear();
193      if (CollectionUtil.hasValues(inValues))
194      {
195         for (Gene2D gene : inValues)
196         {
197            addGene(gene);
198         }
199      }
200   }
201
202   //--------------------------------------------------------------------------
203   public void addGene(Gene2D inValue)
204   {
205      if (inValue.getStrand() == Strand.FORWARD
206          || (null == inValue.getStrand()
207              && inValue.getEndLocation() > inValue.getStartLocation()))
208      {
209         if (null == mForwardGenes)
210         {
211            mForwardGenes = new ArrayList<Gene2D>();
212         }
213
214         mForwardGenes.add(inValue);
215      }
216      else
217      {
218         if (null == mReverseGenes)
219         {
220            mReverseGenes = new ArrayList<Gene2D>();
221         }
222
223         mReverseGenes.add(inValue);
224      }
225   }
226
227   //--------------------------------------------------------------------------
228   public Collection<Gene2D> getGenes()
229   {
230      Collection<Gene2D> genes = new ArrayList<Gene2D>();
231      if (CollectionUtil.hasValues(mForwardGenes)) genes.addAll(mForwardGenes);
232      if (CollectionUtil.hasValues(mReverseGenes)) genes.addAll(mReverseGenes);
233
234      return genes;
235   }
236
237   //--------------------------------------------------------------------------
238   public void setName(String inValue)
239   {
240      mName = inValue;
241   }
242
243   //--------------------------------------------------------------------------
244   public String getName()
245   {
246      return mName;
247   }
248
249
250   //--------------------------------------------------------------------------
251   public void setURL(String inValue)
252   {
253      mURL = inValue;
254   }
255
256   //--------------------------------------------------------------------------
257   public void setDescription(String inValue)
258   {
259      mDescription = inValue;
260   }
261
262   //--------------------------------------------------------------------------
263   public String getDescription()
264   {
265      return mDescription;
266   }
267
268   //--------------------------------------------------------------------------
269   public void setBackgroundColor(Color inValue)
270   {
271      mBackgroundColor = inValue;
272   }
273
274   //--------------------------------------------------------------------------
275   public Color getBackgroundColor()
276   {
277      return mBackgroundColor;
278   }
279
280   //--------------------------------------------------------------------------
281   public void setPctIdColorScale(ColorScale inValue)
282   {
283      mPctIdColorScale = inValue;
284   }
285
286/*
287   //--------------------------------------------------------------------------
288   public void setForwardStrandColor(Color inValue)
289   {
290      mForwardStrandColor = inValue;
291   }
292
293   //--------------------------------------------------------------------------
294   public Color getForwardStrandColor()
295   {
296      return mForwardStrandColor;
297   }
298
299   //--------------------------------------------------------------------------
300   public void setReverseStrandColor(Color inValue)
301   {
302      mReverseStrandColor = inValue;
303   }
304
305   //--------------------------------------------------------------------------
306   public Color getReverseStrandColor()
307   {
308      return mReverseStrandColor;
309   }
310*/
311   //--------------------------------------------------------------------------
312   public void setLabelColor(Color inValue)
313   {
314      mLabelColor = inValue;
315   }
316
317   //--------------------------------------------------------------------------
318   public Color getLabelColor()
319   {
320      return mLabelColor;
321   }
322
323   //--------------------------------------------------------------------------
324   public void setAxisColor(Color inValue)
325   {
326      mAxisColor = inValue;
327   }
328
329   //--------------------------------------------------------------------------
330   public Color getAxisColor()
331   {
332      return mLabelColor;
333   }
334
335   //--------------------------------------------------------------------------
336   public void setHighlightColor(Color inValue)
337   {
338      mHighlightColor = inValue;
339   }
340
341   //--------------------------------------------------------------------------
342   public Color getHighlightColor()
343   {
344      return mHighlightColor;
345   }
346
347   //--------------------------------------------------------------------------
348   public void setLabelFont(Font inValue)
349   {
350      mLabelFont = inValue;
351   }
352
353   //--------------------------------------------------------------------------
354   public void setXAxisStartValue(int inValue)
355   {
356      mXAxisStartValue = inValue;
357   }
358
359   //--------------------------------------------------------------------------
360   public void setXAxisEndValue(int inValue)
361   {
362      mXAxisEndValue = inValue;
363   }
364
365   //--------------------------------------------------------------------------
366   public int getForwardStrandImageHeight()
367   {
368      return mForwardImageHeightInPixels;
369   }
370
371   //--------------------------------------------------------------------------
372   public int getReverseStrandImageHeight()
373   {
374      return mReverseImageHeightInPixels;
375   }
376
377   //--------------------------------------------------------------------------
378   public void setImageWidth(int inValue)
379   {
380      mImageWidthInPixels = inValue;
381   }
382
383   //--------------------------------------------------------------------------
384   public int getImageWidth()
385   {
386      return mImageWidthInPixels;
387   }
388
389   //--------------------------------------------------------------------------
390   public Script getJavascript()
391   {
392      return mJavascript;
393   }
394
395   //--------------------------------------------------------------------------
396   public ImageMap getImageMap(Strand inStrand)
397   {
398      return (inStrand == Strand.FORWARD ? mForwardImageMap : mReverseImageMap);
399   }
400
401   //--------------------------------------------------------------------------
402   public void setShowId(boolean inValue)
403   {
404      mShowId = inValue;
405   }
406
407   //--------------------------------------------------------------------------
408   public XMLNode toSVG(Strand inStrand)
409   {
410      initialize(inStrand);
411
412      SVG svg = new SVG();
413      svg.setFont(mLabelFont);
414      svg.addStyle("shape-rendering:crispEdges"); // Turn off anti-aliasing
415
416      SvgGroup group = svg.addGroup();
417      group.setAttribute("transform", "scale(1)");
418
419
420      int imageHeightInPixels = (inStrand == Strand.FORWARD ? mForwardImageHeightInPixels :
421                                                              mReverseImageHeightInPixels);
422      // Paint the background
423      SvgRect background = group.addRect(new Rectangle(new Point(0, 0),
424                                                       new Dimension(mImageWidthInPixels, imageHeightInPixels)));
425      background.setFill(getBackgroundColor());
426      // background.setStyle("stroke-width: 0.5; stroke: red;"); // For debugging
427
428      // Draw the line separating the description from the alignment
429      SvgRect separator = group.addRect(new Rectangle(new Point(mDescriptionWidthInPixels - (2 * mMinimumBufferInPixels), 0),
430                                                      new Dimension(mMinimumBufferInPixels, imageHeightInPixels - 2)));
431      separator.setFill(mAxisColor);
432
433      // Write the Track name
434      Rectangle textBoundBox = TextUtil.getStringRect(mName, mLabelFont);
435      int xOffset = (int) (mDescriptionWidthInPixels - textBoundBox.getWidth() -  (2 * mLinePaddingInPixels) - mMinimumBufferInPixels);
436      int yOffset = (int) (imageHeightInPixels/2 + textBoundBox.getHeight()/2);
437      group.addText(mName, mLabelFont, new Point(xOffset, yOffset));
438
439      // Add highlights
440      SvgGroup highlightGroup = generateHighlightRegions(imageHeightInPixels);
441      if (highlightGroup != null) group.addSubtag(highlightGroup);
442
443      // Add genes
444      group.addSubtag(generateGenes(inStrand));
445
446      return svg;
447   }
448/*
449   //--------------------------------------------------------------------------
450   public SVG writeImageAsSvg(Strand inStrand)
451   {
452      if (mLabelFont == sDefaultLabelFont)
453      {
454         setLabelFont(Font.decode("Courier-PLAIN-8"));
455      }
456
457      initialize(inStrand);
458
459      int imageHeightInPixels = (inStrand == Strand.FORWARD ? mForwardImageHeightInPixels :
460            mReverseImageHeightInPixels);
461
462      SvgGraphics2D g2 = new SvgGraphics2D();
463      draw(g2, inStrand);
464      SVG svgTag = g2.getRootTag().setWidth(mImageWidthInPixels).setHeight(imageHeightInPixels);
465      svgTag.addStyle("shape-rendering:crispEdges"); // Turn off anti-aliasing
466      return svgTag;
467   }
468*/
469
470   //--------------------------------------------------------------------------
471   public SVG getXAxisAsSVG()
472   {
473      mXAxisImageHeightInPixels = (int) TextUtil.getStringRect(mXAxisEndValue + "", mLabelFont).getWidth() + 7;
474
475      SVG svg = new SVG();
476      svg.setFont(mLabelFont);
477      svg.addStyle("shape-rendering:crispEdges"); // Turn off anti-aliasing
478
479      SvgGroup group = svg.addGroup();
480      group.setAttribute("transform", "scale(1)");
481
482
483      // Paint the background
484      SvgRect background = group.addRect(new Rectangle(new Point(0, 0),
485                                                       new Dimension(mImageWidthInPixels, mXAxisImageHeightInPixels)));
486      background.setFill(getBackgroundColor());
487      // background.setStyle("stroke-width: 0.5; stroke: red;"); // For debugging
488
489      // Draw the x-axis line
490      SvgLine axisLine = group.addLine(new Point(mDescriptionWidthInPixels, 1),
491                                       new Point(mImageWidthInPixels - mRightBufferInPixels, 1));
492//      axisLine.setFill(mAxisColor);
493
494      // Add highlights
495      SvgGroup highlightGroup = generateHighlightRegions(mXAxisImageHeightInPixels);
496      if (highlightGroup != null) group.addSubtag(highlightGroup);
497/*
498      // Write the Track name
499      Rectangle textBoundBox = TextUtil.getStringRect(mName, mLabelFont);
500      int xOffset = (int) (mDescriptionWidthInPixels - textBoundBox.getWidth() -  (2 * mLinePaddingInPixels) - mMinimumBufferInPixels);
501      int yOffset = (int) (imageHeightInPixels/2 + textBoundBox.getHeight()/2);
502      group.addText(mName, mLabelFont, new Point(xOffset, yOffset));
503*/
504
505      determineTickSize();
506      double lastXTranslation = 0;
507      int yOffset = 2;
508
509      for (Integer tickValue : getTickValues())
510      {
511         if (tickValue%mMajorTickStep == 0
512             || tickValue == mXAxisStartValue
513             || tickValue == mXAxisEndValue)
514         {
515            yOffset = 6;
516
517            Rectangle textBoundBox = TextUtil.getStringRect(tickValue + "", mLabelFont);
518            double xTranslation = mDescriptionWidthInPixels
519                               + ((tickValue - mXAxisStartValue) * getXScalingFactor())
520                               - textBoundBox.getHeight() / 4;
521
522            if (xTranslation - lastXTranslation > 8)
523            {
524               SvgText label = group.addText(tickValue + "", mLabelFont, new Point((int)xTranslation, yOffset + 5));
525               label.setTransform("rotate(45 " + (int) (xTranslation - 3) + " " + (yOffset + 5) + ")");
526               lastXTranslation = xTranslation;
527            }
528/*
529            if (xTranslation - lastXTranslation > 8
530                && (tickValue == mXAxisEndValue || xTranslation < mImageWidthInPixels - mRightBufferInPixels - 10))
531            {
532               double yTranslation = yOffset + layout.getAdvance() + 1;
533               g2.translate(xTranslation, yTranslation);
534               g2.rotate(-Math.PI / 2);
535               g2.drawString(tickValue + "", 0, 0);
536               g2.setTransform(origTransform);
537               lastXTranslation = xTranslation;
538
539            }
540*/
541         }
542         else if (tickValue%mMinorTickStep == 0)
543         {
544            yOffset = 2;
545         }
546         else
547         {
548            continue;
549         }
550
551         int xOffset = mDescriptionWidthInPixels
552                       + (int) ((tickValue - mXAxisStartValue) * getXScalingFactor());
553
554         SvgLine tickLine = group.addLine(new Point(xOffset, 1),
555                                          new Point(xOffset, yOffset));
556//         tickLine.setFill(mAxisColor);
557      }
558
559      return svg;
560
561   }
562
563   //--------------------------------------------------------------------------
564   public void draw(Graphics2D g2, Strand inStrand)
565   {
566      if (inStrand == Strand.FORWARD) mForwardImageMap = new ImageMap();
567      else                            mReverseImageMap = new ImageMap();
568      // TODO: Split mJavascript into forward and reverse?
569
570      // Save settings
571      Paint origPaint = g2.getPaint();
572      Font  origFont  = g2.getFont();
573
574      // Save inital location.
575      AffineTransform origTransform = g2.getTransform();
576
577      // Enable anti-aliasing
578//      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
579//                          RenderingHints.VALUE_ANTIALIAS_ON);
580
581
582
583      int imageHeightInPixels = (inStrand == Strand.FORWARD ? mForwardImageHeightInPixels :
584                                                              mReverseImageHeightInPixels);
585
586      // Paint the background
587      setSize(mImageWidthInPixels, imageHeightInPixels);
588      g2.setPaint(getBackgroundColor());
589      g2.fill(this);
590
591      // Draw the line separating the description from the alignment
592      g2.setPaint(mAxisColor);
593//      g2.drawLine(mDescriptionWidthInPixels - mMinimumBufferInPixels, 0,
594//                  mDescriptionWidthInPixels - mMinimumBufferInPixels, imageHeightInPixels - 2);
595      g2.fillRect(mDescriptionWidthInPixels - (2 * mMinimumBufferInPixels), 0,
596                  mMinimumBufferInPixels, imageHeightInPixels - 2);
597
598
599      // Write the Track name
600      g2.setPaint(mLabelColor);
601      g2.setFont(mLabelFont);
602      FontRenderContext frc = g2.getFontRenderContext();
603      TextLayout layout = new TextLayout(mName, mLabelFont, frc);
604      Rectangle textBounds = (g2 instanceof SvgGraphics2D ? TextUtil.getStringRect(mName, mLabelFont) : layout.getBounds().getBounds());
605      int xOffset = (int) (mDescriptionWidthInPixels - textBounds.getWidth()) -  2 * mLinePaddingInPixels - mMinimumBufferInPixels;
606      int yOffset = (int) (imageHeightInPixels/2 + textBounds.getHeight()/2);
607
608      g2.drawString(mName, xOffset, yOffset);
609
610
611      if (mURL != null)
612      {
613         // Set image map data
614         ImageMap imageMap = (inStrand == Strand.FORWARD ? mForwardImageMap : mReverseImageMap);
615         Area area = imageMap.addArea();
616         area.setHref(mURL);
617         area.setShape(Shape.RECT);
618
619         area.setCoords(xOffset + "," + (yOffset - textBounds.getHeight()) + ","
620                        + (xOffset + textBounds.getWidth()) + "," + yOffset);
621      }
622
623
624
625      drawHighlightRegions(g2, imageHeightInPixels);
626
627      drawGenes(g2, inStrand);
628
629
630
631      // Return to orig. location
632      g2.setTransform(origTransform);
633
634      // Restore settings
635      g2.setPaint(origPaint);
636      g2.setFont(origFont);
637   }
638
639
640   //--------------------------------------------------------------------------
641   public Image getImage(Strand inStrand)
642   {
643      initialize(inStrand);
644
645      int imageHeightInPixels = (inStrand == Strand.FORWARD ? mForwardImageHeightInPixels :
646                                                              mReverseImageHeightInPixels);
647
648      Frame frame = new Frame();
649      frame.addNotify();
650      Image image = frame.createImage(mImageWidthInPixels, imageHeightInPixels);
651
652      Graphics2D g2 = (Graphics2D) image.getGraphics();
653      draw(g2, inStrand);
654
655      return image;
656   }
657
658   //--------------------------------------------------------------------------
659   public BufferedImage getBufferedImage(Strand inStrand)
660   {
661      initialize(inStrand);
662
663      int imageHeightInPixels = (inStrand == Strand.FORWARD ? mForwardImageHeightInPixels :
664            mReverseImageHeightInPixels);
665
666      Frame frame = new Frame();
667      frame.addNotify();
668      BufferedImage bufferedImage = new BufferedImage(mImageWidthInPixels,
669                                                      imageHeightInPixels,
670                                                      BufferedImage.TYPE_INT_RGB);
671
672      Graphics2D g2 = (Graphics2D) bufferedImage.getGraphics();
673      draw(g2, inStrand);
674
675      return bufferedImage;
676   }
677
678   //--------------------------------------------------------------------------
679   public BufferedImage getXAxisBufferedImage()
680   {
681      Frame frame = new Frame();
682      frame.addNotify();
683      BufferedImage bufferedImage = new BufferedImage(mImageWidthInPixels,
684                                                      mXAxisImageHeightInPixels,
685                                                      BufferedImage.TYPE_INT_RGB);
686
687      Graphics2D g2 = (Graphics2D) bufferedImage.getGraphics();
688      drawXAxis(g2);
689
690      return bufferedImage;
691   }
692
693   //--------------------------------------------------------------------------
694   public void writeImageAsJpeg(OutputStream inStream, Strand inStrand)
695   throws IOException
696   {
697      BufferedImage bufferedImage = getBufferedImage(inStrand);
698      ImageIO_Util.writeBufferedImageAsJpeg(bufferedImage, inStream);
699   }
700
701   //--------------------------------------------------------------------------
702   public void writeXAxisImageAsJpeg(OutputStream inStream)
703   throws IOException
704   {
705      mXAxisImageHeightInPixels = getLabelWidthInPixels(mXAxisEndValue + "") + 7;
706
707      BufferedImage bufferedImage = getXAxisBufferedImage();
708      ImageIO_Util.writeBufferedImageAsJpeg(bufferedImage, inStream);
709   }
710
711   //--------------------------------------------------------------------------
712   public void writeImageAsPng(OutputStream inStream, Strand inStrand)
713   throws IOException
714   {
715      BufferedImage bufferedImage = getBufferedImage(inStrand);
716      ImageIO.write(bufferedImage, "png", inStream);
717   }
718 
719
720   //##########################################################################
721   // PRIVATE METHODS
722   //##########################################################################
723
724   //--------------------------------------------------------------------------
725   private void initialize(Strand inStrand)
726   {
727      List<Gene2D> genes = (inStrand == Strand.FORWARD ? mForwardGenes : mReverseGenes);
728
729      if (genes != null)
730      {
731         for (Gene2D gene : genes)
732         {
733            initializeGene(gene);
734         }
735      }
736
737      // Figure out how many lines high the image will be and set the image height.
738      createLineMap(inStrand);
739   }
740
741   //--------------------------------------------------------------------------
742   private void initializeGene(Gene2D inGene)
743   {
744      inGene.setDisplayRegion(mXAxisStartValue, mXAxisEndValue);
745      inGene.setXScalingFactor(getXScalingFactor());
746//      inGene.setForwardStrandColor(getForwardStrandColor());
747//      inGene.setReverseStrandColor(getReverseStrandColor());
748
749      if (mLabelFont != null)
750      {
751         inGene.setLabelFont(mLabelFont);
752      }
753
754      if (mShowId != null)
755      {
756         inGene.setShowId(mShowId);
757      }
758   }
759
760   //--------------------------------------------------------------------------
761   private boolean areAnyGenesInDisplayRegion(List<Gene2D> inGenes)
762   {
763      boolean result = false;
764      if (inGenes != null)
765      {
766         for (Gene2D gene : inGenes)
767         {
768            gene.setDisplayRegion(mXAxisStartValue, mXAxisEndValue);
769            if (gene.inDisplayRegion())
770            {
771               result = true;
772               break;
773            }
774         }
775      }
776
777      return result;
778   }
779
780   //--------------------------------------------------------------------------
781   private double getXScalingFactor()
782   {
783      if (0 == mXScalingFactor)
784      {
785         mXScalingFactor = (double)(mImageWidthInPixels - mDescriptionWidthInPixels - mRightBufferInPixels)
786                                    /(mXAxisEndValue - mXAxisStartValue + 1);
787      }
788
789      return mXScalingFactor;
790   }
791
792   //--------------------------------------------------------------------------
793   private int getScaledPosition(int inPosition)
794   {
795      int xOffset = (int) ((inPosition - mXAxisStartValue) * getXScalingFactor());
796
797      return xOffset;
798   }
799
800   //--------------------------------------------------------------------------
801   private Gene2D getLastGeneForLevel(int inLevel, List<List<Gene2D>> inLevelLists)
802   {
803      Gene2D outGene = null;
804
805      if (inLevelLists.size() >= inLevel)
806      {
807         List<Gene2D> levelList = inLevelLists.get(inLevel - 1);
808         outGene = levelList.get(levelList.size() - 1);
809      }
810
811      return outGene;
812   }
813
814   //--------------------------------------------------------------------------
815   private void addGeneToLevel(Gene2D inGene, int inLevel, List<List<Gene2D>> inLevelLists)
816   {
817      List<Gene2D> levelList = null;
818
819      if (inLevelLists.size() < inLevel)
820      {
821         levelList = new ArrayList<Gene2D>();
822         inLevelLists.add(levelList);
823      }
824      else
825      {
826         levelList = inLevelLists.get(inLevel - 1);
827      }
828
829      levelList.add(inGene);
830   }
831
832
833   //--------------------------------------------------------------------------
834   private boolean genesAreTooCloseTogether(Gene2D inGene1, Gene2D inGene2)
835   {
836      boolean resultValue = false;
837
838      if (getScaledPosition(inGene1.getRight()) + (mShowId && inGene1.getShowId() ? inGene1.getLabelWidthInPixels() : 0)
839          + mMinimumBufferInPixels > getScaledPosition(inGene2.getLeft()))
840      {
841         resultValue = true;
842      }
843
844      return resultValue;
845   }
846
847   //--------------------------------------------------------------------------
848   private void createLineMap(Strand inStrand)
849   {
850      Map<Gene2D, String> lineMap = null;
851      List<List<Gene2D>> levelLists = new ArrayList<List<Gene2D>>();
852
853      List<Gene2D> genes = (inStrand == Strand.FORWARD ? mForwardGenes : mReverseGenes);
854
855      if (genes != null)
856      {
857         Collections.sort(genes);
858
859         lineMap = new HashMap<Gene2D, String>();
860
861         for (Gene2D gene : genes)
862         {
863            if (gene.inDisplayRegion())
864            {
865               int level = 1;
866               boolean assigningLevel = true;
867               while (assigningLevel)
868               {
869                  Gene2D lastGene = getLastGeneForLevel(level, levelLists);
870                  if (lastGene != null
871                        && genesAreTooCloseTogether(lastGene, gene))
872                  {
873                     level++;
874                  }
875                  else
876                  {
877                     addGeneToLevel(gene, level, levelLists);
878                     lineMap.put(gene, level + "");
879                     assigningLevel = false;
880                  }
881               }
882            }
883
884         }
885      }
886
887      if (inStrand == Strand.FORWARD)
888      {
889         mForwardLineMap = lineMap;
890         mNumForwardLines = levelLists.size();
891         mForwardLineHeights = determineLineHeights(levelLists);
892//         mForwardImageHeightInPixels = mNumForwardLines * (mLineHeightInPixels + mLinePaddingInPixels);
893         mForwardImageHeightInPixels = sum(mForwardLineHeights) + mForwardLineHeights.size() * mLinePaddingInPixels;
894      }
895      else
896      {
897         mReverseLineMap = lineMap;
898         mNumReverseLines = levelLists.size();
899         mReverseLineHeights = determineLineHeights(levelLists);
900//         mReverseImageHeightInPixels = mNumReverseLines * (mLineHeightInPixels + mLinePaddingInPixels);
901         mReverseImageHeightInPixels = sum(mReverseLineHeights) + mReverseLineHeights.size() * mLinePaddingInPixels;
902      }
903
904   }
905
906
907   //--------------------------------------------------------------------------
908   private int sum(List<Integer> inValues)
909   {
910      int sum = 0;
911
912      if (CollectionUtil.hasValues(inValues))
913      {
914         for (Integer value : inValues)
915         {
916            sum += value;
917         }
918      }
919
920      return sum;
921   }
922
923   //--------------------------------------------------------------------------
924   private List<Integer> determineLineHeights(List<List<Gene2D>> inLevelLists)
925   {
926      List<Integer> lineHeights = new ArrayList<Integer>(inLevelLists.size());
927
928      for (List<Gene2D> level : inLevelLists)
929      {
930         double maxLineHeight = 0;
931         for (Gene2D gene : level)
932         {
933            if (gene.getHeightInPixels() > maxLineHeight) maxLineHeight = gene.getHeightInPixels();
934         }
935
936         lineHeights.add((int)maxLineHeight);
937      }
938
939
940      return lineHeights;
941   }
942
943   //--------------------------------------------------------------------------
944   private int getYOffsetForLine(int inLine, Strand inStrand)
945   {
946      int yOffset = 0;
947
948      List<Integer> lineHeights = (inStrand == Strand.FORWARD ? mForwardLineHeights : mReverseLineHeights);
949      for (int i = 0; i < inLine; i++)
950      {
951         yOffset -= lineHeights.get(i);
952      }
953
954      yOffset -= inLine * mLinePaddingInPixels;
955
956      return yOffset;
957   }
958
959   //--------------------------------------------------------------------------
960   private void drawHighlightRegions(Graphics2D g2, int inImageHeight)
961   {
962      if (mHighlightRegions != null)
963      {
964         for (HighlightRegion region : mHighlightRegions)
965         {
966            int xOffset = mDescriptionWidthInPixels + (int)((region.getStart() - mXAxisStartValue) * getXScalingFactor());
967            int width = (int) ((region.getEnd() - region.getStart() - 1) * getXScalingFactor());
968            if (width < 1) width = 1;
969            g2.setPaint(mHighlightColor);
970
971            g2.fillRect(xOffset, 0, width, inImageHeight);
972         }
973
974      }
975   }
976
977   //--------------------------------------------------------------------------
978   private SvgGroup generateHighlightRegions(int inImageHeight)
979   {
980      SvgGroup highlightGroup = null;
981      if (mHighlightRegions != null)
982      {
983         highlightGroup = new SvgGroup();
984         for (HighlightRegion region : mHighlightRegions)
985         {
986            int xOffset = mDescriptionWidthInPixels + (int)((region.getStart() - mXAxisStartValue) * getXScalingFactor());
987            int width = (int) ((region.getEnd() - region.getStart() - 1) * getXScalingFactor());
988            if (width < 1) width = 1;
989            highlightGroup.addRect(new Rectangle(new Point(xOffset, 0),
990                                                 new Dimension(width, inImageHeight))).setFill(mHighlightColor);
991         }
992      }
993
994      return highlightGroup;
995   }
996
997   //--------------------------------------------------------------------------
998   private SvgGroup generateGenes(Strand inStrand)
999   {
1000      // Move to the bottom left of the genomic display
1001      int imageHeightInPixels = 0;
1002      List<Gene2D> genes = null;
1003      Map<Gene2D, String> lineMap = null;
1004      if (inStrand == Strand.FORWARD)
1005      {
1006         imageHeightInPixels = mForwardImageHeightInPixels;
1007         genes = mForwardGenes;
1008         lineMap = mForwardLineMap;
1009      }
1010      else
1011      {
1012         imageHeightInPixels = mReverseImageHeightInPixels;
1013         genes = mReverseGenes;
1014         lineMap = mReverseLineMap;
1015      }
1016
1017      SvgGroup group = new SvgGroup().setTransform("translate(" + mDescriptionWidthInPixels + ", " + imageHeightInPixels + ")");
1018
1019
1020      if (genes != null)
1021      {
1022         for (Gene2D gene : genes)
1023         {
1024            if (gene.inDisplayRegion())
1025            {
1026
1027               SvgLink svgLink = null;
1028               List<Link> urls = gene.getURLs();
1029               if (CollectionUtil.hasValues(urls))
1030               {
1031                  if (1 == urls.size())
1032                  {
1033                     svgLink = new SvgLink(urls.get(0).getURL());
1034                  }
1035                  else
1036                  {
1037                     PopupMenuJS menu = new PopupMenuJS();
1038                     menu.setMenuTitle(gene.getId());
1039                     for (Link link : urls)
1040                     {
1041                        menu.addLink(link);
1042                     }
1043
1044                     // Add the menu to the track's javascript
1045                     mJavascript.addContent(menu.generateMenuJavascript());
1046                     svgLink = new SvgLink(menu.getDisplayMenuJavascript());
1047                  }
1048               }
1049
1050               SvgNode svgNode = null;
1051               if (svgLink != null)
1052               {
1053                  svgNode = svgLink;
1054               }
1055               else
1056               {
1057                  svgNode = new SvgGroup();
1058               }
1059               group.addSubtag(svgNode);
1060               
1061               // Move to gene location
1062
1063               // What line is the gene on? Move to upper left of gene.
1064               int line = Integer.parseInt(lineMap.get(gene));
1065               if (inStrand == Strand.REVERSE)
1066               {
1067                  line = mNumReverseLines - line + 1;
1068               }
1069
1070               //XXXX           int xOffset = (int) ((gene.getStartLocation() - mXAxisStartValue) * getXScalingFactor());
1071               int geneLeft = (int) (gene.getLeft() * mXScalingFactor);
1072               int bound = (int) (mXAxisStartValue * mXScalingFactor);
1073               int xOffset = geneLeft - bound;
1074//               int xOffset = (int) ((gene.getLeft() - mXAxisStartValue) * getXScalingFactor());
1075               // Don't back up into the description region.
1076               if (xOffset < 0) xOffset = 0;
1077        /*       // Don't go off the right side of the display region.
1078               if (xOffset > mImageWidthInPixels - mDescriptionWidthInPixels - mMinimumBufferInPixels)
1079               {
1080                  xOffset = mImageWidthInPixels - mDescriptionWidthInPixels - mMinimumBufferInPixels;
1081               }
1082        */
1083
1084//               int yOffset = -line * (mLineHeightInPixels + mLinePaddingInPixels);
1085               int yOffset = getYOffsetForLine(line, inStrand);
1086
1087//               SvgGroup geneGroup = group.addGroup().setTransform();
1088
1089               svgNode.setTransform("translate(" + xOffset + ", " + yOffset + ")");
1090
1091               svgNode.addSubtag(gene.toSVG());
1092            }
1093         }
1094      }
1095
1096      return group;
1097   }
1098
1099   //--------------------------------------------------------------------------
1100   private void drawGenes(Graphics2D g2, Strand inStrand)
1101   {
1102      TooltipJS tooltip = new TooltipJS();
1103
1104      int imageHeightInPixels = 0;
1105      List<Gene2D> genes = null;
1106      Map<Gene2D, String> lineMap = null;
1107      ImageMap imageMap = null;
1108      if (inStrand == Strand.FORWARD)
1109      {
1110         imageHeightInPixels = mForwardImageHeightInPixels;
1111         genes = mForwardGenes;
1112         lineMap = mForwardLineMap;
1113         imageMap = mForwardImageMap;
1114      }
1115      else
1116      {
1117         imageHeightInPixels = mReverseImageHeightInPixels;
1118         genes = mReverseGenes;
1119         lineMap = mReverseLineMap;
1120         imageMap = mReverseImageMap;
1121      }
1122
1123      // Move to the bottom left of the genomic display
1124      g2.translate(mDescriptionWidthInPixels, imageHeightInPixels);
1125
1126      if (genes != null)
1127      {
1128         // Save initial location.
1129         AffineTransform origTransform = g2.getTransform();
1130
1131         for (Gene2D gene : genes)
1132         {
1133            if (gene.inDisplayRegion())
1134            {
1135               // Dynamically set the gene model color based on the mapped %id?  
1136               if (mPctIdColorScale != null
1137                   && null == gene.getColor()
1138                   && gene.getSeqMappingCoverage() != null
1139                   && gene.getSeqMappingCoverage().getPercentIdentity() != null)
1140               {
1141                  gene.setColor(mPctIdColorScale.assignColorForValue(gene.getSeqMappingCoverage().getPercentIdentity() / 100));
1142               }
1143
1144               // Move to gene location
1145
1146               // What line is the gene on? Move to upper left of gene.
1147               int line = Integer.parseInt(lineMap.get(gene));
1148               if (inStrand == Strand.REVERSE)
1149               {
1150                  line = mNumReverseLines - line + 1;
1151               }
1152
1153               //XXXX           int xOffset = (int) ((gene.getStartLocation() - mXAxisStartValue) * getXScalingFactor());
1154               int geneLeft = (int) (gene.getLeft() * mXScalingFactor);
1155               int bound = (int) (mXAxisStartValue * mXScalingFactor);
1156               int xOffset = geneLeft - bound;
1157//               int xOffset = (int) ((gene.getLeft() - mXAxisStartValue) * getXScalingFactor());
1158               // Don't back up into the description region.
1159               if (xOffset < 0) xOffset = 0;
1160        /*       // Don't go off the right side of the display region.
1161               if (xOffset > mImageWidthInPixels - mDescriptionWidthInPixels - mMinimumBufferInPixels)
1162               {
1163                  xOffset = mImageWidthInPixels - mDescriptionWidthInPixels - mMinimumBufferInPixels;
1164               }
1165        */
1166
1167//               int yOffset = -line * (mLineHeightInPixels + mLinePaddingInPixels);
1168               int yOffset = getYOffsetForLine(line, inStrand);
1169
1170               g2.translate(xOffset, yOffset);
1171
1172               gene.draw(g2);
1173
1174               // Return to orig. location
1175               g2.setTransform(origTransform);
1176
1177               // Set image map data
1178               Area area = imageMap.addArea();
1179               setAreaURL(area, gene);
1180
1181               tooltip.addTooltip(area, gene.getTooltipContent());
1182               area.setShape(Shape.RECT);
1183
1184               int x1 = mDescriptionWidthInPixels + xOffset;
1185               if (x1 < mDescriptionWidthInPixels) x1 = mDescriptionWidthInPixels;
1186               int x2 = mDescriptionWidthInPixels + xOffset + gene.getScaledWidthInPixels()
1187                        + (mShowId ? gene.getLabelWidthInPixels() : 0);
1188               if (x2 > mImageWidthInPixels)
1189               {
1190                  x2 = mImageWidthInPixels;
1191               }
1192               area.setCoords(x1 + "," + (imageHeightInPixels + yOffset) + ","
1193                              + x2 + "," + (imageHeightInPixels + yOffset + gene.getHeightInPixels() - 1));
1194            }
1195         }
1196      }
1197   }
1198
1199   //--------------------------------------------------------------------------
1200   private void setAreaURL(Area inArea, Gene2D inGene)
1201   {
1202      List<Link> urls = inGene.getURLs();
1203      if (CollectionUtil.hasValues(urls))
1204      {
1205         if (1 == urls.size())
1206         {
1207            Link link = urls.get(0);
1208            inArea.setHref(link.getURL());
1209         }
1210         else
1211         {
1212            PopupMenuJS menu = new PopupMenuJS();
1213            menu.setMenuTitle(inGene.getId());
1214            for (Link link : urls)
1215            {
1216               menu.addLink(link);
1217            }
1218
1219             // Add the menu to the track's javascript
1220            mJavascript.addContent(menu.generateMenuJavascript());
1221
1222            inArea.setHref(menu.getDisplayMenuJavascript());
1223         }
1224      }
1225   }
1226
1227   //--------------------------------------------------------------------------
1228   public void drawXAxis(Graphics2D g2)
1229   {
1230      // Save settings
1231      Paint origPaint = g2.getPaint();
1232      Font  origFont  = g2.getFont();
1233
1234      // Save inital location.
1235      AffineTransform origTransform = g2.getTransform();
1236
1237      // Enable anti-aliasing
1238      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1239                          RenderingHints.VALUE_ANTIALIAS_ON);
1240
1241      // Paint the background
1242      g2.setPaint(Color.white);
1243      g2.fillRect(0, 0, mImageWidthInPixels, mXAxisImageHeightInPixels);
1244
1245
1246      drawHighlightRegions(g2, mXAxisImageHeightInPixels);
1247
1248      // Draw the x-axis line
1249      g2.setPaint(mAxisColor);
1250      g2.drawLine(mDescriptionWidthInPixels, 0,
1251                  mImageWidthInPixels - mRightBufferInPixels, 0);
1252
1253      determineTickSize();
1254      double lastXTranslation = 0;
1255      int yOffset = 2;
1256
1257      for (Integer tickValue : getTickValues())
1258      {
1259         if (tickValue%mMajorTickStep == 0
1260             || tickValue == mXAxisStartValue
1261             || tickValue == mXAxisEndValue)
1262         {
1263            yOffset = 4;
1264
1265            g2.setFont(mLabelFont);
1266            FontRenderContext frc = g2.getFontRenderContext();
1267            TextLayout layout = new TextLayout(tickValue + "", mLabelFont, frc);
1268            double xTranslation = mDescriptionWidthInPixels
1269                               + ((tickValue - mXAxisStartValue) * getXScalingFactor())
1270                               + layout.getBounds().getHeight() / 2;
1271
1272            if (xTranslation - lastXTranslation > 8
1273                && (tickValue == mXAxisEndValue || xTranslation < mImageWidthInPixels - mRightBufferInPixels - 10))
1274            {
1275               double yTranslation = yOffset + layout.getAdvance() + 1;
1276               g2.translate(xTranslation, yTranslation);
1277               g2.rotate(-Math.PI / 2);
1278               g2.drawString(tickValue + "", 0, 0);
1279               g2.setTransform(origTransform);
1280               lastXTranslation = xTranslation;
1281            }
1282         }
1283         else if (tickValue%mMinorTickStep == 0)
1284         {
1285            yOffset = 2;
1286         }
1287         else
1288         {
1289            continue;
1290         }
1291
1292         int xOffset = mDescriptionWidthInPixels
1293                       + (int) ((tickValue - mXAxisStartValue) * getXScalingFactor());
1294         g2.drawLine(xOffset, 0,
1295                     xOffset, yOffset);
1296      }
1297
1298
1299      // Return to orig. location
1300      g2.setTransform(origTransform);
1301
1302      // Restore settings
1303      g2.setPaint(origPaint);
1304      g2.setFont(origFont);
1305   }
1306
1307   //--------------------------------------------------------------------------
1308   private void determineTickSize()
1309   {
1310      int xAxisWidth = mXAxisEndValue - mXAxisStartValue + 1;
1311      String widthString = xAxisWidth + "";
1312      mMajorTickStep = (int) Math.pow(10, widthString.length() - 1);
1313      mMinorTickStep = mMajorTickStep / 10;
1314   }
1315
1316   //--------------------------------------------------------------------------
1317   private List<Integer> getTickValues()
1318   {
1319      List<Integer> tickValues = new ArrayList<Integer>();
1320
1321      // First add the major ticks
1322      tickValues.add(mXAxisStartValue);
1323
1324      for (int tickValue = (int) (Math.ceil(mXAxisStartValue / mMajorTickStep) * mMajorTickStep);
1325           tickValue < mXAxisEndValue;
1326           tickValue += mMajorTickStep)
1327      {
1328         if (tickValue > mXAxisStartValue)
1329         {
1330            tickValues.add(tickValue);
1331         }
1332      }
1333
1334      tickValues.add(mXAxisEndValue);
1335
1336      // Now add the minor ticks
1337      for (int tickValue = (int) (Math.ceil(mXAxisStartValue / mMinorTickStep) * mMinorTickStep);
1338           tickValue < mXAxisEndValue;
1339           tickValue += mMinorTickStep)
1340      {
1341         if (tickValue%mMajorTickStep != 0
1342             && tickValue != mXAxisStartValue)
1343         {
1344            tickValues.add(tickValue);
1345         }
1346      }
1347
1348      return tickValues;
1349   }
1350
1351   //--------------------------------------------------------------------------
1352   public int getLabelWidthInPixels(String inLabel)
1353   {
1354      int widthInPixels = 0;
1355
1356      if (inLabel != null
1357            && inLabel.length() > 0)
1358      {
1359         TextLayout layout = new TextLayout(inLabel, mLabelFont, sFRC);
1360         widthInPixels = (int) layout.getBounds().getWidth() + 1;
1361      }
1362
1363      return widthInPixels;
1364   }
1365
1366
1367
1368
1369   //--------------------------------------------------------------------------
1370   private class HighlightRegion
1371   {
1372      private int mStart;
1373      private int mEnd;
1374
1375      //-----------------------------------------------------------------------
1376      public HighlightRegion(int inStart, int inEnd)
1377      {
1378         if (inEnd < inStart)
1379         {
1380            mStart = inEnd;
1381            mEnd = inStart;
1382         }
1383         else
1384         {
1385            mStart = inStart;
1386            mEnd = inEnd;
1387         }
1388      }
1389
1390      //-----------------------------------------------------------------------
1391      public int getStart()
1392      {
1393         return mStart;
1394      }
1395
1396      //-----------------------------------------------------------------------
1397      public int getEnd()
1398      {
1399         return mEnd;
1400      }
1401   }
1402
1403}
1404