{**********************************************}
{   TeeChart Knob Gauge style                  }
{   Copyright (c) 2011-2020 by Steema Software }
{**********************************************}
unit TeeKnobGauge;
{$I TeeDefs.inc}

interface

uses
  {$IFNDEF LINUX}
  {$IFNDEF FMX}
  Windows, Messages,
  {$ENDIF}
  {$ENDIF}
  SysUtils, Classes, Math,
  {$IFDEF D10}
  Types,
  {$ENDIF}

  {$IFDEF FMX}
  FMX.Types, System.UITypes,
  {$IFDEF D16}
  FMX.Platform,
  {$ENDIF}
  {$ELSE}
  Graphics, Controls, Forms,
  {$ENDIF}

  {$IFDEF FMX}
  FMXTee.Canvas, FMXTee.Procs, FMXTee.Engine, FMXTee.Gauges.Circular,
  FMXTee.Gauges.Linear
  {$ELSE}
  TeeLinearGauge, TeeCircularGauge, TeEngine, TeCanvas, TeeProcs
  {$ENDIF};

type
  TKnobGauge=class(TCircularGauge,ITeeEventListener)
  private
    FActiveCenter : Boolean;
    FActiveChange : Boolean;
    FAxisInside   : Boolean;
    FHighLight    : TChartGradient;
    FInnerEllipse : TRect;

    IAxisGap      : Integer;
    OldX,OldY     : Integer;
    OldCursor     : TCursor;

    function CalcRectangle:TRect;
    procedure DrawHighLight(R: TRect; Const StartAngle, EndAngle: Integer; FarSide: Boolean);
    procedure SetAxisInside(Const Value: Boolean);
    procedure SetActiveCenter(Const Value: Boolean);
    procedure SetHighLight(Const Value: TChartGradient);
  protected
    procedure CalcOrigRect; override;
    procedure DrawAllValues; override;
    procedure DrawAxis; override;
    procedure DrawCenter; override;
    procedure DrawColorLine(const Line:TGaugePointerRange); override;
    procedure DrawFace; override;
    class Function GetEditorClass:String; override;
    procedure PrepareForGallery; override;
    procedure SetAxisOnce; override;

    procedure SetGaugePalette(const Palette:Array of TColor); override;

    Procedure SetParentChart(const Value:TCustomAxisPanel); override;
    procedure SetValues; override;
    procedure TeeEvent(Event:TTeeEvent); // ITeeEventListener interface
  public
    Constructor Create(AOwner:TComponent); override;
    Destructor Destroy; override;

    procedure Assign(Source:TPersistent); override;
    function CenterPoint:TPoint;

    property HighLight: TChartGradient read FHighLight write SetHighLight;
  published
    property Active;
    property Cursor;
    property ParentChart;
    property DataSource;  { after ParentChart }
    property PercentFormat;
    property ShowInLegend;
    property Title;
    property ValueFormat;

    { events }
    property AfterDrawValues;
    property BeforeDrawValues;
    property OnAfterAdd;
    property OnBeforeAdd;
    property OnChange;
    property OnClearValues;
    property OnClick;
    property OnDblClick;
    property OnMouseEnter;
    property OnMouseLeave;

    property XValues;
    property YValues;

    property ActiveCenter:Boolean read FActiveCenter write SetActiveCenter default False;
    property AxisInside:Boolean read FAxisInside write SetAxisInside default False;
    property Center;
    property Circled;
    property EndPoint;
    property Face;
    property Frame;
    property GreenLine stored False;
    property Hand stored False;
    property LabelsInside;
    property Maximum;
    property Minimum;
    property MinorTickDistance;
    property MinorTicks;
    property RedLine stored False;
    property RotateLabels;
    property RotationAngle;
    property Ticks;
    property TotalAngle;
    property Value;
  end;

implementation

uses
  {$IFDEF FMX}
  FMXTee.Chart, FMXTee.Series, FMXTee.Gauges.Numeric,
  FMXTee.ProConstants
  {$ELSE}

  {$IFDEF LCL}
  Types,
  {$ENDIF}

  Chart, TeeProCo, Series, TeeNumericGauge
  {$ENDIF}
  ;

{ TKnobGauge }

type
  TFrameAccess=class(TFramedBorder);

Const
  BluesPalette : Array[0..23] of TColor=
  (
    $FE0000,
    $FFCEAA,
    $683A1A,

    $D1AC93,
    $E9DCCF,
    $D1AC93,

    $282864, //6
    $DCDCF0,
    $DCDCF0,

    $1EC81E,
    $CDFACD,
    $CDFACD,

    $488BAB, //12
    $5BBEFD,
    $000000,
    $8ED5FF,

    $835504, //16
    $8A5C09,
    $000000,
    $704216,

    $835504,
    $835504,
    $646464,
    $646464
    );

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

  Hand.Distance:=0;
  Hand.Offset:=100;
  Hand.Style:=psTriangle;
  Hand.Shadow.Transparency:=50;
  Hand.Shadow.Size:=1;
  Hand.Gradient.Visible:=False;

  SetGaugePalette(BluesPalette);

  GreenLine.Gradient.MidColor:=clNone;
  GreenLine.Gradient.EndColor:=clNone;
  GreenLine.Pen.Style:=psClear;
  GreenLine.Spiralled:=False;

  RedLine.Gradient.MidColor:=clNone;
  RedLine.Gradient.EndColor:=clNone;
  RedLine.Pen.Style:=psClear;
  RedLine.Spiralled:=False;

  // Reset frame visibility
  Frame.Visible:=False;
  TFrameAccess(Frame).DefaultVisible:=False;

  EndPoint.Visible:=False;

  FHighLight:=TChartGradient.Create(CanvasChanged);
  FHighLight.Visible:=True;
  FHighLight.StartColor:=Center.Color;
  FHighLight.EndColor:=Center.Color;
  FHighLight.MidColor:=$FDF5E6;
  FHighLight.Direction:=gdLeftRight;

  Center.Size:=50; // %

  OldX:=-1;
  OldY:=-1;
  OldCursor:=crNone;
end;

Destructor TKnobGauge.Destroy;
begin
  FHighLight.Free;
  inherited;
end;

type
  TTeePanelAccess=class(TCustomTeePanel);

procedure TKnobGauge.CalcOrigRect;
var h,
    w,
    tmp : TCoordinate;
begin
  inherited;

  if Axis.Texts.Visible then
  begin
    TTeePanelAccess(ParentChart).TryAssignFont(Axis.Texts.Font);
    IOrigRect.Top:=IOrigRect.Top+{$IFNDEF FMX}Round{$ENDIF}(ParentChart.Canvas.FontHeight*0.5);
  end;

  //Make a square
  w:=IOrigRect.Right-IOrigRect.Left;
  h:=IOrigRect.Bottom-IOrigRect.Top;

  if w>h then
  begin
    tmp:=(w-h) {$IFDEF FMX}*0.5{$ELSE}div 2{$ENDIF};
    IOrigRect:=TeeRect(IOrigRect.Left+tmp,IOrigRect.Top,IOrigRect.Right-tmp,IOrigRect.Bottom);
  end
  else
  begin
    tmp:=(h-w) {$IFDEF FMX}*0.5{$ELSE}div 2{$ENDIF};
    IOrigRect:=TeeRect(IOrigRect.Left,IOrigRect.Top+tmp,IOrigRect.Right,IOrigRect.Bottom-tmp);
  end;
end;

function TKnobGauge.CenterPoint:TPoint;
begin
  Result:=TeePoint(CircleXCenter,CircleYCenter);
end;

procedure TKnobGauge.DrawAllValues;
begin
  CalcOrigRect;

  IAxisGap:=1;

  if Axis<>nil then
  begin
    if Axis.MinorTicks.Visible then
       Inc(IAxisGap,Axis.MinorTickLength);

    if Axis.Ticks.Visible then
       Inc(IAxisGap,Axis.TickLength);
  end;

  inherited;
end;

procedure TKnobGauge.DrawAxis;
begin
  if not FAxisInside then
     InflateRect(INewRect, -IAxisGap, -IAxisGap);

  inherited;
end;

procedure TKnobGauge.DrawHighLight(R: TRect; Const StartAngle, EndAngle: Integer; FarSide: Boolean);
var
  XCenter,YCenter,
  XRadius,YRadius : Single;

  function PointFromCircle(const Radians:Single): TPointFloat;
  var tmpSin, tmpCos : {$IFDEF D15}Double{$ELSE}Extended{$ENDIF};
  begin
    SinCos(Radians,tmpSin,tmpCos);
    result.X:=XCenter+(XRadius*tmpCos);
    result.Y:=YCenter-(YRadius*tmpSin);
  end;

var tmp : Double;
    tmpP: TPointFloatArray;
    i,j,NumP : Integer;
    dx,dy : TCoordinate;
begin
  NumP:=EndAngle-StartAngle+1;

  if (EndAngle-20)>=(StartAngle+20) then
     Inc(NumP,(EndAngle-20)-(StartAngle+20)+1);

  if NumP>0 then
  begin
    SetLength(tmpP,NumP);

    XCenter:=(R.Left+R.Right)*0.5;
    YCenter:=(R.Top+R.Bottom)*0.5;

    XRadius:=(R.Right-R.Left)*0.5;
    YRadius:=(R.Bottom-R.Top)*0.5;

    j:=0;
    for i:=StartAngle to EndAngle do
    begin
      tmpP[j]:=PointFromCircle(TeePiStep*i);
      inc(j);
    end;

    R.Right:=R.Right+{$IFNDEF FMX}Round{$ENDIF}(XRadius*0.5);
    R.Bottom:=R.Bottom+{$IFNDEF FMX}Round{$ENDIF}(YRadius*0.5);

    XRadius:=(R.Right-R.Left)*0.5;
    YRadius:=(R.Bottom-R.Top)*0.5;

    if FarSide then
       tmp:=2*0.190
    else
       tmp:=2*0.010;

    dx:={$IFNDEF FMX}Round{$ENDIF}(XRadius*tmp);
    dy:={$IFNDEF FMX}Round{$ENDIF}(YRadius*tmp);

    R:=TeeRect(R.Left-dx,R.Top-dy,R.Right-dx,R.Bottom-dy);

    XCenter:=(R.Left+R.Right)*0.5;
    YCenter:=(R.Top+R.Bottom)*0.5;

    for i:=EndAngle-20 downto StartAngle+20 do
    begin
      tmpP[j]:=PointFromCircle(TeePiStep*i);
      inc(j);
    end;

    ParentChart.Canvas.Gradient.Assign(FHighLight);

    {$IFDEF FMX}
    ParentChart.Canvas.Polygon(tmpP);
    {$ELSE}
    ParentChart.Canvas.PolygonFloat(tmpP);
    {$ENDIF}

    tmpP:=nil;
  end;
end;

procedure TKnobGauge.DrawCenter;
{$IFDEF FMX}
const
  OneFifteen:Single=1/15;
{$ENDIF}

var tmpR: TRect;
begin
  inherited;

  if Center.Visible and HighLight.Visible then
  begin
    tmpR:=CalcRectangle;

    {$IFDEF FMX}
    tmpR.Inflate(-(tmpR.Right-tmpR.Left)*OneFifteen, -(tmpR.Bottom-tmpR.Top)*OneFifteen);
    {$ELSE}
    InflateRect(tmpR, -(tmpR.Right-tmpR.Left) div 15, -(tmpR.Bottom-tmpR.Top) div 15);
    {$ENDIF}

    DrawHighLight(tmpR, 90, 180, False);
    DrawHighLight(tmpR, 270, 360, True);

    FInnerEllipse:=CalcRectangle;
  end;
end;

procedure TKnobGauge.DrawColorLine(const Line:TGaugePointerRange);
var tmpR : TRect;
begin
  if Line.Visible then
  begin
    tmpR:=INewRect;
    InflateRect(tmpR, IAxisGap-1, IAxisGap-1);
    Line.Draw(CalcValue(Line.StartValue),CalcValue(Line.EndValue),tmpR);
  end;
end;

procedure TKnobGauge.DrawFace;
begin
  if not FAxisInside then
     InflateRect(INewRect, -IAxisGap, -IAxisGap);

  inherited;

  if not FAxisInside then
     InflateRect(INewRect, IAxisGap, IAxisGap);
end;

procedure TKnobGauge.Assign(Source: TPersistent);
begin
  if Source is TKnobGauge then
  with TKnobGauge(Source) do
  begin
    Self.FActiveCenter:=FActiveCenter;
    Self.FAxisInside:=FAxisInside;
    Self.FHighLight.Assign(FHighLight);
  end;

  inherited;
end;

class function TKnobGauge.GetEditorClass: String;
begin
  result:='TKnobGaugeEditor';
end;

procedure TKnobGauge.SetActiveCenter(const Value: Boolean);
begin
  SetBooleanProperty(FActiveCenter,Value);
end;

procedure TKnobGauge.SetAxisInside(const Value: Boolean);
begin
  SetBooleanProperty(FAxisInside,Value);
end;

procedure TKnobGauge.SetGaugePalette(const Palette:Array of TColor);

  procedure SetColors(const Line: TGaugeSeriesPointer; Color1,Color2,Color3: Integer);
  begin
    if Line.Gradient.Visible then
    begin
      Line.Gradient.StartColor:=GetPaletteColor(Color1);
      Line.Gradient.MidColor:=GetPaletteColor(Color2);
      Line.Gradient.EndColor:=GetPaletteColor(Color3);
    end;
  end;

begin
  inherited;

  if Assigned(GreenLine) then
  begin
    GreenLine.Gradient.Direction:=gdRightLeft;
    SetColors(GreenLine,9,10,11);
  end;

  if Assigned(RedLine) then
  begin
    RedLine.Gradient.Direction:=gdLeftRight;
    SetColors(RedLine,6,7,8);
  end;

  if Assigned(Center) then
  begin
    Center.Color:=GetPaletteColor(17);
    Center.Gradient.Visible:=False;
  end;

  if Assigned(Hand) then
  begin
    Hand.Pen.Color:=GetPaletteColor(12);
    Hand.Color:=GetPaletteColor(15);
  end;
end;

type
  TCustomTeePanelAccess=class(TCustomTeePanel);

procedure TKnobGauge.SetAxisOnce;
begin
  inherited;
  Axis.Texts.Font.Size:=12;
end;

procedure TKnobGauge.SetParentChart(const Value: TCustomAxisPanel);
begin
  if Assigned(ParentChart) then
     TCustomTeePanelAccess(ParentChart).RemoveListener(Self);

  inherited;

  if not (csDestroying in ComponentState) then
     if Assigned(ParentChart) then
        TCustomTeePanelAccess(ParentChart).AddListener(Self);
end;

procedure TKnobGauge.SetValues;
var tmpW, tmpH: Integer;
begin
  inherited;

  // Pending: How to respect Center Size instead of forcing it here?  (as % ? )

  tmpW:={$IFDEF FMX}Round{$ENDIF}(IOrigRect.Right-IOrigRect.Left);
  tmpH:={$IFDEF FMX}Round{$ENDIF}(IOrigRect.Bottom-IOrigRect.Top);

  if tmpW > 0 then
  begin
    Center.HorizSize:=tmpW div 4;
    Hand.HorizSize:=tmpW div 5;
  end;

  if tmpH > 0 then
     Center.VertSize:={$IFDEF FMX}Round{$ENDIF}(IOrigRect.Bottom-IOrigRect.Top) div 4;

  RedLine.VertSize:=IAxisGap-1;
  GreenLine.VertSize:=IAxisGap-1;
end;

procedure TKnobGauge.SetHighLight(const Value: TChartGradient);
begin
  FHighLight.Assign(Value);
end;

procedure TKnobGauge.TeeEvent(Event: TTeeEvent);

  procedure ResetCursor;
  begin
    {$IFNDEF FMX}
    if OldCursor<>crNone then
       Screen.Cursor:=OldCursor;
    {$ENDIF}

    OldCursor:=crNone;
  end;

var CX,CY: Integer;
begin
  if (Event is TTeeMouseEvent) and FActiveCenter then
  With TTeeMouseEvent(Event) do
  if (Button={$IFDEF FMX}TMouseButton.{$ENDIF}mbLeft) then
  begin
    if Event=meDown then
    begin
      {$IFNDEF FMX}
      if (OldCursor=crNone) or (Screen.Cursor<>crHandPoint) then
         OldCursor:=Screen.Cursor;
      {$ENDIF}

      FActiveChange:=PointInEllipse(TeePoint(X,Y),FInnerEllipse);

      if FActiveChange then
         TeeChangeCursor(nil, crHandPoint)
      else
         ResetCursor;
    end
    else
    if Event=meUp then
    begin
      FActiveChange:=False;
      ResetCursor;
    end
    else
    if Event=meMove then
    begin
      if FActiveChange then
      begin
        if (X<>OldX) and (Y<>OldY) then
        begin
          CX:=CenterPoint.{$IFDEF FMX}Round.{$ENDIF}X;
          CY:=CenterPoint.{$IFDEF FMX}Round.{$ENDIF}Y;

          if ((((X>OldX) or (Y>OldY)) and (X>CX) and (Y<CY))        //1st quadrant, 12-3 o'clock
            or(((X<OldX) or (Y>OldY)) and (X>CX) and (Y>CY))        //2nd quadrant
            or(((X<OldX) or (Y<OldY)) and (X<CX) and (Y>CY))        //3rd quadrant
            or(((X>OldX) or (Y<OldY)) and (X<CX) and (Y<CY))) then  //4th quadrant
          begin
            if Value<Maximum then Value:=Value+((Maximum-Minimum)*0.01)
                             else Value:=Maximum;
          end
          else
          begin
            if Value>Minimum then Value:=Value-((Maximum-Minimum)*0.01)
                             else Value:=Minimum;
          end;
        end;
      end;

      OldX:={$IFDEF FMX}Round{$ENDIF}(X);
      OldY:={$IFDEF FMX}Round{$ENDIF}(Y);
    end;
  end;
end;

function TKnobGauge.CalcRectangle:TRect;
begin
  result:=RectFromCenter(CircleXCenter,CircleYCenter,Center.HorizSize,Center.VertSize);
end;

procedure TKnobGauge.PrepareForGallery;
begin
  inherited;
  Hand.Distance:=0;
  Axis.Texts.Font.Color:=clDkGray;
end;

initialization
  RegisterTeeSeries(TKnobGauge, @TeeMsg_KnobGauge,
                                @TeeMsg_GalleryGauges,1);
finalization
  UnRegisterTeeSeries([TKnobGauge]);
end.
