{**********************************************}
{   TSeriesBandTool                            }
{   Copyright (c) 2003-2020 by Steema Software }
{   Portions Copyright (c) 2015 by             }
{   Martin Kaul (mkaul@leuze.de)               }
{**********************************************}
unit TeeSeriesBandTool;
{$I TeeDefs.inc}

interface

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

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

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

  {$ELSE}

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

  Graphics, Controls,
  {$ENDIF}

  {$IFDEF LINUX}
  Types,
  {$ENDIF}
  {$IFDEF FMX}
  FMXTee.Canvas, FMXTee.Tools, FMXTee.Engine, FMXTee.Procs, FMXTee.Chart,
  FMXTee.Constants, FMXTee.ProConstants, FMXTee.Series
  {$ELSE}
  TeCanvas, TeeTools, TeEngine, TeeProcs, Chart, TeeConst, TeeProCo, Series
  {$ENDIF}
  ;

type
  TBandCondition=(bcAll, bcLower, bcHigher);

  // Series-Band tool, use it to display a band between two (line) series
  // created 2003-12-14 by mkaul@leuze.de
  TSeriesBandTool=class(TTeeCustomToolSeries)
  private
    FBoundValue   : Double;
    FCondition    : TBandCondition;
    FDrawBehind   : Boolean;
    FSeries2      : TChartSeries;
    FTransparency : TTeeTransparency;

    ISerie1Drawed : Boolean;
    ISerie2Drawed : Boolean;
    IPaintedBand  : Boolean;

    procedure AfterSeriesDraw(Sender:TObject);
    procedure BeforeSeriesDraw(Sender:TObject);
    function GetGradient:TTeeGradient;
    procedure SetDrawBehind(const Value: Boolean);
    procedure SetGradient(const Value: TTeeGradient);
    procedure SetTransparency(const Value: TTeeTransparency);
    procedure SetBoundValue(const Value: double);
    procedure SetCondition(const Value: TBandCondition);
    procedure TryToPaint(Sender:TObject);
  protected
    procedure ChartEvent(AEvent: TChartToolEvent); override;
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
    Procedure SeriesEvent(AEvent:TChartToolEvent; ASeries:TChartSeries); override;
    procedure SetSeries(const Value: TChartSeries); override;
    procedure SetSeries2(const Value: TChartSeries); virtual;
    procedure DrawBandTool; virtual;
    class Function GetEditorClass:String; override;
  public
    Constructor Create(AOwner:TComponent); override;

    procedure Assign(Source:TPersistent); override;
    class Function Description:String; override;
    class Function LongDescription:String; override;
  published
    property Active;
    property BoundValue: Double read FBoundValue write SetBoundValue;
    property Brush;
    property Condition:TBandCondition read FCondition write SetCondition default bcAll;
    property DrawBehindSeries:Boolean read FDrawBehind
                                      write SetDrawBehind default True;
    property Gradient:TTeeGradient read GetGradient write SetGradient;
    property Pen;
    property Series;
    property Series2:TChartSeries read FSeries2 write SetSeries2;
    property Transparency:TTeeTransparency read FTransparency
                                           write SetTransparency default 0;
  end;

implementation

Uses
  {$IFDEF FMX}
  {$IFDEF D17}
  System.UIConsts,
  {$ENDIF}
  {$ENDIF}
  Math;

// Created 2003-12-14 by mkaul@leuze.de.
// Improved by Steema.

{ TSeriesBandTool }

Constructor TSeriesBandTool.Create(AOwner: TComponent);
begin
  inherited;
  FDrawBehind:=True;
  Brush.Color:={$IFDEF FMX}claWhite{$ELSE}clWhite{$ENDIF};
  Pen.Visible:=False;
end;

Procedure TSeriesBandTool.SeriesEvent(AEvent:TChartToolEvent; ASeries:TChartSeries);
begin
  inherited;

  if Active then
    case AEvent of
      cteAfterDrawSeries  : AfterSeriesDraw(ASeries);
      cteBeforeDrawSeries : BeforeSeriesDraw(ASeries);
    end;
end;

procedure TSeriesBandTool.ChartEvent(AEvent: TChartToolEvent);
begin
  inherited;

  if (AEvent=cteBeforeDraw) and Active then
  begin
    // Flags to detect when should the color band be painted: 
    ISerie1Drawed:=False;
    ISerie2Drawed:=False;
    IPaintedBand:=False;
  end;
end;

procedure TSeriesBandTool.TryToPaint(Sender:TObject); // bugs:6
begin
  if Assigned(Series)  and (Sender=Series)  then
     ISerie1Drawed:=True;

  if Assigned(Series2) and (Sender=Series2) then
     ISerie2Drawed:=True;

  if (FDrawBehind and (ISerie1Drawed or ISerie2Drawed))
     or
     ((not FDrawBehind) and (ISerie1Drawed and (ISerie2Drawed or not Assigned(Series2)))
     ) then
  begin
    if not IPaintedBand then
    begin
      DrawBandTool;
      IPaintedBand:=True;
    end;
  end;
end;

procedure TSeriesBandTool.AfterSeriesDraw(Sender: TObject);
begin
  if not FDrawBehind then
     TryToPaint(Sender);
end;

procedure TSeriesBandTool.BeforeSeriesDraw(Sender: TObject);
begin
  if FDrawBehind then
     TryToPaint(Sender);
end;

procedure TSeriesBandTool.SetDrawBehind(const Value: Boolean);
begin
  SetBooleanProperty(FDrawBehind,Value);
end;

class Function TSeriesBandTool.Description:String;
begin
  result:=TeeMsg_SeriesBandTool;
end;

procedure TSeriesBandTool.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  if (Operation=opRemove) and (AComponent=Series2) then
     Series2:=nil;

  inherited;
end;

function TSeriesBandTool.GetGradient:TTeeGradient;
begin
  result:=Brush.Gradient;
end;

procedure TSeriesBandTool.SetSeries(const Value: TChartSeries);
begin
  inherited;
  Repaint;
end;

procedure TSeriesBandTool.SetSeries2(const Value: TChartSeries);
begin
  if FSeries2<>Value then
  begin
    if Assigned(FSeries2) then
       FSeries2.RemoveFreeNotification(Self);

    FSeries2:=Value;

    if Assigned(FSeries2) then
       FSeries2.FreeNotification(Self);

    Repaint;
  end;
end;

type
  TSeriesAccess=class(TChartSeries);

procedure TSeriesBandTool.DrawBandTool;

  function ZRect(const ZPosition:Integer):TRect;
  begin
    result:=ParentChart.ChartRect;

    {$IFDEF FMX}
    result.Inflate(-1,-1);
    {$ELSE}
    Dec(result.Bottom,1);
    Dec(result.Right,1);
    Inc(result.Top,1);
    Inc(result.Left,1);
    {$ENDIF}

    result:=ParentChart.Canvas.RectFromRectZ(result,ZPosition);
  end;

var
  tmpPoints : TPointArray;

  procedure DrawPolygon(const ZPosition:Integer);
  var tmp : TCanvas3D;
  begin
    tmp:=ParentChart.Canvas;

    if ParentChart.View3D then tmp.PolygonWithZ(tmpPoints,ZPosition)
                          else tmp.Polygon(tmpPoints);
  end;

  function CalcStairs(const ASeries:TChartSeries):Boolean;
  begin
    if Assigned(ASeries) then
    begin
      TSeriesAccess(ASeries).CalcFirstLastVisibleIndex;
      result:=(ASeries is TCustomSeries) and TCustomSeries(ASeries).Stairs;
    end
    else
      result:=False;
  end;

  procedure AddPointXY(const AX,AY:Integer);
  var i : Integer;
  begin
    i:=Length(tmpPoints);
    SetLength(tmpPoints,i+1);
    tmpPoints[i].X:=AX;
    tmpPoints[i].Y:=AY;
  end;

  procedure AddPoint(const ASeries:TChartSeries; IsStairs:Boolean;
                     Index:Integer; IsForward:Boolean=True);
  var i : Integer;
  begin
    i:=Length(tmpPoints);

    if IsStairs then
       SetLength(tmpPoints,i+2)
    else
       SetLength(tmpPoints,i+1);

    if IsForward then
    begin
      if isStairs then
      begin
        tmpPoints[i].X:=ASeries.CalcXPos(Index);

        if Index>0 then
           tmpPoints[i].Y:=ASeries.CalcYPos(Index-1)
        else
           tmpPoints[i].Y:=ASeries.CalcYPos(Index);

        Inc(i);
      end;

      tmpPoints[i].X:=ASeries.CalcXPos(Index);
      tmpPoints[i].Y:=ASeries.CalcYPos(Index);
    end
    else
    begin
      tmpPoints[i].X:=ASeries.CalcXPos(Index);
      tmpPoints[i].Y:=ASeries.CalcYPos(Index);
      Inc(i);

      if isStairs then
      begin
        tmpPoints[i].X:=tmpPoints[i-1].X;

        if Index>0 then
           tmpPoints[i].Y:=ASeries.CalcYPos(Index-1)
        else
           tmpPoints[i].Y:=tmpPoints[i-1].Y;
      end;
    end;
  end;

  var X1,X2,V1,V2 : TChartValues;

  function TryAddCrossPoint(t:Integer; isStairs,isStairs2:Boolean):Boolean;
  var tmp1,
      tmp2,
      X,
      Y : Double;
  begin
    if isStairs then tmp1:=V1[t-1]
                else tmp1:=V1[t];

    if isStairs2 then tmp2:=V2[t-1]
                 else tmp2:=V2[t];

    result:=CrossingLines(X1[t],tmp1, X1[t-1],V1[t-1],
                          X2[t],tmp2, X2[t-1],V2[t-1], x,y);

    if result then
    begin
      if Series.YMandatory then
         AddPointXY(Series.CalcXPosValue(X),Series.CalcYPosValue(Y))
      else
         AddPointXY(Series.CalcXPosValue(Y),Series.CalcYPosValue(X));
    end
    else
    if IsStairs then
       AddPoint(Series,IsStairs,t);
  end;

var
  tmpBlend  : TTeeBlend;

  procedure PrepareBand(const ZPosition:Integer);
  var tmpR : TRect;
  begin
    if Transparency>0 then
    begin
      if ParentChart.Canvas.SupportsAlpha then
         tmpR:=TeeZeroRect
      else
         tmpR:=ZRect(ZPosition);

      tmpBlend:=ParentChart.Canvas.BeginBlending(tmpR,Transparency);
    end
    else
      tmpBlend:=nil;

    ParentChart.Canvas.AssignBrush(Brush);
    ParentChart.Canvas.AssignVisiblePen(Pen);

    if ParentChart.ClipPoints then
       ParentChart.Canvas.ClipRectangle(ZRect(ZPosition));
  end;

var t         : Integer;
    tt        : Integer;
    i         : Integer;
    tmpZ      : Integer;
    tmpMax    : Integer;
    tmpStart  : Integer;
    isStairs  : Boolean;
    isStairs2 : Boolean;
begin
  if Active and Assigned(ParentChart) and Assigned(Series) and Series.Active then
  begin
    if (
         Assigned(Series2) and
         (Series.FirstValueIndex<>-1) and
         (Series2.FirstValueIndex<>-1)
        )
        or
         (Series.FirstValueIndex<>-1) then
    begin
      isStairs:=CalcStairs(Series);
      isStairs2:=CalcStairs(Series2);

      tmpZ:=Series.StartZ;

      if Assigned(Series2) then
         tmpZ:=Math.Max(tmpZ,Series2.StartZ);

      PrepareBand(tmpZ);

      tmpPoints:=nil;
      try
        if Series.FirstValueIndex<>-1 then
        begin
          tmpMax:=Math.Max(0,Series.FirstValueIndex-1);

          if (FCondition=bcAll) or (not Assigned(Series2)) then
             for t:=tmpMax to Series.LastValueIndex do
                 AddPoint(Series,IsStairs,t)
          else
          begin
            t:=tmpMax;
            tmpStart:=t;

            X1:=Series.NotMandatoryValueList.Value;
            X2:=Series2.NotMandatoryValueList.Value;
            V1:=Series.MandatoryValueList.Value;
            V2:=Series2.MandatoryValueList.Value;

            repeat
              if ( (FCondition=bcLower) and (V1[t]<V2[t]) ) or
                 ( (FCondition=bcHigher) and (V1[t]>V2[t]) ) then
              begin
                if (t=tmpStart) and (t>0) then
                   TryAddCrossPoint(t,isStairs,isStairs2);

                AddPoint(Series,IsStairs,t);
                Inc(t);
              end
              else
              begin
                if t>tmpStart then
                begin
                  if TryAddCrossPoint(t,isStairs,isStairs2) then
                     Dec(t);

                  for tt:=t downto tmpStart do
                      AddPoint(Series2,IsStairs2,tt,False);

                  DrawPolygon(tmpZ);

                  tmpPoints:=nil;
                end;

                Inc(t);
                tmpStart:=t;
              end;

            until t>Series.LastValueIndex;

            if tmpStart<=t-1 then
            begin
              for tt:=t-1 downto tmpStart do
                  AddPoint(Series2,IsStairs2,tt,False);

              DrawPolygon(tmpZ);
            end;
          end;
        end;

        if Assigned(Series2) then
        begin
          if FCondition=bcAll then
            if Series2.FirstValueIndex<>-1 then
            begin
              tmpMax:=Math.Max(0,Series2.FirstValueIndex-1);

              for t:=Series2.LastValueIndex downto tmpMax do
                  AddPoint(Series2,IsStairs2,t,False);
            end;
        end
        else
        begin
          // need only two points for bound right and left coordinate

          i:=Length(tmpPoints);
          SetLength(tmpPoints, i+2);

          tmpPoints[i].X:=tmpPoints[i-1].X;
          tmpPoints[i].Y:=Series.CalcYPosValue(FBoundValue);
          tmpPoints[i+1].X:=tmpPoints[0].X;
          tmpPoints[i+1].Y:=tmpPoints[i].Y;
        end;

        DrawPolygon(tmpZ);
      finally
        tmpPoints:=nil;
      end;

      ParentChart.Canvas.UnClipRectangle;

      if Transparency>0 then
         ParentChart.Canvas.EndBlending(tmpBlend);
    end;
  end;
end;

class function TSeriesBandTool.GetEditorClass: String;
begin
  result:='TSeriesBandToolEdit';
end;

procedure TSeriesBandTool.SetGradient(const Value: TTeeGradient);
begin
  Gradient.Assign(Value);
end;

procedure TSeriesBandTool.SetTransparency(const Value: TTeeTransparency);
begin
  if FTransparency<>Value then
  begin
    FTransparency:=Value;
    Repaint;
  end;
end;

procedure TSeriesBandTool.SetBoundValue(const Value: double);
begin
  SetDoubleProperty(FBoundValue,Value);
end;

class function TSeriesBandTool.LongDescription: String;
begin
  result:=TeeMsg_SeriesBandDesc;
end;

procedure TSeriesBandTool.SetCondition(const Value: TBandCondition);
begin
  if FCondition<>Value then
  begin
    FCondition:=Value;
    Repaint;
  end;
end;

procedure TSeriesBandTool.Assign(Source: TPersistent);
begin
  if Source is TSeriesBandTool then
  begin
    FBoundValue:=TSeriesBandTool(Source).FBoundValue;
    FCondition:=TSeriesBandTool(Source).FCondition;
    FDrawBehind:=TSeriesBandTool(Source).FDrawBehind;
    Series2:=TSeriesBandTool(Source).FSeries2;
    FTransparency:=TSeriesBandTool(Source).FTransparency;
  end;

  inherited;
end;

initialization
  RegisterTeeTools([TSeriesBandTool]);
finalization
  UnRegisterTeeTools([TSeriesBandTool]);
end.
