{**********************************************}
{   TPolarGridSeries                           }
{   Copyright (c) 2006-2020 by Steema Software }
{**********************************************}
unit TeePolarGrid;
{$I TeeDefs.inc}

interface

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

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

  {$IFDEF FMX}
  FMX.Types,
  {$ELSE}
  Graphics, StdCtrls, ComCtrls, Controls, ExtCtrls,
  {$ENDIF}

  {$IFDEF FMX}

  {$IFDEF D19}
  FMX.Graphics, // inline warning
  {$ENDIF}

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

type
  TPolarGridStyle=(gsSquared, gsCircled);

  TPolarGridSeries=class(TCustomPolarSeries) // , ITeeScreenToValues  (see TDragPoint tool)
  private
    FCentered   : Boolean;
    FGridStyle  : TPolarGridStyle;
    FNumSectors : Integer;
    FNumTracks  : Integer;
    FPalette    : TSeriesColorPalette;
    FValues     : TChartValueList;

    procedure DrawCell(ValueIndex: Integer; const UsePen:Boolean);
    function InternalX(const Value:TChartValue):TChartValue;
    procedure SetCentered(const Value: Boolean);
    procedure SetGridStyle(const Value: TPolarGridStyle);
    procedure SetPalette(const Value: TSeriesColorPalette);
    procedure SetValues(const Value: TChartValueList);
    function GetCell(Sector, Track: Integer): TChartValue;
    procedure SetCell(Sector, Track: Integer; const Value: TChartValue);
  protected
    Procedure AddSampleValues(NumValues:Integer; OnlyMandatory:Boolean=False); override;
    procedure AssignFormatting(Source: TChartSeries); override;

    {$IFDEF FMX}
    Procedure CalcXYPosition(const XValue,YValue:TChartValue; ARadius:Integer; out X,Y:Single); overload;
    {$ENDIF}

    procedure CellPolygon(ValueIndex:Integer; out P:TFourPoints);
    Procedure DoAfterDrawValues; override;
    Procedure DoBeforeDrawChart; override;
    Procedure DrawPolarCircle(HalfWidth,HalfHeight,Z:Integer); override;
    procedure DrawValue(ValueIndex:Integer); override;
    Function GetCircleLabel(Const Angle:Double; Index:Integer):String; override;
    class Function GetEditorClass:String; override;
    Procedure PrepareForGallery; override;
  public
    Constructor Create(AOwner: TComponent); override;
    Destructor Destroy; override;

    Procedure Assign(Source:TPersistent); override;

    Function AddCell(const Sector,Track:Integer; const Value:TChartValue):Integer;
    Function CalcXPos(ValueIndex:Integer):Integer; override;
    Function CalcYPos(ValueIndex:Integer):Integer; override;
    Function Clicked(x,y:Integer):Integer; override;
    Function CountLegendItems:Integer; override;
    function GetCellColor(const Value:TChartValue):TColor;
    Function IsValidSourceOf(Value:TChartSeries):Boolean; override;
    Function LegendItemColor(LegendIndex:Integer):TColor; override;
    Function LegendString( LegendIndex:Integer;
                           LegendTextStyle:TLegendTextStyle ):String; override;

    procedure PositionOf(const ValueIndex:Integer; out Sector,Track:Integer);

    property Cell[Sector,Track:Integer]:TChartValue read GetCell write SetCell;
  published
    property CellValues:TChartValueList read FValues write SetValues;
    property Centered:Boolean read FCentered write SetCentered default True;
    property NumSectors:Integer read FNumSectors write FNumSectors default 10;
    property NumTracks:Integer read FNumTracks write FNumTracks default 10;

    { Published declarations }
    property Active;
    property ColorEachPoint;
    property HorizAxis;
    property SeriesColor;
    property VertAxis;

    property AngleIncrement;
    property AngleValues;
    property Brush;
    property CircleBackColor;
    property CircleGradient;

    property AngleLabels;

    // Deprecated. See AngleLabels property
    property CircleLabels;
    property CircleLabelsFont;
    property CircleLabelsInside;
    property CircleLabelsRotated;
    property ClockWiseLabels;
    property LabelsMargin;

    property CirclePen;
    property GridStyle:TPolarGridStyle read FGridStyle write SetGridStyle default gsSquared;
    property Labels;
    property Palette:TSeriesColorPalette read FPalette write SetPalette;
    property Pen;
    property RadiusIncrement;
    property RadiusValues;
    property RotationAngle default 90;
    property Transparency;
    property TreatNulls;

    { events }
    property OnGetCircleLabel;
  end;

implementation

uses
  Math,
  {$IFDEF FMX}
  FMXTee.ProConstants
  {$ELSE}
  TeeProCo
  {$ENDIF};

{ TPolarGridSeries }

Constructor TPolarGridSeries.Create(AOwner: TComponent);
begin
  inherited;
  FValues:=TChartValueList.Create(Self,'Values'); { <-- dont translate ! }

  RotationAngle:=90;

  FCentered:=True;
  FNumSectors:=10;
  FNumTracks:=10;

  CalcVisiblePoints:=False;
  XValues.Name:='Sectors';
  XValues.Order:=loNone;
  YValues.Name:='Tracks';

  FPalette:=TSeriesColorPalette.Create(Self);

  Pen.Color:={$IFDEF FMX}claBlack{$ELSE}clBlack{$ENDIF};
  Brush.Style:=bsSolid;

  Pointer.Hide;
end;

function TPolarGridSeries.AddCell(const Sector, Track: Integer;
  const Value: TChartValue): Integer;
begin
  FValues.TempValue:=Value;
  result:=AddXY(Sector,Track);
end;

procedure TPolarGridSeries.AssignFormatting(Source: TChartSeries);
begin
  if Source is TPolarGridSeries then
  with TPolarGridSeries(Source) do
  begin
    Self.Palette:=Palette;
  end;

  inherited;
end;

procedure TPolarGridSeries.Assign(Source: TPersistent);
begin
  if Source is TPolarGridSeries then
  with TPolarGridSeries(Source) do
  begin
    Self.CellValues:=CellValues;
    Self.FCentered:=FCentered;
    Self.FGridStyle:=FGridStyle;
    Self.FNumSectors:=FNumSectors;
    Self.FNumTracks:=FNumTracks;
  end;

  inherited;
end;

function TPolarGridSeries.IsValidSourceOf(Value: TChartSeries): Boolean;
begin
  result:=Value is TPolarGridSeries;
end;

Destructor TPolarGridSeries.Destroy;
begin
  FPalette.Free;
  inherited;
end;

procedure TPolarGridSeries.AddSampleValues(NumValues: Integer;
  OnlyMandatory: Boolean);
var Sector : Integer;
    Track  : Integer;
    tmp    : TChartValue;
begin
  NumSectors:=NumValues;
  NumTracks:=NumValues;

  BeginUpdate;

  for Sector:=0 to NumSectors-1 do
      for Track:=0 to NumTracks-1 do
      begin
        tmp:=0.5*Sqr(Cos(Sector/(NumSectors*0.2)))+
                 Sqr(Cos(Track/(NumTracks*0.2)))-
                 Cos(Track/(NumTracks*0.5));

        AddCell(Sector,Track,tmp);
      end;

  EndUpdate;
end;

Procedure TPolarGridSeries.PrepareForGallery;
Begin
  inherited;
  FillSampleValues(8);
end;

function TPolarGridSeries.InternalX(const Value:TChartValue):TChartValue;
begin
  result:=Value;

  if Centered then
     result:=result-0.5;

  result:=result*360.0/NumSectors;

  if ClockWiseLabels then
     result:=360-result;
end;

Function TPolarGridSeries.CalcXPos(ValueIndex:Integer):Integer;
var tmp : Integer;
begin
  CalcXYPosition(InternalX(XValues[ValueIndex]),YValues[ValueIndex],XRadius,result,tmp);
end;

Function TPolarGridSeries.CalcYPos(ValueIndex:Integer):Integer;
var tmp : Integer;
begin
  CalcXYPosition(InternalX(XValues[ValueIndex]),YValues[ValueIndex],YRadius,tmp,result);
end;

{$IFDEF FMX}
Procedure TPolarGridSeries.CalcXYPosition(const XValue,YValue:TChartValue; ARadius:Integer; out X,Y:Single);
var tmpX, tmpY : Integer;
begin
  CalcXYPosition(XValue,YValue,ARadius,tmpX,tmpY);
  x:=tmpX;
  y:=tmpY;
end;
{$ENDIF}

procedure TPolarGridSeries.CellPolygon(ValueIndex:Integer; out P:TFourPoints);
var xx,yy : TCoordinate;
    tmpX : TChartValue;
    tmpY : TChartValue;
    tmpX0,
    tmpX1 : TChartValue;
begin
  tmpX:=XValues[ValueIndex];
  tmpY:=YValues[ValueIndex]-0.5;

  tmpX0:=InternalX(tmpX);

  CalcXYPosition(tmpX0,tmpY,YRadius,P[0].x,P[0].y);

  tmpX1:=InternalX(tmpX+1);

  CalcXYPosition(tmpX1,tmpY,XRadius,P[1].x,yy);
  CalcXYPosition(tmpX1,tmpY,YRadius,xx,P[1].y);

  if tmpY>=-0.5 then
  begin
    tmpY:=tmpY+1;

    CalcXYPosition(tmpX0,tmpY,XRadius,P[3].x,yy);
    CalcXYPosition(tmpX0,tmpY,YRadius,xx,P[3].y);

    CalcXYPosition(tmpX1,tmpY,XRadius,P[2].x,yy);
    CalcXYPosition(tmpX1,tmpY,YRadius,xx,P[2].y);
  end
  else
  begin
    P[3].X:=CircleXCenter;
    P[3].Y:=CircleYCenter;
    P[2]:=P[3];
  end;
end;

type
  TPaletteAccess=class(TCustom3DPaletteSeries);

function TPolarGridSeries.GetCellColor(const Value:TChartValue):TColor;
begin
  result:=TPaletteAccess(FPalette.Series).GetValueColorValue(Value);
end;

procedure TPolarGridSeries.DrawValue(ValueIndex: Integer);
begin
  DrawCell(ValueIndex,True);
end;

procedure TPolarGridSeries.DrawCell(ValueIndex: Integer; const UsePen:Boolean);
const
  TwoPi=2.0*Pi;

var tmpColor : TColor;
    tmpBlend : TTeeBlend;
    P        : TFourPoints;

    tmp,
    tmpY,
    tmpDelta,
    tmpStart,
    tmpEnd : TChartValue;
begin
  tmpColor:=InternalColor(ValueIndex);

  if (tmpColor<>clNone) or (TreatNulls=tnIgnore) then
  begin
    if tmpColor=clTeeColor then
       if (not ColorEachPoint) and (FPalette.Series.UseColorRange or FPalette.Series.UsePalette) then
           tmpColor:=GetCellColor(CellValues[ValueIndex])
       else
           tmpColor:=GetValueColor(ValueIndex);

    CellPolygon(ValueIndex,P); // <-- This is done here because old GDI = not SupportAlpha

    if UsePen then
       ParentChart.Canvas.AssignVisiblePen(Self.Pen);

    with ParentChart.Canvas do
    begin
      Brush.Color:=tmpColor;

      if Transparency>0 then
         if SupportsAlpha then
            tmpBlend:=BeginBlending(TeeZeroRect,Transparency)
         else
            tmpBlend:=BeginBlending(RectFromPolygon(P,Length(P)),Transparency)
      else
         tmpBlend:=nil;

      if GridStyle=gsSquared then
      begin
        if ParentChart.View3D then
           PolygonWithZ(P,MiddleZ)
        else
           Polygon(P);
      end
      else
      begin
        tmpDelta:=TwoPi/NumSectors;

        tmpStart:=(InternalX(XValues[ValueIndex])+RotationAngle)*TeePiStep;

        if Centered then
           tmpStart:=tmpStart+(TeePiStep/NumSectors);

        tmpEnd:=tmpStart+tmpDelta;

        tmpY:=YValues[ValueIndex];
        tmp:=(1+tmpY)/NumTracks;

        Donut(CircleXCenter,CircleYCenter,Round(AxisXRadius*tmp),Round(AxisYRadius*tmp),
              tmpStart,tmpEnd,100*tmpY/(tmpY+1));
      end;

      if Transparency>0 then
         EndBlending(tmpBlend);
    end;
  end;
end;

class function TPolarGridSeries.GetEditorClass: String;
begin
  result:='TPolarGridEditor';
end;

procedure TPolarGridSeries.DoBeforeDrawChart;
begin
  FPalette.InitColors(Color,CellValues.MinValue,CellValues.MaxValue);

  if NumSectors>0 then // <-- avoid div by zero
     AngleIncrement:=360.0/NumSectors
  else
     AngleIncrement:=360;

  // Reset axes grid lines to fit Polar sectors and tracks:
  GetHorizAxis.Increment:=AngleIncrement;
  GetVertAxis.Increment:=1;

  // Force showing axes
  ParentChart.Axes.Behind:=False;

  inherited;
end;

function TPolarGridSeries.CountLegendItems: Integer;
begin
  result:=FPalette.Series.CountLegendItems;
end;

function TPolarGridSeries.LegendItemColor(LegendIndex: Integer): TColor;
begin
  result:=FPalette.Series.LegendItemColor(LegendIndex);
end;

function TPolarGridSeries.LegendString(LegendIndex: Integer;
  LegendTextStyle: TLegendTextStyle): String;
begin
  result:=FPalette.Series.LegendString(LegendIndex,LegendTextStyle);
end;

procedure TPolarGridSeries.SetPalette(const Value: TSeriesColorPalette);
begin
  FPalette.Assign(Value);
end;

procedure TPolarGridSeries.SetValues(const Value: TChartValueList);
begin
  FValues.Assign(Value);
end;

function TPolarGridSeries.GetCircleLabel(const Angle: Double;
  Index: Integer): String;
begin
  if (Index>0) and ClockWiseLabels then
     Index:=NumSectors-Index;

  result:=IntToStr(Index);
end;

Procedure TPolarGridSeries.DrawPolarCircle(HalfWidth,HalfHeight,Z:Integer);
var P    : TPoint;
    t    : Integer;
    tmpX : TChartValue;
    xx,
    yy   : TCoordinate;
begin
  if GridStyle=gsCircled then
  begin
    ParentChart.Canvas.Brush.Style:=bsClear;
    inherited;
  end
  else
  begin
    for t:=0 to NumSectors do
    begin
      tmpX:=InternalX(t);

      CalcXYPosition(tmpX,NumTracks-0.5,AxisXRadius,P.X,yy);
      CalcXYPosition(tmpX,NumTracks-0.5,AxisYRadius,xx,P.Y);

      if t=0 then
         ParentChart.Canvas.MoveTo3D(P.X,P.Y,Z)
      else
         ParentChart.Canvas.LineTo3D(P.X,P.Y,Z);
    end;
  end;
end;

procedure TPolarGridSeries.DoAfterDrawValues;
begin
  if HoverEnabled and (Selected.HoverIndex<>-1) then
  begin
    PrepareHoverPen(Selected.HoverIndex);
    ParentChart.Canvas.AssignBrush(Selected.Hover.Brush);

    DrawCell(Selected.HoverIndex,False);
  end;

  ParentChart.Canvas.AssignVisiblePen(CirclePen);
  DrawPolarCircle(CircleWidth div 2,CircleHeight div 2,EndZ);

  inherited;
end;

procedure TPolarGridSeries.SetCentered(const Value: Boolean);
begin
  SetBooleanProperty(FCentered,Value);
end;

function TPolarGridSeries.Clicked(x, y: Integer): Integer;
var t   : Integer;
    P   : TFourPoints;
    tmp : TPoint;
begin
  result:=TeeNoPointClicked;

  if Count>0 then
  begin
    if Assigned(ParentChart) then
       ParentChart.Canvas.Calculate2DPosition(X,Y,StartZ);

    tmp.X:=X;
    tmp.Y:=Y;

    for t:=0 to Count-1 do
    if not IsNull(t) then
    begin
      CellPolygon(t,P);

      if PointInPolygon(tmp,P) then
      begin
        result:=t;
        break;
      end;
    end;
  end;
end;

procedure TPolarGridSeries.SetGridStyle(const Value: TPolarGridStyle);
begin
  if FGridStyle<>Value then
  begin
    FGridStyle:=Value;
    Repaint;
  end;
end;

function TPolarGridSeries.GetCell(Sector, Track: Integer): TChartValue;
begin
  result:=CellValues[(Sector*NumTracks)+Track];
end;

procedure TPolarGridSeries.SetCell(Sector, Track: Integer;
  const Value: TChartValue);
begin
  CellValues[(Sector*NumTracks)+Track]:=Value;
end;

procedure TPolarGridSeries.PositionOf(const ValueIndex: Integer;
  out Sector, Track: Integer);
begin
  Sector:=ValueIndex div NumSectors;
  Track:=ValueIndex mod NumSectors;
end;

initialization
  RegisterTeeSeries(TPolarGridSeries, @TeeMsg_PolarGridSeries,
                                      @TeeMsg_Gallery3D,1);
finalization
  UnRegisterTeeSeries([TPolarGridSeries]);
end.

