NyquistPlotter.java

/*
 * Created on 2008/12/05
 * Copyright (C) 2008 Koga Laboratory. All rights reserved.
 *
 */
package org.mklab.tool.control;

import java.util.ArrayList;
import java.util.List;

import org.mklab.nfc.matrix.DoubleMatrix;
import org.mklab.tool.graph.gnuplot.Canvas;
import org.mklab.tool.graph.gnuplot.Gnuplot;


/**
 * ナイキスト線図を描画するクラスです。
 * 
 * @author koga
 * @version $Revision$, 2008/12/05
 */
public class NyquistPlotter extends Plotter {

  /** Gnuplot */
  private Gnuplot gnuplot;
  /** キャンバス */
  private Canvas graph;

  /** 実部データ */
  private DoubleMatrix reals;
  /** 虚部データ */
  private DoubleMatrix imags;

  /** 単位円を描くならばtrue */
  private boolean isDrawingUnitCircle = true;

  /** (-1,0)点を描くならばtrue */
  private boolean isDrawingMinusOneZeroPoint = true;

  /** 実軸を描くならばtrue */
  private boolean isDrawingRealAxis = false;

  /** 虚軸を描くならばtrue */
  private boolean isDrawingImaginaryAxis = false;

  /**
   * 新しく生成された<code>NyquistPlotter</code>オブジェクトを初期化します。
   * 
   * @param gnuplot Gnuplot
   */
  public NyquistPlotter(final Gnuplot gnuplot) {
    this.gnuplot = gnuplot;
    this.graph = this.gnuplot.createCanvas();
  }

  /**
   * ナイキスト線図をプロットします。
   * 
   * @param realImagList 実部と虚部
   */
  public void plot(List<List<DoubleMatrix>> realImagList) {
    final List<DoubleMatrix> magnitudes = new ArrayList<>();
    for (final List<DoubleMatrix> realImag : realImagList) {
      final DoubleMatrix realColumn = realImag.get(0);
      final DoubleMatrix imagColumn = realImag.get(1);
      magnitudes.add(realColumn.powerElementWise(2).add(imagColumn.powerElementWise(2)).sqrtElementWise());
    }

    if (this.graph.isKeepingLineProperties() == false) {
      setupLineNameAndType(magnitudes);
    }

    setupRealImag(realImagList);
    plotNyquist();

    if (this.graph.isKeepingLineProperties() == false) {
      initializeLineTypes();
    }
  }

  /**
   * ナイキスト線図を描画します。
   */
  private void plotNyquist() {
    final double xInterval = Math.abs(this.reals.max().doubleValue() - this.reals.min().doubleValue());
    final double xScaling = Math.pow(10, -Math.floor(Math.log10(xInterval)));
    final double xGridInterval = Math.ceil(xInterval * xScaling / 6) / xScaling;

    final double xMin = Math.floor(this.reals.min().doubleValue() / xGridInterval) * xGridInterval;
    final double xMax = Math.ceil(this.reals.max().doubleValue() / xGridInterval) * xGridInterval;

    final double yInterval = Math.abs(this.imags.max().doubleValue() - this.imags.min().doubleValue());
    final double yScaling = Math.pow(10, -Math.floor(Math.log10(yInterval)));
    final double yGridInterval = Math.ceil(yInterval * yScaling / 6) / yScaling;

    final double yMin = Math.floor(this.imags.min().doubleValue() / yGridInterval) * yGridInterval;
    final double yMax = Math.ceil(this.imags.max().doubleValue() / yGridInterval) * yGridInterval;

    this.graph.setXLabel("Re"); //$NON-NLS-1$
    this.graph.setYLabel("Im"); //$NON-NLS-1$
    this.graph.setGridVisible(true);
    this.graph.setTitle("Nyquist Plot"); //$NON-NLS-1$
    this.graph.setXRange(xMin, xMax);
    this.graph.setXTics(xMin, xGridInterval, xMax);
    this.graph.setYRange(yMin, yMax);
    this.graph.setYTics(yMin, yGridInterval, yMax);
    this.graph.plot(this.reals, this.imags, this.lineNames.values().toArray(new String[this.lineNames.size()]));
  }

  /**
   * 実部データと虚部データを設定します。
   * 
   * @param realImagList 実部データと虚部データ
   */
  private void setupRealImag(List<List<DoubleMatrix>> realImagList) {
    DoubleMatrix localReals = new DoubleMatrix(0, 0);
    DoubleMatrix localImags = new DoubleMatrix(0, 0);

    double realMin = Double.POSITIVE_INFINITY;
    double realMax = Double.NEGATIVE_INFINITY;
    double imaginaryMin = Double.POSITIVE_INFINITY;
    double imaginaryMax = Double.NEGATIVE_INFINITY;

    for (List<DoubleMatrix> realImag : realImagList) {
      final DoubleMatrix real = realImag.get(0);
      final DoubleMatrix imag = realImag.get(1);

      for (int outputNumber = 1; outputNumber <= real.getRowSize(); outputNumber++) {
        final DoubleMatrix re = real.getRowVector(outputNumber);
        final DoubleMatrix im = imag.getRowVector(outputNumber);

        if (re.isZero() && im.isZero()) {
          continue;
        }

        if (localReals.getRowSize() == 0 && localImags.getRowSize() == 0) {
          localReals = re;
          localImags = im;
        } else {
          localReals = localReals.appendDown(re);
          localImags = localImags.appendDown(im);
        }

        realMin = Math.min(realMin, re.min().doubleValue());
        realMax = Math.max(realMax, re.max().doubleValue());
        imaginaryMin = Math.min(imaginaryMin, im.min().doubleValue());
        imaginaryMax = Math.max(imaginaryMax, im.max().doubleValue());
      }
    }

    this.reals = localReals;
    this.imags = localImags;

    if (this.isDrawingMinusOneZeroPoint) {
      drawMinusOneZeroPoint(realMin, realMax, imaginaryMin, imaginaryMax);
    }

    if (this.isDrawingUnitCircle) {
      drawUnitCircle();
    }

    if (this.isDrawingRealAxis) {
      drawRealAxis(realMin, realMax);
    }

    if (this.isDrawingImaginaryAxis) {
      drawImaginaryAxis(imaginaryMin, imaginaryMax);
    }
  }

  /**
   * (-1,0)の点を描画します。
   * 
   * @param realMin 実部の最小値
   * @param realMax 実部の最大値
   * @param imagMin 虚部の最小値
   * @param imagMax 虚部の最大値
   */
  private void drawMinusOneZeroPoint(final double realMin, final double realMax, final double imagMin, final double imagMax) {
    final double maxRadius = 0.01;
    final double radius = Math.min(Math.min(realMax - realMin, imagMax - imagMin) / 100, maxRadius);
    final double centerX = -1;
    final double centerY = 0;

    drawCircle(centerX, centerY, radius);
  }

  /**
   * 単位円を描画します。
   */
  private void drawUnitCircle() {
    final double radius = 1;
    final double centerX = 0;
    final double centerY = 0;

    drawCircle(centerX, centerY, radius);
  }

  /**
   * 円を描画します。
   * 
   * @param centerX 円の中心のx座標
   * @param centerY 円の中心のy座標
   * @param radius 円の半径
   */
  private void drawCircle(final double centerX, final double centerY, final double radius) {
    final int size = this.reals.getColumnSize();
    final DoubleMatrix localReal = new DoubleMatrix(1, size);
    final DoubleMatrix localImag = new DoubleMatrix(1, size);

    for (int i = 0; i < size; i++) {
      final double theta = 2 * Math.PI / (size - 1) * i;
      localReal.setElement(1, i + 1, centerX + radius * Math.cos(theta));
      localImag.setElement(1, i + 1, centerY + radius * Math.sin(theta));
    }

    final int lineSize = this.reals.getRowSize();

    this.reals = this.reals.appendDown(localReal);
    this.imags = this.imags.appendDown(localImag);

    final SystemInputOutputKey key = new SystemInputOutputKey(lineSize + 1, lineSize + 1);
    this.lineNames.put(key, ""); //$NON-NLS-1$
    this.lineNumbers.put(key, Integer.valueOf(lineSize + 1));
  }

  /**
   * 実軸を描画します。
   * 
   * @param min 実部データの最小値
   * @param max 実部データの最大値
   */
  private void drawRealAxis(final double min, final double max) {
    double reMin = min;
    double reMax = max;

    if (0.1 < Math.abs(reMax) || 0.1 < Math.abs(reMin)) {
      if (reMax < 1) {
        reMax = 1;
      } else if (100 < reMax) {
        reMax = 100;
      } else {
        reMax = Math.ceil(reMax / 10) * 10;
      }
      if (-1 < reMin) {
        reMin = -2;
      } else if (reMin < -100) {
        reMin = -100;
      } else {
        reMin = Math.floor(reMin / 10) * 10;
      }
    }

    // Real axis
    this.graph.setHolding(true);
    this.graph.setXRange(reMin, reMax);
    final double grid = Math.pow(10, Math.floor(Math.log10((reMax - reMin) / 10))) * 2;
    this.graph.setXTics(reMin, grid, reMax);
    this.graph.plot(new DoubleMatrix(new double[] {reMin, reMax}), new DoubleMatrix(1, 2), new String[] {""}, new String[0], new String[] {"with lines 1 0"}); //$NON-NLS-1$ //$NON-NLS-2$
    this.graph.setHolding(false);
  }

  /**
   * 虚軸を描画します。
   * 
   * @param min 虚部データの最小値
   * @param max 虚部データの最大値
   */
  private void drawImaginaryAxis(final double min, final double max) {
    double imMin = min;
    double imMax = max;

    if (0.1 < Math.abs(imMax) || 0.1 < Math.abs(imMin)) {
      if (imMax < 1) {
        imMax = 1;
      } else if (100 < imMax) {
        imMax = 100;
      } else {
        imMax = Math.ceil(imMax / 10) * 10;
      }
      if (-1 < imMin) {
        imMin = -1;
      } else if (imMin < -100) {
        imMin = -100;
      } else {
        imMin = Math.floor(imMin / 10) * 10;
      }
    }

    // Imaginary axis
    this.graph.setHolding(true);
    this.graph.setYRange(imMin, imMax);
    final double grid = Math.pow(10, Math.floor(Math.log10((imMax - imMin) / 10))) * 2;
    this.graph.setYTics(imMin, grid, imMax);
    this.graph.plot(new DoubleMatrix(1, 2), new DoubleMatrix(new double[] {-imMax, imMax}), new String[] {""}, new String[0], new String[] {"with lines 1 0"}); //$NON-NLS-1$ //$NON-NLS-2$
    this.graph.setHolding(false);
  }

  /**
   * @see org.mklab.tool.control.Plotter#setGraphLineWidth(int, int)
   */
  @Override
  public void setGraphLineWidth(final int lineNumber, final int width) {
    if (this.graph != null) {
      this.graph.setLineWidth(lineNumber, width);
    }
  }

  /**
   * @see org.mklab.tool.control.Plotter#getGraphLineWidth(int)
   */
  @Override
  public int getGraphLineWidth(final int lineNumber) {
    return this.graph.getLineWidth(lineNumber);
  }

  /**
   * @see org.mklab.tool.control.Plotter#setGraphLineType(int, int)
   */
  @Override
  public void setGraphLineType(final int lineNumber, final int type) {
    if (this.graph != null) {
      this.graph.setLineType(lineNumber, type);
    }
  }

  /**
   * @see org.mklab.tool.control.Plotter#getGraphLineType(int)
   */
  @Override
  public int getGraphLineType(final int lineNumber) {
    return this.graph.getLineType(lineNumber);
  }

  /**
   * @see org.mklab.tool.control.Plotter#setGraphLineVisible(int, boolean)
   */
  @Override
  public void setGraphLineVisible(final int lineNumber, final boolean visible) {
    if (this.graph != null) {
      this.graph.setLineVisible(lineNumber, visible);
    }
  }

  /**
   * @see org.mklab.tool.control.Plotter#isGraphLineVisible(int)
   */
  @Override
  public boolean isGraphLineVisible(final int lineNumber) {
    return this.graph.isLineVisible(lineNumber);
  }

  /**
   * @see org.mklab.tool.control.Plotter#setGraphLineName(int, java.lang.String)
   */
  @Override
  public void setGraphLineName(final int lineNumber, final String name) {
    if (this.graph != null) {
      this.graph.setLineName(lineNumber, name);
    }
  }

  /**
   * @see org.mklab.tool.control.Plotter#getGraphLineName(int)
   */
  @Override
  public String getGraphLineName(final int lineNumber) {
    return this.graph.getLineName(lineNumber);
  }

  /**
   * @see org.mklab.tool.control.Plotter#setGraphFontSize(int)
   */
  @Override
  public void setGraphFontSize(final int fontSize) {
    if (this.graph != null) {
      this.graph.setFontSize(fontSize);
    }
  }

  /**
   * @see org.mklab.tool.control.Plotter#getGraphFontSize()
   */
  @Override
  public int getGraphFontSize() {
    return this.graph.getFontSize();
  }

  /**
   * 線のプロパティを保存するか設定します。
   * 
   * @param isKeeping 線のプロパティを保存するならばtrue、そうでなければfalse
   */
  public void setKeepingLineProperties(final boolean isKeeping) {
    this.graph.setKeepingLineProperties(isKeeping);
  }

  /**
   * 単位円を描くか判定します。
   * 
   * @return 単位円を描くならばtrue
   */
  public boolean isDrawingUnitCircle() {
    return this.isDrawingUnitCircle;
  }

  /**
   * 単位円を描くから設定します。
   * 
   * @param isDrawing 単位円を描くならばtrue
   */
  public void setDrawingUnitCircle(final boolean isDrawing) {
    this.isDrawingUnitCircle = isDrawing;
  }

  /**
   * (-1,0)点を描くか判定します。
   * 
   * @return (-1,0)点を描くならばtrue
   */
  public boolean isDrawingMinusOneZeroPoint() {
    return this.isDrawingMinusOneZeroPoint;
  }

  /**
   * (-1,0)点を描くから設定します。
   * 
   * @param isDrawing (-1,0)点を描くならばtrue
   */
  public void setDrawingMinusOneZeroPoint(final boolean isDrawing) {
    this.isDrawingMinusOneZeroPoint = isDrawing;
  }
}