{**********************************************}
{   TeeChart Delphi Component Library          }
{   TSmithSeries Component                     }
{                                              }
{   Copyright (c) 2000-2020 by Marjan Slatinek }
{        All Rights Reserved                   }
{**********************************************}
unit TeeSmith;
{$I TeeDefs.inc}

interface

uses {$IFNDEF LINUX}
     {$IFNDEF FMX}
     Windows, Messages,
     {$ENDIF}
     {$ENDIF}
     SysUtils, Classes,

     {$IFDEF D16}
     System.UITypes,
     {$ENDIF}

     {$IFDEF FMX}
     FMX.Types, System.Types,

     {$IFDEF D19}
     FMX.Graphics,
     {$ENDIF}

     {$ELSE}
     Graphics,
     {$ENDIF}

     {$IFDEF FMX}
     FMXTee.Engine, FMXTee.Chart, FMXTee.Canvas, FMXTee.Series,
     FMXTee.Procs, FMXTee.Series.Polar
     {$ELSE}
     TeeProcs, TeEngine, Chart, Series, TeCanvas, TeePolar
     {$ENDIF};

type
  TSmithSeries=class(TCustomCircledSeries)
  private
    FImagSymbol : String;

    IFirstNonNull : Boolean;

    OldX,OldY   : Integer;

    function GetResistanceValues: TChartValueList;
    function GetReactance: TChartValueList;
    Function GetCPen:TChartPen;
    Function GetRPen:TChartPen;
    Function GetCLabels:Boolean;
    Function GetRLabels:Boolean;
    procedure SetResistanceValues(Value: TChartValueList);
    procedure SetReactance(Value: TChartValueList);
    procedure SetRPen(const Value: TChartPen);
    procedure SetCPen(const Value: TChartPen);
    procedure SetCLabels(const Value: Boolean);
    procedure SetRLabels(const Value: Boolean);
    function GetCLabelsFont: TTeeFont;
    function GetRLabelsFont: TTeeFont;
    procedure SetCLabelsFont(const Value: TTeeFont);
    procedure SetRLabelsFont(const Value: TTeeFont);
    procedure SetImagSymbol(Const Value:String);
  protected
    procedure AddSampleValues(NumValues: Integer; OnlyMandatory:Boolean=False); override;
    procedure DoBeforeDrawValues; override;
    procedure DrawValue(ValueIndex: Integer); override;
    class Function GetEditorClass:String; override;
    function GetXCircleLabel(Const Reactance:Double):String;
    procedure LinePrepareCanvas(ValueIndex:Integer);
    Procedure PrepareForGallery; override;
  public
    Constructor Create(AOwner: TComponent); override;

    function AddPoint(Const Resist,React: Double; Const ALabel: String='';
                      AColor: TColor=clTeeColor): Integer;
    function CalcXPos(ValueIndex: Integer): Integer; override;
    function CalcYPos(ValueIndex: Integer): Integer; override;
    function Clicked(X,Y:Integer):Integer; override;
    procedure DrawRCircle(Const Value:Double; Z:Integer; ShowLabel:Boolean=True);
    procedure DrawXCircle(Const Value:Double; Z:Integer; ShowLabel:Boolean=True);
    procedure PosToZ(X,Y: Integer; out Resist,React: Double);

    procedure ZToPos(const ValueIndex:Integer; out X,Y: Integer); overload; {$IFDEF D9}inline;{$ENDIF}
    procedure ZToPos(const Resist,React: TChartValue; out X,Y: Integer); overload;
    procedure ZToPos(const Resist,React: TChartValue; AXRadius,AYRadius:Integer; out X,Y: Integer); overload;
  published
    property Active;
    property CCirclePen:TChartPen read GetCPen write SetCPen;
    property CircleBackColor;
    property CircleGradient;
    property CirclePen;
    property CLabels:Boolean read GetCLabels write SetCLabels;
    property CLabelsFont:TTeeFont read GetCLabelsFont write SetCLabelsFont;
    property ColorEachLine;
    property ColorEachPoint;
    property DrawStyle;
    property ImagSymbol:String read FImagSymbol write SetImagSymbol;

    property ResistanceValues:TChartValueList read GetResistanceValues write SetResistanceValues;
    property ReactanceValues:TChartValueList read GetReactance write SetReactance;
    property Pen;
    property Pointer;
    property PointerBehind;
    property RCirclePen:TChartPen read GetRPen write SetRPen;
    property RLabels:Boolean read GetRLabels write SetRLabels;
    property RLabelsFont:TTeeFont read GetRLabelsFont write SetRLabelsFont;
    property TreatNulls;

    property OnGetPointerStyle;
  end;

implementation

Uses {$IFDEF FMX}
     FMXTee.Constants, FMXTee.ProConstants
     {$ELSE}
     {$IFDEF LCL}
     LCLType, Types,
     {$ENDIF}
     TeeProCo, TeeConst
     {$ENDIF};

{ TSmithSeries }

Constructor TSmithSeries.Create(AOwner: TComponent);
begin
  inherited;

  XValues.Name          := TeeMsg_SmithResistance;
  XValues.Order         := loNone;
  YValues.Name          := TeeMsg_SmithReactance;
  FImagSymbol           := 'i';
  IAdjustCircleForMarks := False;

  Shadow.Visible:=False;
end;

procedure TSmithSeries.SetCLabels(Const Value: Boolean);
begin
  if Assigned(GetVertAxis) then
     GetVertAxis.Labels:=Value;
end;

procedure TSmithSeries.SetRLabels(Const Value: Boolean);
begin
  if Assigned(GetHorizAxis) then
     GetHorizAxis.Labels:=Value;
end;

procedure TSmithSeries.SetRPen(const Value: TChartPen);
begin
  if Assigned(GetVertAxis) then
     GetVertAxis.Grid.Assign(Value);
end;

procedure TSmithSeries.SetCPen(const Value: TChartPen);
begin
  if Assigned(GetHorizAxis) then
     GetHorizAxis.Grid.Assign(Value);
end;

function TSmithSeries.GetResistanceValues: TChartValueList;
begin
  Result:=XValues;
end;

function TSmithSeries.GetReactance: TChartValueList;
begin
  Result:=YValues;
end;

procedure TSmithSeries.SetResistanceValues(Value: TChartValueList);
begin
  SetXValues(Value);
end;

procedure TSmithSeries.SetReactance(Value: TChartValueList);
begin
  SetYValues(Value);
end;

procedure TSmithSeries.LinePrepareCanvas(ValueIndex:Integer);
begin
  if Pen.Visible then
  begin
    if ValueIndex=-1 then
       ParentChart.Canvas.AssignVisiblePenColor(Pen,SeriesColor)
    else
       ParentChart.Canvas.AssignVisiblePenColor(Pen,ValueColor[ValueIndex]);
  end
  else
    ParentChart.Canvas.Pen.Style:=psClear;

  ParentChart.Canvas.BackMode:=cbmTransparent;
end;

procedure TSmithSeries.ZToPos(const ValueIndex:Integer; out X,Y: Integer);
begin
  ZToPos(XValues.Value[ValueIndex],YValues.Value[ValueIndex],X,Y);
end;

{ impendance to Position}
{ (GRe,GIm)=(1-z)/(1+z)                   }
procedure TSmithSeries.ZToPos(Const Resist,React: TChartValue; out X,Y: Integer);
begin
  ZToPos(Resist,React,AxisXRadius,AxisYRadius,X,Y);
end;

{ impendance to Position}
{ (GRe,GIm)=(1-z)/(1+z)                   }
procedure TSmithSeries.ZToPos(Const Resist,React: TChartValue; AXRadius,AYRadius:Integer; out X,Y: Integer);
var GRe    : TChartValue;
    GIm    : TChartValue;
    Norm2  : TChartValue;
    InvDen : TChartValue;
    tmp    : TChartValue;
begin
  Norm2 := Sqr(Resist)+Sqr(React);

  tmp:=(Norm2+2*Resist+1);

  if tmp=0 then
     InvDen :=1
  else
     InvDen := 1/tmp;

  GRe := (Norm2-1)*InvDen;
  GIm := 2*React*InvDen;
  X := CircleXCenter+Round(GRe*AXRadius);
  Y := CircleYCenter-Round(GIm*AYRadius);
end;

{ Position to impendance}
{ (ZRe,ZIm)=(1+gamma)/(1-gamma) }
procedure TSmithSeries.PosToZ(X,Y: Integer; out Resist,React: Double);
var GRe    : Double;
    GIm    : Double;
    Norm2  : Double;
    InvDen : Double;
begin
  X := X-CircleXCenter;
  Y := CircleYCenter-Y;
  GRe := X/AxisXRadius;
  GIm := Y/AxisYRadius;
  Norm2 := Sqr(GRe)+Sqr(GIm);
  InvDen := 1/(Norm2-2*GRe+1);
  Resist := (1-Norm2)*InvDen;
  React := 2*GIm*InvDen;
end;

Procedure TSmithSeries.DrawRCircle(Const Value:Double; Z:Integer;
                                   ShowLabel: Boolean);
var tmp       : Double;
    HalfXSize,
    HalfYSize : TCoordinate;
begin
  if Value<>-1 then
  begin
    { Transform R }
    tmp := 1/(1+Value);
    HalfXSize := {$IFNDEF FMX}Round{$ENDIF}(tmp*AxisXRadius);
    HalfYSize := {$IFNDEF FMX}Round{$ENDIF}(tmp*AxisYRadius);

    { Circles are always right aligned }
    with ParentChart.Canvas do
    begin
      Brush.Style:=bsClear;
      EllipseWithZ(CircleRect.Right-2*HalfXSize,CircleYCenter-HalfYSize,
                   CircleRect.Right,CircleYCenter+HalfYSize,Z);
    end;

    if ShowLabel and GetHorizAxis.Visible then
    begin
      ParentChart.PrepareFont3D(RLabelsFont);

      with ParentChart.Canvas do
      begin
        TextAlign:=TA_CENTER;
        BackMode:=cbmTransparent;
        TextOut3D( {$IFDEF FMX}Round{$ENDIF}(CircleXCenter+AxisXRadius-2*HalfXSize),
                   CircleYCenter,EndZ,FloatToStr(Value));
      end;
    end;
  end;
end;

procedure TSmithSeries.DrawValue(ValueIndex: Integer);
var X : Integer;
    Y : Integer;
begin
  if IsPainted(ValueIndex) then
  begin
    ZToPos(ValueIndex,X,Y);

    if not IFirstNonNull then
    begin
      ParentChart.Canvas.MoveTo3D(X,Y,StartZ);  { <-- first point }
      IFirstNonNull:=True;
    end
    else
    if (X<>OldX) or (Y<>OldY) then
    begin
      if not IsNull(ValueIndex) then
         LinePrepareCanvas(ValueIndex);

      ParentChart.Canvas.LineTo3D(X,Y,StartZ);
    end;

    OldX:=X;
    OldY:=Y;
  end
  else
  if TreatNulls=tnSkip then
     IFirstNonNull:=False;
end;

procedure TSmithSeries.DrawXCircle(Const Value:Double; Z: Integer; ShowLabel: boolean);

  Procedure DrawXCircleLabel(const XVal: Double; X,Y: Integer);
  var Angle     : Double;
      tmpHeight : Integer;
      tmpWidth  : Integer;
      tmpSt     : String;
  begin
    if GetVertAxis.Visible and ShowLabel then
    with ParentChart.Canvas do
    begin
      ParentChart.PrepareFont3D(CLabelsFont);

      tmpHeight:=FontHeight;
      tmpSt:=GetXCircleLabel(XVal);

      Angle:=PointToAngle(X,Y)*57.29577;

      if Angle>=360 then Angle:=Angle-360;
      begin
        if (Angle=0) or (Angle=180) then Dec(Y,tmpHeight div 2)
        else
        if (Angle>0) and (Angle<180) then Dec(Y,tmpHeight);

        if (Angle=90) or (Angle=270) then TextAlign:=ta_Center
        else
        if (Angle>90) and (Angle<270) then TextAlign:=ta_Right
                                      else TextAlign:=ta_Left;

        tmpWidth:=TextWidth('0') div 2;

        if Angle=0 then Inc(x,tmpWidth) else
        if Angle=180 then Dec(x,tmpWidth);

        BackMode:=cbmTransparent;
        TextOut3D(X,Y,EndZ,tmpSt);
      end;
    end;
  end;

var X1,X2,X3,X4,
    Y1,Y2,Y3,Y4,
    tmpPen,
    XLabel,
    YLabel      : Integer;
    HalfXSize,
    HalfYSize   : Integer;
    InvValue    : Double;
    tmpIs3D     : Boolean;
begin
  tmpPen:=CirclePen.HalfWidth;

  if Value <> 0 then
  with ParentChart.Canvas do
  begin
    InvValue := 1/Value;

    ZToPos(0,Value,X4,Y4); // Endpos

    if ShowLabel then
    begin
      ZToPos(0,Value,XRadius+tmpPen,YRadius+tmpPen,XLabel,YLabel);
      DrawXCircleLabel(Value,XLabel,YLabel);
    end;

    ZToPos(100,Value,X3,Y3); // Startpos

    // ellipse bounding points
    HalfXSize := Round(InvValue*AxisXRadius);
    HalfYSize := Round(InvValue*AxisYRadius);

    X1 := {$IFDEF FMX}Round{$ENDIF}(CircleRect.Right - HalfXSize);
    X2 := {$IFDEF FMX}Round{$ENDIF}(CircleRect.Right + HalfXSize);
    Y1 := CircleYCenter;
    Y2 := Y1-2*HalfYSize;

    // TODO PENDING:
    // Canvas.Arc support for 3D non-orthogonal Arc lines

    // Fixed bug 253
    tmpIs3D:=ParentChart.View3D and ParentChart.View3DOptions.Orthogonal;

    if tmpIs3D then
       Arc(X1,Y1,X2,Y2,X4,Y4,X3,Y3,EndZ)
    else
       Arc(X1,Y1,X2,Y2,X4,Y4,X3,Y3);

    ZToPos(0,-Value,X4,Y4); // Endpos

    if tmpIs3D then
       Arc(X1,Y1,X2,Y1+2*HalfYSize,X3,Y3,X4,Y4,EndZ)
    else
       Arc(X1,Y1,X2,Y1+2*HalfYSize,X3,Y3,X4,Y4);

    if ShowLabel then
    begin
      ZToPos(0,-Value,XRadius+tmpPen,YRadius+tmpPen,XLabel,YLabel);
      DrawXCircleLabel(-Value,XLabel,YLabel);
    end;
  end
  else
  begin { special case then reactance is zero }
    X1 := {$IFDEF FMX}Round{$ENDIF}(CircleRect.Left);
    X2 := {$IFDEF FMX}Round{$ENDIF}(CircleRect.Right);
    Y1 := CircleYCenter;

    ParentChart.Canvas.LineWithZ(X1,Y1,X2,Y1,MiddleZ);

    if ShowLabel then
       DrawXCircleLabel(0,X1-tmpPen,Y1);
  end;
end;

Procedure TSmithSeries.AddSampleValues(NumValues:Integer; OnlyMandatory:Boolean=False);
var t : Integer;
begin
  for t:=0 to NumValues-1 do
      AddPoint( 6.5*t/NumValues,(RandomValue(t)+3.8)/NumValues);
end;

{ NOTE : the assumption is all points are normalized by Z0 }
function TSmithSeries.AddPoint(Const Resist,React: Double; Const ALabel: String; AColor: TColor): Integer;
begin
  result:=AddXY(Resist,React,ALabel,AColor);
end;

function TSmithSeries.CalcXPos(ValueIndex: Integer): Integer;
var DummyY : Integer;
begin
  ZToPos(ValueIndex,Result,DummyY);
end;

function TSmithSeries.CalcYPos(ValueIndex: Integer): Integer;
var DummyX : Integer;
begin
  ZToPos(ValueIndex,DummyX,Result);
end;

Function TSmithSeries.Clicked(X,Y:Integer):Integer;
var t : Integer;
begin
  if Assigned(ParentChart) then
     ParentChart.Canvas.Calculate2DPosition(X,Y,StartZ);

  result:=inherited Clicked(X,Y);

  if (result=TeeNoPointClicked) and
     (FirstValueIndex>-1) and (LastValueIndex>-1) then

    if Pointer.Visible then
    for t:=FirstValueIndex to LastValueIndex do
        if (Abs(CalcXPos(t)-X)<Pointer.HorizSize) and
           (Abs(CalcYPos(t)-Y)<Pointer.VertSize) then
        begin
          result:=t;
          break;
        end;
end;

procedure TSmithSeries.DoBeforeDrawValues;

  procedure DrawXCircleGrid;
  const DefaultX: Array [0..11] of Double =
        (0,0.1,0.3,0.5,0.8,1,1.5,2,3,5,7,10);
  var i : Integer;
  begin
    with ParentChart.Canvas do
    begin
      Brush.Style:=bsClear;
      AssignVisiblePen(CCirclePen);
      BackMode:=cbmTransparent;
    end;

    for i := 0 to High(DefaultX) do
        DrawXCircle(DefaultX[i],MiddleZ,CLabels);
  end;

  procedure DrawRCircleGrid;
  const DefaultR: Array [0..6] of Double =
        (0,0.2,0.5,1,2,5,10);
  var i : Integer;
  begin
    with ParentChart.Canvas do
    begin
      Brush.Style:=bsClear;
      AssignVisiblePen(RCirclePen);
      BackMode:=cbmTransparent;
    end;

    for i := 0 to High(DefaultR) do
        DrawRCircle(DefaultR[i],MiddleZ,RLabels);
  end;

  procedure DrawAxis;
  begin
    if GetVertAxis.Visible then DrawXCircleGrid;
    if GetHorizAxis.Visible then DrawRCircleGrid;
  end;

  Procedure DrawCircle;
  var tmpX,
      tmpY : Integer;
  begin
    DrawCircleShadow;

    With ParentChart.Canvas do
    Begin
      AssignBrush(CircleBrush, CalcCircleBackColor);
      AssignVisiblePen(CirclePen);

      tmpX:=CircleWidth div 2;
      tmpY:=CircleHeight div 2;
      EllipseWithZ( CircleXCenter-tmpX,CircleYCenter-tmpY,
                    CircleXCenter+tmpX,CircleYCenter+tmpY, EndZ);
    end;
  end;

var t   : Integer;
    tmp : Integer;
    First : Boolean;
Begin
  tmp:=-CirclePen.HalfWidth;
  if tmp<>0 then
     InflateRect(ParentChart.ChartRect,tmp,tmp);

  First:=False;

  With ParentChart do
  for t:=0 to SeriesCount-1 do
  if Series[t].Active and (Series[t] is Self.ClassType) then
  begin
    if Series[t]=Self then
    begin
      if not First then
      begin
         if CLabels then
         begin
           With ChartRect do
           begin
             tmp:=Canvas.FontHeight+2;
             Top:=Top+tmp;
             Bottom:=Bottom-tmp;
             tmp:=Canvas.TextWidth('360');
             Left:=Left+tmp;
             Right:=Right-tmp;
           end;
         end;
      end;

      break;
    end;

    First:=True;
  end;

  inherited;

  First:=False;
  With ParentChart do
  for t:=0 to SeriesCount-1 do
  if Series[t].Active and (Series[t] is Self.ClassType) then
  begin
    if Series[t]=Self then
    begin
      if not First then
      begin
        DrawCircle;

        if Axes.Visible and Axes.Behind then
           DrawAxis;
      end;

      break;
    end;

    First:=True;
  end;

  IFirstNonNull:=False;
end;

class function TSmithSeries.GetEditorClass: String;
begin
  result:='TSmithSeriesEdit';
end;

function TSmithSeries.GetXCircleLabel(Const Reactance: Double): String;
begin
  Result:=FloatToStr(Reactance)+FImagSymbol;
end;

function TSmithSeries.GetCLabels: Boolean;
begin
  if Assigned(GetVertAxis) then
     result:=GetVertAxis.Labels
  else
     result:=True;
end;

function TSmithSeries.GetCPen: TChartPen;
begin
  if Assigned(GetVertAxis) then
     result:=GetVertAxis.Grid
  else
     result:=Pen;  // workaround
end;

function TSmithSeries.GetRLabels: Boolean;
begin
  if Assigned(GetHorizAxis) then
     result:=GetHorizAxis.Labels
  else
     result:=True;
end;

function TSmithSeries.GetRPen: TChartPen;
begin
  if Assigned(GetHorizAxis) then
     result:=GetHorizAxis.Grid
  else
     result:=Pen; // workaround
end;

function TSmithSeries.GetCLabelsFont: TTeeFont;
begin
  if Assigned(GetVertAxis) then
     result:=GetVertAxis.LabelsFont
  else
     result:=Marks.Font; // workaround
end;

function TSmithSeries.GetRLabelsFont: TTeeFont;
begin
  if Assigned(GetHorizAxis) then
     result:=GetHorizAxis.LabelsFont
  else
     result:=Marks.Font; // workaround
end;

procedure TSmithSeries.SetCLabelsFont(const Value: TTeeFont);
begin
  if Assigned(GetVertAxis) then
     GetVertAxis.LabelsFont:=Value
end;

procedure TSmithSeries.SetRLabelsFont(const Value: TTeeFont);
begin
  if Assigned(GetHorizAxis) then
     GetHorizAxis.LabelsFont:=Value;
end;

procedure TSmithSeries.SetImagSymbol(const Value: String);
begin
  SetStringProperty(FImagSymbol,Value);
end;

procedure TSmithSeries.PrepareForGallery;
begin
  inherited;

  With ParentChart do
  begin
    Chart3DPercent:=5;
    RightAxis.Labels:=False;
    TopAxis.Labels:=False;

    With View3DOptions do
    begin
      Orthogonal:=False;
      Elevation:=360;
      Zoom:=90;
    end;
  end;
end;

initialization
  RegisterTeeSeries(TSmithSeries, @TeeMsg_GallerySmith,
                                  @TeeMsg_GalleryExtended,2);
finalization
  UnRegisterTeeSeries([TSmithSeries]);
end.
