gt:         currentYLine += rowHeight;
context: /*
   * Signal Visualization Tools for the Owl Platform
   * Copyright (C) 2012 Robert Moore
   * 
   * This program is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation; either version 2 of the License, or
   * (at your option) any later version.
   *  
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *  
   * You should have received a copy of the GNU General Public License along
   * with this program; if not, write to the Free Software Foundation, Inc.,
   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
   */
  
  package com.owlplatform.sigvis.panels;
  
  import java.awt.AlphaComposite;
  import java.awt.Color;
  import java.awt.Composite;
  import java.awt.Font;
  import java.awt.FontMetrics;
  import java.awt.Graphics;
  import java.awt.Graphics2D;
  import java.awt.Point;
  import java.awt.RenderingHints;
  import java.awt.event.MouseEvent;
  import java.awt.event.MouseListener;
  import java.awt.event.MouseMotionListener;
  import java.awt.event.MouseWheelEvent;
  import java.awt.event.MouseWheelListener;
  import java.awt.font.FontRenderContext;
  import java.awt.geom.GeneralPath;
  import java.awt.geom.Line2D;
  import java.awt.geom.Rectangle2D;
  import java.awt.image.BufferedImage;
  import java.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.Date;
  import java.util.List;
  
  import javax.swing.JComponent;
  
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.owlplatform.sigvis.DataCache2;
  import com.owlplatform.sigvis.DataCache2.ValueType;
  import com.owlplatform.sigvis.ImageResources;
  import com.owlplatform.sigvis.structs.ChartItem;
  
  public class LineChart extends JComponent implements DisplayPanel,
      MouseListener, MouseMotionListener, MouseWheelListener {
  
    private static final Logger log = LoggerFactory.getLogger(LineChart.class);
  
    protected static final long MAX_TIME_GAP = 2000l;
  
    protected static final int DEFAULT_MARGIN = 20;
  
    protected static final int MARGIN_TOP = 0;
    protected static final int MARGIN_RIGHT = 1;
    protected static final int MARGIN_BOTTOM = 2;
    protected static final int MARGIN_LEFT = 3;
  
    protected int[] margins = { DEFAULT_MARGIN * 2, DEFAULT_MARGIN,
        DEFAULT_MARGIN * 2, DEFAULT_MARGIN * 2 };
  
    // protected int maxElements = 100;
  
    // Default to 30 seconds so as not to draw too much
    protected long maxAge = 30000;
  
    // Minimum of 10 seconds
    protected static final long MIN_DISPLAY_AGE = 10000;
  
    protected static final long DISPAY_AGE_STEP = 5000;
  
    public long getMaxAge() {
      return maxAge;
    }
  
    public void setMaxAge(long maxAge) {
      this.maxAge = maxAge;
      if (this.timeOffset < 0) {
        this.timeOffset = 0;
      } else if (this.timeOffset > (this.cache.getMaxCacheAge() - this.maxAge)) {
        this.timeOffset = this.cache.getMaxCacheAge() - this.maxAge;
      }
    }
  
    protected float maxValue = 1f;
  
    protected float currValMax = 1f;
  
    protected float minValue = 0f;
  
    protected boolean scrolling = true;
  
    protected long nextMinCheck = System.currentTimeMillis();
  
    protected long lastRepaint = 0l;
  
    protected boolean selfAdjustMax = false;
  
    protected boolean drawPoints = true;
  
    protected boolean displayLegend = false;
  
    protected float legendHeight = 0;
  
    protected boolean drawHorizontalLines = true;
  
    protected boolean drawVerticalLines = true;
  
    protected Color axisTextColor = Color.LIGHT_GRAY;
  
    protected Color backgroundColor = Color.BLACK;
  
    protected Color chartGridColor = Color.DARK_GRAY;
  
    protected Color borderColor = Color.LIGHT_GRAY;
  
    protected boolean enableAntiAliasing = true;
  
    protected boolean useTransparency = false;
  
    protected float minFps = 8;
  
    protected float currFps = 30f;
  
    protected int slowFrames = 0;
  
    protected Composite fillUnderAlpha = AlphaComposite.getInstance(
        AlphaComposite.SRC_OVER, 0.2f);
  
    protected String lastStream = null;
  
    protected long highlightTime = 2000l;
  
    protected float yAdjustValue = 0f;
  
    protected float adjustInterval = 1.0f;
  
    protected BufferedImage downArrowImg = null;
  
    protected BufferedImage upArrowImg = null;
  
    protected BufferedImage playImg = null;
  
    protected BufferedImage pauseImg = null;
  
    protected boolean amRaiseMaxY = false;
  
    protected boolean amLowerMaxY = false;
  
    protected DataCache2 cache;
  
    protected String displayedId = null;
  
    protected boolean deviceIsTransmitter = false;
  
    protected ValueType type;
  
    public float getMinFps() {
      return this.minFps;
    }
  
    public void setMinFps(float minFps) {
      this.minFps = minFps;
    }
  
    @Override
    public boolean isAntiAlias() {
      return enableAntiAliasing;
    }
  
    @Override
    public void setAntiAlias(boolean enableAntiAliasing) {
      this.enableAntiAliasing = enableAntiAliasing;
    }
  
    public boolean isLegendDisplayed() {
      return displayLegend;
    }
  
    public void setDisplayLegend(boolean drawLegend) {
      this.displayLegend = drawLegend;
      if (!this.displayLegend) {
        this.legendHeight = 0f;
      }
    }
  
    public boolean isSelfAdjustMax() {
      return selfAdjustMax;
    }
  
    public void setSelfAdjustMax(boolean selfAdjustMax) {
      this.selfAdjustMax = selfAdjustMax;
    }
  
    protected boolean selfAdjustMin = false;
  
    public boolean isScrolling() {
      return scrolling;
    }
  
    public void setScrolling(boolean scrolling) {
      this.scrolling = scrolling;
    }
  
    public LineChart(final ValueType type, final DataCache2 cache) {
      super();
      this.cache = cache;
      this.type = type;
      this.playImg = ImageResources.IMG_PLAY;
      this.pauseImg = ImageResources.IMG_PAUSE;
      this.upArrowImg = ImageResources.IMG_UP_ARROW;
      this.downArrowImg = ImageResources.IMG_DOWN_ARROW;
      // TODO: Uncomment the below to support dragging x-axis (time)
      // this.addMouseMotionListener(this);
      this.addMouseListener(this);
      this.addMouseWheelListener(this);
      if (this.cache.isClone()) {
        this.scrolling = false;
        this.lastRepaint = this.cache.getCreationTs();
      } else {
        this.lastRepaint = System.currentTimeMillis();
      }
  
      this.setDoubleBuffered(true);
  
    }
  
    public void clear() {
      // TODO: Anything to do here?
    }
  
    @Override
    public void paintComponent(Graphics graphics) {
      super.paintComponent(graphics);
  
      if (this.scrolling) {
        this.lastRepaint = System.currentTimeMillis();
      }
  
      Graphics2D g2 = (Graphics2D) graphics;
      if (this.enableAntiAliasing) {
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
      }
  
      int screenWidth = this.getWidth();
      int screenHeight = this.getHeight();
  
      Color origColor = g2.getColor();
      Composite origComposite = g2.getComposite();
  
      this.drawBackground(g2, screenWidth, screenHeight);
  
      if (this.displayedId == null) {
        return;
      }
  
      List<String> devices = this.deviceIsTransmitter ? this.cache
          .getReceiverIds() : this.cache.getFiduciaryTransmitterIds();
      if (devices == null) {
        log.warn("No complimentary devices for {}", this.displayedId);
        return;
      }
      Collections.sort(devices);
  
      if (this.displayLegend) {
        this.drawLegend(g2, devices, screenWidth, screenHeight);
      }
  
      g2.setColor(Color.DARK_GRAY);
      this.drawChartGrid(g2, screenWidth, screenHeight,
          (float) (this.maxAge / 10.0), (this.maxValue - this.minValue) / 10f);
  
      String[] streamIdArray = devices.toArray(new String[] {});
      Color drawColor = Color.BLUE;
      int streamNum = 0;
      float newMax = this.minValue;
      for (String streamId : streamIdArray) {
        float adjusted = 0.9f * streamNum / streamIdArray.length;
        float hue = adjusted;
        float sat = 0.95f;
        float bright = 0.95f;
        drawColor = new Color(Color.HSBtoRGB(hue, sat, bright));
        g2.setColor(drawColor);
  
        // TODO: Grab variance list instead
        Collection<ChartItem<Float>> sampleList = this.generateDisplayedData(
            this.deviceIsTransmitter ? this.displayedId : streamId,
            this.deviceIsTransmitter ? streamId: this.displayedId);
  
        ++streamNum;
        if (sampleList == null || sampleList.isEmpty()) {
  
          continue;
        }
  
        float tempMax = this.deviceIsTransmitter ? this.drawStream(g2, streamId,
            this.displayedId, sampleList, screenWidth, screenHeight) : this
            .drawStream(g2, this.displayedId, streamId, sampleList, screenWidth,
                screenHeight);
        if (tempMax > newMax) {
          newMax = tempMax;
        }
      }
  
      if (this.selfAdjustMax) {
        this.adjustMaxY(newMax, this.maxValue - this.minValue);
      }
  
      this.drawAdjustInfo(g2, screenWidth, screenHeight);
      this.drawPauseInfo(g2, screenWidth, screenHeight);
  
      // if (highlightStream != null) {
      // g2.setColor(Color.WHITE);
      // this.drawStream(g2, highlightStream, screenWidth, screenHeight);
      // }
  
      g2.setColor(Color.LIGHT_GRAY);
      this.drawChartBorders(g2, screenWidth, screenHeight);
      this.drawStatsValues(g2, screenWidth, screenHeight);
      this.drawTimestamp(g2, screenWidth, screenHeight);
      g2.setColor(origColor);
      g2.setComposite(origComposite);
  
      long renderTime = System.currentTimeMillis() - this.lastRepaint;
      this.currFps = this.currFps * 0.875f + (1000f / renderTime) * 0.125f;
  
      // if (this.enableAntiAliasing && (this.currFps < this.minFps * 0.9f)) {
      // ++this.slowFrames;
      // if (this.slowFrames > 3) {
      // this.enableAntiAliasing = false;
      // log.warn("FPS: {} Disabling Anti-Aliasing.", this.currFps);
      // }
      // } else if (this.enableAntiAliasing) {
      // this.slowFrames = 0;
      // }
    }
  
    protected Collection<ChartItem<Float>> generateDisplayedData(String txer,
        String rxer) {
      long youngestItem = this.lastRepaint - this.timeOffset;
      if (this.cache.isClone()) {
        youngestItem = this.cache.getCreationTs() - this.timeOffset;
      }
      long oldestItem = youngestItem - this.maxAge;
      
      if (this.type == ValueType.RSSI) {
        return this.cache.getRssiList(rxer, txer,oldestItem,youngestItem);
      } else if (this.type == ValueType.VARIANCE) {
        return this.cache.getVarianceList(rxer, txer, oldestItem, youngestItem);
      }
      return null;
    }
    
    protected void drawBackground(Graphics2D g, int screenWidth, int screenHeight){
      g.setColor(Color.BLACK);
      g.fillRect(0, 0, screenWidth, screenHeight);
    }
  
    protected void drawAdjustInfo(Graphics g, int screenWidth, int screenHeight) {
      if (this.amLowerMaxY && this.downArrowImg != null) {
        g.drawImage(this.downArrowImg, screenWidth - this.margins[MARGIN_RIGHT],
            this.margins[MARGIN_TOP] + (int) this.legendHeight + 5
                + this.margins[MARGIN_RIGHT], screenWidth,
            this.margins[MARGIN_TOP] + (int) this.legendHeight + 5 + 2
                * this.margins[MARGIN_RIGHT], 0, 0, this.downArrowImg.getWidth(),
            this.downArrowImg.getHeight(), null);
      } else if (this.amRaiseMaxY && this.upArrowImg != null) {
        g.drawImage(this.upArrowImg, screenWidth - this.margins[MARGIN_RIGHT],
            this.margins[MARGIN_TOP] + (int) this.legendHeight + 5
                + this.margins[MARGIN_RIGHT], screenWidth,
            this.margins[MARGIN_TOP] + (int) this.legendHeight + 5 + 2
                * this.margins[MARGIN_RIGHT], 0, 0, this.upArrowImg.getWidth(),
            this.upArrowImg.getHeight(), null);
      }
    }
  
    protected void drawPauseInfo(Graphics g, int screenWidth, int screenHeight) {
      if (this.scrolling && this.playImg != null) {
        g.drawImage(this.playImg, screenWidth - this.margins[MARGIN_RIGHT],
            this.margins[MARGIN_TOP] + (int) this.legendHeight, screenWidth,
            this.margins[MARGIN_TOP] + (int) this.legendHeight
                + this.margins[MARGIN_RIGHT], 0, 0, this.playImg.getWidth(),
            this.playImg.getHeight(), null);
      } else if (this.pauseImg != null) {
        g.drawImage(this.pauseImg, screenWidth - this.margins[MARGIN_RIGHT],
            this.margins[MARGIN_TOP] + (int) this.legendHeight, screenWidth,
            this.margins[MARGIN_TOP] + (int) this.legendHeight
                + this.margins[MARGIN_RIGHT], 0, 0, this.pauseImg.getWidth(),
            this.pauseImg.getHeight(), null);
      }
    }
  
    protected void adjustMaxY(float tempMax, float yRange) {
      float adjustBy = 0f;
      // Check to see if we need to adjust...
      if (tempMax < (this.maxValue - yRange * 0.15)) {
        float diff = this.maxValue - tempMax;
  
        if (this.yAdjustValue < diff) {
          this.yAdjustValue = diff;
        }
  
        // If not adjusting, then set up the adjustment interval
        // if (!this.amLowerMaxY) {
        // this.yAdjustValue = this.maxYValue - tempMax;
        this.amLowerMaxY = true;
        this.amRaiseMaxY = false;
        // }
        adjustBy = this.yAdjustValue * (1f / (this.adjustInterval * this.minFps));
  
        if (adjustBy > diff) {
          adjustBy = diff;
        }
        this.maxValue -= adjustBy;
        if (this.maxValue < this.minValue + 1) {
          this.maxValue = this.minValue + 1;
        }
      } else if (tempMax > this.maxValue) {
        float diff = tempMax * 1.1f - this.maxValue;
  
        if (diff > this.yAdjustValue) {
          this.yAdjustValue = diff;
        }
        //
        // if(!this.amRaiseMaxY){
        // this.yAdjustValue = tempMax*1.1f - this.maxYValue;
        this.amRaiseMaxY = true;
        this.amLowerMaxY = false;
        // }
        adjustBy = this.yAdjustValue * (1f / (this.adjustInterval * this.minFps));
  
        if (adjustBy > diff) {
          adjustBy = diff;
        }
        this.maxValue += adjustBy;
      }
  
      else {
        this.yAdjustValue = 0f;
        this.amRaiseMaxY = false;
        this.amLowerMaxY = false;
        return;
      }
  
      /*
       * else if (tempMax > this.maxYValue - yRange * 0.1f) { float increaseBy =
       * (tempMax - this.maxYValue) (1f / this.minFps);
       * log.info("MFPS: {}, Increase by {}", this.minFps, increaseBy);
       * this.maxYValue += increaseBy; }
       */
    }
  
    protected void drawLegend(final Graphics g, final List<String> devices,
        final int screenWidth, final int screenHeight) {
      Graphics2D g2 = (Graphics2D) g;
      if (devices == null && devices.size() == 0) {
        return;
      }
      FontMetrics fontMetrics = g2.getFontMetrics();
  
      float stringHeight = (float) fontMetrics.getStringBounds("TEST", g)
          .getHeight();
  
      float rowHeight = stringHeight + 10;
      ArrayList<Integer> lineBreakIndexes = new ArrayList<Integer>();
  
      // Determine the number of rows and columns
      // Assume a square box for color display at string height, 5px spacing,
      // then text
      // 10px spacing between rows and columns
  
      String[] streamIdArray = devices.toArray(new String[] {});
  
      int streamIndex = 0;
      float availableWidth = screenWidth - this.margins[MARGIN_LEFT]
          - this.margins[MARGIN_RIGHT];
      float currentRowWidth = 0f;
      for (String id : streamIdArray) {
        float entryWidth = fontMetrics.stringWidth(id.toString()) + 10;
        currentRowWidth += entryWidth;
        if (currentRowWidth > availableWidth) {
          lineBreakIndexes.add(streamIndex);
          currentRowWidth = entryWidth;
        }
        ++streamIndex;
      }
  
      streamIndex = 0;
  
      float currentYLine = this.margins[MARGIN_TOP] + stringHeight;
      float currentXStart = this.margins[MARGIN_LEFT];
  
      Color drawColor = Color.BLUE;
      for (String streamId : streamIdArray) {
        if (lineBreakIndexes.contains(streamIndex)) {
  