{*********************************************}
{  TeeChart Gauge Series Types                }
{  Copyright (c) 2002-2020 by Steema Software }
{  All Rights Reserved                        }
{*********************************************}
unit TeeGauges;
{$I TeeDefs.inc}

interface

uses
  {$IFNDEF LINUX}
  Windows,
  {$ENDIF}
  Classes,

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

  {$IFDEF D9}
  Types,
  {$ENDIF}

  Series, TeEngine;

const
  TeeHandDistance=3;  // default pixels distance from center to labels or axis

type
  THandStyle=(hsLine,hsTriangle);
  TGaugeShapeStyle=(gsSlice, gsEllipse, gsPolygon);

  TGaugeSeries=class(TCircledSeries)
  private
    FAngle         : Double;
    FCenter        : TSeriesPointer;
    FCirclePen     : TChartPen;
    FDistance      : Integer;
    FEndPoint      : TSeriesPointer;
    FFullRepaint   : Boolean;
    FHandWidth     : Integer;
    FLabelsInside  : Boolean;
    FMax           : Double;
    FMin           : Double;
    FMinorDistance : Integer;
    FShapeStyle    : TGaugeShapeStyle;
    FStyle         : THandStyle;

    FOnChange      : TNotifyEvent;

    ICenter   : TPoint;
    ILastModifiedChart : TCustomAxisPanel;

    function AngleOfValue(const Value:Single):Single;
    procedure CalcLinePoints(out P0,P1:TPoint);
    function CalcPoint(const Angle:Double; Center:TPoint;
                       const RadiusX,RadiusY:Double):TPoint;
    procedure CustomizeChart;
    procedure DrawValueLine;
    function GetValue: Double;

    procedure SetAngle(const Value: Double);
    procedure SetCenter(const Value: TSeriesPointer);
    procedure SetCirclePen(const Value: TChartPen);
    procedure SetDistance(const Value: Integer);
    procedure SetEndPoint(const Value: TSeriesPointer);
    procedure SetFullRepaint(const Value: Boolean);
    procedure SetHandWidth(const Value: Integer);
    procedure SetLabelsInside(const Value: Boolean);
    procedure SetMax(const AValue: Double);
    procedure SetMin(const AValue: Double);
    procedure SetMinorDistance(const Value: Integer);
    procedure SetShapeStyle(const Value: TGaugeShapeStyle);
    procedure SetStyle(const Value: THandStyle);
    procedure SetValue(const AValue: Double);
    function ShouldDoXor:Boolean;
    function SizePointer(const APointer:TSeriesPointer):Integer;
  protected
    Procedure AddSampleValues(NumValues:Integer; OnlyMandatory:Boolean=False); override;
    Procedure AssignFormatting(Source:TChartSeries); override;
    Procedure DoBeforeDrawValues; override;
    procedure DrawAllValues; override;
    Procedure GalleryChanged3D(Is3D:Boolean); override;
    class Function GetEditorClass:String; override;
    Procedure NotifyNewValue(Sender:TChartSeries; ValueIndex:Integer); override;
    Procedure PrepareForGallery; override;
    Procedure SetActive(Value:Boolean); override;
    Procedure SetParentChart(Const Value:TCustomAxisPanel); override;
  public
    Constructor Create(AOwner:TComponent); override;
    Destructor Destroy; override;

    Procedure Assign(Source:TPersistent); override;

    function Axis:TChartAxis;
    Function Clicked(x,y:Integer):Integer; override;
    Function NumSampleValues:Integer; override;
    function MaxYValue:TChartValue; override;
    function MinYValue:TChartValue; override;
  published
    property Center:TSeriesPointer read FCenter write SetCenter;
    property Circled default True;
    property CircleGradient;
    property CirclePen:TChartPen read FCirclePen write SetCirclePen;
    property EndPoint:TSeriesPointer read FEndPoint write SetEndPoint;
    property FullRepaint:Boolean read FFullRepaint write SetFullRepaint default False;
    property Maximum:Double read FMax write SetMax;
    property Minimum:Double read FMin write SetMin;
    property MinorTickDistance:Integer read FMinorDistance write SetMinorDistance default 0;
    property HandDistance:Integer read FDistance write SetDistance default TeeHandDistance;
    property HandStyle:THandStyle read FStyle write SetStyle default hsLine;
    property HandWidth:Integer read FHandWidth write SetHandWidth default 0;
    property LabelsInside:Boolean read FLabelsInside write SetLabelsInside default True;
    property Pen;
    property RotationAngle default 135;
    property ShowInLegend default False;
    property ShapeStyle:TGaugeShapeStyle read FShapeStyle write SetShapeStyle default gsSlice;
    property TotalAngle:Double read FAngle write SetAngle;
    property Value:Double read GetValue write SetValue;

    { events }
    property OnChange:TNotifyEvent read FOnChange write FOnChange;
  end;

implementation

uses
  Graphics, SysUtils, Math,

  {$IFDEF LCL}
  LCLType, Types,
  {$ENDIF}

  Chart, TeCanvas, TeeProcs, TeeConst, TeeProCo;

{ TGaugeSeries }

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

  FDistance:=TeeHandDistance;

  Circled:=True;
  ShowInLegend:=False;
  FLabelsInside:=True;

  FCenter:=TSeriesPointer.Create(Self);
  with FCenter do
  begin
    Brush.Style:=bsSolid;
    Color:=clBlack;
    Style:=psCircle;
    HorizSize:=8;
    VertSize:=8;
    Gradient.Visible:=True;
  end;

  FEndPoint:=TSeriesPointer.Create(Self);
  with FEndPoint do
  begin
    Brush.Style:=bsSolid;
    Color:=clWhite;
    Style:=psCircle;
    HorizSize:=3;
    VertSize:=3;
    Visible:=False;
  end;

  Shadow.Visible:=False;

  FCirclePen:=TChartPen.Create(CanvasChanged);
  
  Add(0);
  Maximum:=100;
  TotalAngle:=90;
  RotationAngle:=135;
end;

Destructor TGaugeSeries.Destroy;
begin
  FEndPoint.Free;
  FCenter.Free;
  FCirclePen.Free;
  inherited;
end;

function TGaugeSeries.AngleOfValue(const Value:Single):Single;
var tmpRange : Double;
begin
  tmpRange:=Maximum-Minimum;

  if tmpRange=0 then
     result:=90+RotationAngle
  else
     result:=360-(TotalAngle-((Value-Minimum)*TotalAngle/tmpRange))+RotationAngle;
end;

Function TGaugeSeries.CalcPoint(const Angle:Double; Center:TPoint;
                                const RadiusX,RadiusY:Double):TPoint;
var tmpSin,
    tmpCos : Extended;
begin
  SinCos(Angle,tmpSin,tmpCos);
  result.X:=Center.X-Round(RadiusX*tmpCos);
  result.Y:=Center.Y-Round(RadiusY*tmpSin);
end;

type
  TTeePanelAccess=class(TCustomTeePanel);

procedure TGaugeSeries.DrawAllValues;
var
  tmpStep2 : Double;
  tmpXRad  : Double;
  tmpYRad  : Double;

  procedure DrawMinorTicks(const Value:Double);
  var t : Integer;
      tmp : Double;
      tmpValue : Double;
  begin
    for t:=1 to Axis.MinorTickCount do
    begin
      tmpValue:=(Value+t*tmpStep2);

      if tmpValue>Maximum then break
      else
      begin
        tmp:=TeePiStep*AngleOfValue(tmpValue);

        ParentChart.Canvas.Line( CalcPoint(tmp,ICenter,tmpXRad,tmpYRad),
              CalcPoint(tmp,ICenter,XRadius-MinorTickDistance,YRadius-MinorTickDistance));
      end;
    end;
  end;

  procedure DrawBackground;
  var tmpA,
      tmpB : Single;
  begin
    DrawCircleShadow;

    ParentChart.Canvas.AssignBrush(CircleBrush);
    ParentChart.Canvas.AssignVisiblePen(CirclePen);

    if (FShapeStyle=gsEllipse) or (TotalAngle=360) then
       ParentChart.Canvas.EllipseWithZ(CircleRect,StartZ)
    else
    begin
      tmpA:=180-AngleOfValue(Maximum);
      tmpB:=180-AngleOfValue(Minimum);

      if tmpA<0 then tmpA:=360+tmpA;

      ParentChart.Canvas.Pie(CircleRect,tmpA,tmpB);
    end;
  end;

var tmp : Double;
    tmpAngle : Double;
    tmpSin,
    tmpCos : Extended;
    tmpS  : String;
    tmpValue,
    tmpStep : Double;
    tmpFontH,
    AIndex : Integer;
    P3   : TPoint;
    P4   : TPoint;
begin
  ICenter.X:=CircleXCenter;
  ICenter.Y:=CircleYCenter;

  tmpStep:=Axis.Increment;
  if tmpStep=0 then
     tmpStep:=(Maximum-Minimum)*0.25;

  if Brush.Style<>bsClear then
     DrawBackground;

  // center
  if FCenter.Visible then
  begin
    FCenter.PrepareCanvas(ParentChart.Canvas,FCenter.Color);
    FCenter.Draw(ICenter);
  end;

  // ticks and labels
  if Axis.Ticks.Visible or Axis.Labels then
  begin
    TTeePanelAccess(ParentChart).TryAssignFont(Axis.Items.Format.Font);
    ParentChart.Canvas.AssignVisiblePen(Axis.Ticks);
    ParentChart.Canvas.BackMode:=cbmTransparent;

    tmpXRad:=(XRadius-Axis.TickLength);
    tmpYRad:=(YRadius-Axis.TickLength);

    tmpFontH:=ParentChart.Canvas.FontHeight;

    if ((Maximum-Minimum)<>0) and (tmpStep<>0) then
    begin
      tmpValue:=Minimum;

      AIndex := 0;

      repeat
        tmpAngle:=AngleOfValue(tmpValue);
        tmp:=TeePiStep*tmpAngle;

        P3:=CalcPoint(tmp,ICenter,tmpXRad,tmpYRad);
        P4:=CalcPoint(tmp,ICenter,XRadius,YRadius);

        if Axis.Ticks.Visible then
           ParentChart.Canvas.Line(P3,P4);

        if Axis.Labels then
        begin
          tmpS:=FormatFloat(ValueFormat,tmpValue);

          if Assigned(ParentChart.OnGetAxisLabel) then
             ParentChart.OnGetAxisLabel(Axis,nil,AIndex,tmps); // TV52010353

          if not LabelsInside then
             P3:=CalcPoint(tmp,ICenter,XRadius+tmpFontH,YRadius+tmpFontH);

          SinCos(TeePiStep*tmpAngle,tmpsin,tmpCos);

          if LabelsInside then
          begin
            if tmpAngle>360 then tmpAngle:=tmpAngle-360;
            if tmpAngle<0 then tmpAngle:=360+tmpAngle;

            Inc(P3.x,Round(ParentChart.Canvas.TextWidth(tmpS)*0.5*tmpCos));
          end;

          if (tmpAngle>180) and (tmpAngle<360) then
             Inc(P3.y,Round(tmpFontH*tmpSin));

          ParentChart.Canvas.TextOut(P3.x,P3.y,tmpS);
          Inc(AIndex);
        end;

        tmpValue:=tmpValue+tmpStep;

      Until ((TotalAngle<360) and (tmpValue>Maximum)) or
            ((TotalAngle>=360) and (tmpValue>=Maximum));
    end;
  end;

  // minor ticks
  if Axis.MinorTicks.Visible and (Axis.MinorTickCount>0) then
  begin
    ParentChart.Canvas.AssignVisiblePen(Axis.MinorTicks);

    tmpXRad:=XRadius-Axis.MinorTickLength-MinorTickDistance;
    tmpYRad:=YRadius-Axis.MinorTickLength-MinorTickDistance;

    if tmpStep<>0 then
    begin
      tmpStep2:=tmpStep/(Axis.MinorTickCount+1);

      tmpValue:=Minimum;

      repeat
        DrawMinorTicks(tmpValue);
        tmpValue:=tmpValue+tmpStep;
      Until tmpValue>(Maximum-tmpStep);

      if tmpValue<Maximum then
         DrawMinorTicks(tmpValue);
    end;
  end;

  // axis
  if Axis.Axis.Visible then
  begin
    ParentChart.Canvas.AssignVisiblePen(Axis.Axis);

    with CircleRect do
         ParentChart.Canvas.Arc(Left,Top,Right,Bottom,180-AngleOfValue(Minimum),180-AngleOfValue(Maximum));
  end;

  // value line
  if Pen.Visible then
     DrawValueLine;
end;

Function TGaugeSeries.Clicked(x,y:Integer):Integer;
var P0,P1 : TPoint;
begin
  CalcLinePoints(P0,P1);

  if Assigned(ParentChart) then
     ParentChart.Canvas.Calculate2DPosition(X,Y,MiddleZ);

  if PointInLine(TeePoint(x,y),P0,P1,Pen.Width+1) then
     result:=0
  else
     result:=TeeNoPointClicked;
end;

function TGaugeSeries.SizePointer(const APointer:TSeriesPointer):Integer;
begin
  if APointer.Visible then
  begin
    result:=2*APointer.VertSize;

    if APointer.Pen.Visible then
       Inc(result,APointer.Pen.Width);
  end
  else result:=0;
end;

procedure TGaugeSeries.CalcLinePoints(out P0,P1:TPoint);
var tmp      : Double;
    tmpDist  : Integer;
begin
  tmp:=TeePiStep*AngleOfValue(Value);

  P1:=CalcPoint(tmp,ICenter,XRadius,YRadius);

  tmpDist:=HandDistance+SizePointer(FEndPoint);

  if Axis.Labels and FLabelsInside then
  begin
    TTeePanelAccess(ParentChart).TryAssignFont(Axis.Items.Format.Font);
    Inc(tmpDist,ParentChart.Canvas.FontHeight);
  end;

  if tmpDist>0 then
     P1:=PointAtDistance(ICenter,P1,tmpDist);

  if FCenter.Visible and (FCenter.Style=psCircle) then
     P0:=PointAtDistance(P1,ICenter,SizePointer(FCenter) div 2)
  else
     P0:=ICenter;
end;

procedure TGaugeSeries.DrawValueLine;
var tmp : Double;
    P4  : TPoint;
    tmpCenter : TPoint;
    tmpSize : Integer;
begin
  if Assigned(ParentChart) then
  begin
    CalcLinePoints(tmpCenter,P4);

    if ShouldDoXor then
       TeePrepareXorPen(ParentChart,Pen)
    else
       ParentChart.Canvas.AssignVisiblePen(Pen);

    if HandStyle=hsLine then
       ParentChart.Canvas.Line(tmpCenter,P4)
    else
    begin
      if HandWidth=0 then
         if Center.Visible then
            tmpSize:=Center.Size
         else
            tmpSize:=4
      else
         tmpSize:=HandWidth;

      tmp:=TeePiStep*AngleOfValue(Value);
      ParentChart.Canvas.Polygon([CalcPoint(tmp-Pi,tmpCenter,tmpSize,tmpSize),P4,CalcPoint(tmp+Pi,tmpCenter,tmpSize,tmpSize)]);
    end;

    // end point
    if FEndPoint.Visible then
    begin
      if ShouldDoXor then FEndPoint.Pen.Mode:=pmNotXor
                     else FEndPoint.Pen.Mode:=pmCopy;

      FEndPoint.PrepareCanvas(ParentChart.Canvas,FEndPoint.Color);

      if ShouldDoXor then
         ParentChart.Canvas.Brush.Style:=bsClear;

      P4:=PointAtDistance(ICenter,P4,-SizePointer(FEndPoint) div 2);
      FEndPoint.Draw(P4);
    end;

    if ShouldDoXor then
       ParentChart.Canvas.Pen.Mode:=pmCopy;
  end;
end;

procedure TGaugeSeries.SetAngle(const Value: Double);
begin
  SetDoubleProperty(FAngle,Value);
end;

procedure TGaugeSeries.SetCenter(const Value: TSeriesPointer);
begin
  FCenter.Assign(Value);
end;

procedure TGaugeSeries.SetDistance(const Value: Integer);
begin
  SetIntegerProperty(FDistance,Value);
end;

procedure TGaugeSeries.SetMax(const AValue: Double);
begin
  SetDoubleProperty(FMax,AValue);
  Value:=Math.Min(Maximum,Value);
end;

procedure TGaugeSeries.SetMin(const AValue: Double);
begin
  SetDoubleProperty(FMin,AValue);
  Value:=Math.Max(Minimum,Value);
end;

procedure TGaugeSeries.CustomizeChart;
begin
  with TCustomChart(ParentChart) do
  begin
    Walls.Visible:=False;
    View3D:=False;
    Frame.Hide;
  end;

  Axis.TickLength:=14;
  Axis.MinorTickLength:=6;
end;

procedure TGaugeSeries.SetParentChart(const Value: TCustomAxisPanel);
begin
  inherited;

  if not (csDestroying in ComponentState) then
  begin
    FCenter.ParentChart:=ParentChart;
    FEndPoint.ParentChart:=ParentChart;
  end;

  if Visible and Assigned(ParentChart) then
     CustomizeChart;
end;

procedure TGaugeSeries.SetStyle(const Value: THandStyle);
begin
  if FStyle<>Value then
  begin
    FStyle:=Value;
    Repaint;
  end;
end;

procedure TGaugeSeries.SetValue(const AValue: Double);
var tmpValidCanvas : Boolean;
begin
  if Value<>AValue then
  begin
    tmpValidCanvas:=Assigned(ParentChart) and
                    ParentChart.Showing and
                    (ParentChart.Canvas.ReferenceCanvas<>nil);

    if tmpValidCanvas and ShouldDoXor then
       DrawValueLine;

    MandatoryValueList.Value[0]:=AValue;

    if tmpValidCanvas then
       if ShouldDoXor then
          DrawValueLine
       else
          Repaint;

    if Assigned(FOnChange) then
       FOnChange(Self);
  end;
end;

procedure TGaugeSeries.SetFullRepaint(const Value: Boolean);
begin
  SetBooleanProperty(FFullRepaint,Value);
end;

function TGaugeSeries.GetValue: Double;
begin
  if Count=0 then Add(0);
  result:=MandatoryValueList.Value[0];
end;

procedure TGaugeSeries.SetEndPoint(const Value: TSeriesPointer);
begin
  FEndPoint.Assign(Value);
end;

procedure TGaugeSeries.AddSampleValues(NumValues: Integer; OnlyMandatory:Boolean=False);
begin
  Value:=Minimum+(Maximum-Minimum)*Random;
end;

procedure TGaugeSeries.PrepareForGallery;
begin
  inherited;

  with Axis do
  begin
    MinorTickCount:=1;
    TickLength:=4;
    MinorTickLength:=2;
  end;

  HandDistance:=3;
  Center.VertSize:=2;
  Center.HorizSize:=2;
  FullRepaint:=True;

  Pen.Color:=clBlue;

  Frame.Show;
end;

function TGaugeSeries.NumSampleValues: Integer;
begin
  result:=1;
end;

procedure TGaugeSeries.SetMinorDistance(const Value: Integer);
begin
  SetIntegerProperty(FMinorDistance,Value);
end;

procedure TGaugeSeries.GalleryChanged3D(Is3D: Boolean);
begin
  inherited;
  ParentChart.View3D:=False;
  Circled:=True;
end;

class function TGaugeSeries.GetEditorClass: String;
begin
  result:='TGaugeSeriesEditor'; // Do not localize
end;

function TGaugeSeries.MaxYValue: TChartValue;
begin
  result:=FMax;
end;

function TGaugeSeries.MinYValue: TChartValue;
begin
  result:=FMin;
end;

function TGaugeSeries.Axis: TChartAxis;
begin
  result:=GetVertAxis;
end;

procedure TGaugeSeries.SetLabelsInside(const Value: Boolean);
begin
  SetBooleanProperty(FLabelsInside,Value);
end;

procedure TGaugeSeries.NotifyNewValue(Sender: TChartSeries;
  ValueIndex: Integer);
begin
  Value:=MandatoryValueList[ValueIndex];
  inherited;
end;

procedure TGaugeSeries.SetActive(Value: Boolean);
begin
  inherited;

  if Active and Assigned(ParentChart) then
  begin
    if ILastModifiedChart<>ParentChart then
    begin
      CustomizeChart;
      ILastModifiedChart:=ParentChart;
    end;
  end;
end;

procedure TGaugeSeries.DoBeforeDrawValues;
var tmp : Integer;
begin
  tmp:=Axis.Axis.HalfWidth;

  if Axis.Labels and (not LabelsInside) then
  begin
    TTeePanelAccess(ParentChart).TryAssignFont(Axis.Items.Format.Font);
    Inc(tmp,ParentChart.Canvas.FontHeight);
  end;

  if tmp<>0 then
     InflateRect(ParentChart.ChartRect,-tmp,-tmp);

  inherited;
end;

procedure TGaugeSeries.Assign(Source: TPersistent);
begin
  if Source is TGaugeSeries then
  with TGaugeSeries(Source) do
  begin
    Self.FFullRepaint:=FFullRepaint;
    Self.FMax:=FMax;
    Self.FMin:=FMin;
  end;

  inherited;
end;

procedure TGaugeSeries.AssignFormatting(Source: TChartSeries);
begin
  if Source is TGaugeSeries then
  with TGaugeSeries(Source) do
  begin
    Self.FAngle:=FAngle;
    Self.FCenter.Assign(FCenter);
    Self.FCirclePen.Assign(FCirclePen);
    Self.FDistance:=FDistance;
    Self.FEndPoint.Assign(FEndPoint);
    Self.FHandWidth:=FHandWidth;
    Self.FLabelsInside:=FLabelsInside;
    Self.FMinorDistance:=FMinorDistance;
    Self.FStyle:=FStyle;
    Self.FShapeStyle:=FShapeStyle;
  end;

  inherited;
end;

procedure TGaugeSeries.SetShapeStyle(const Value: TGaugeShapeStyle);
begin
  if FShapeStyle<>Value then
  begin
    FShapeStyle:=Value;
    Repaint;
  end;
end;

procedure TGaugeSeries.SetCirclePen(const Value: TChartPen);
begin
  FCirclePen.Assign(Value);
end;

procedure TGaugeSeries.SetHandWidth(const Value: Integer);
begin
  SetIntegerProperty(FHandWidth,Value);
end;

function TGaugeSeries.ShouldDoXor: Boolean;
begin
  result:=(not FFullRepaint) and Assigned(ParentChart) and
          Assigned(ParentChart.Canvas) and
          ParentChart.Canvas.SupportsXORMode;
end;

initialization
  RegisterTeeSeries(TGaugeSeries, @TeeMsg_GalleryGauge,
                                  @TeeMsg_GalleryGauges,1);
finalization
  UnRegisterTeeSeries([TGaugeSeries]);
end.
