{

 Axis transformations.

 *****************************************************************************
  See the file COPYING.modifiedLGPL.txt, included in this distribution,
  for details about the license.
 *****************************************************************************

 Authors: Alexander Klenin

}
unit TATransformations;

{$MODE ObjFPC}{$H+}

interface

uses
  Classes, SysUtils,
  TAChartUtils;

type

  TChartAxisTransformations = class;

  { TAxisTransform }

  TAxisTransform = class(TIndexedComponent)
  strict private
    FEnabled: Boolean;
    FTransformations: TChartAxisTransformations;
    procedure SetEnabled(AValue: Boolean);
    procedure SetTransformations(AValue: TChartAxisTransformations);
  protected
    procedure ReadState(Reader: TReader); override;
    procedure SetParentComponent(AParent: TComponent); override;
  protected
    procedure Changed;
    function GetIndex: Integer; override;
    procedure SetIndex(AValue: Integer); override;
  protected
    FDrawData: TDrawDataItem;
    procedure ClearBounds; virtual;
    function GetDrawDataClass: TDrawDataItemClass; virtual;
    procedure SetChart(AChart: TObject);
    procedure UpdateBounds(var AMin, AMax: Double); virtual;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  public
    procedure Assign(ASource: TPersistent); override;
    function GetParentComponent: TComponent; override;
    function HasParent: Boolean; override;
  public
    function AxisToGraph(AX: Double): Double; virtual;
    function GraphToAxis(AX: Double): Double; virtual;

    property Transformations: TChartAxisTransformations
      read FTransformations write SetTransformations;
  published
    property Enabled: Boolean read FEnabled write SetEnabled default true;
  end;

  TAxisTransformClass = class of TAxisTransform;

  {$IFNDEF fpdoc} // Workaround for issue #18549.
  TAxisTransformEnumerator = specialize TTypedFPListEnumerator<TAxisTransform>;
  {$ENDIF}

  TAxisTransformList = class(TIndexedComponentList)
  public
    function GetEnumerator: TAxisTransformEnumerator;
  end;

  { TChartAxisTransformations }

  TChartAxisTransformations = class(TComponent)
  strict private
    FBroadcaster: TBroadcaster;
    FList: TAxisTransformList;
  protected
    procedure SetName(const AValue: TComponentName); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  public
    procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
    procedure SetChildOrder(Child: TComponent; Order: Integer); override;
  public
    function AxisToGraph(AX: Double): Double;
    procedure ClearBounds;
    function GraphToAxis(AX: Double): Double;
    procedure SetChart(AChart: TObject);
    procedure UpdateBounds(var AMin, AMax: Double);

    property Broadcaster: TBroadcaster read FBroadcaster;
  published
    property List: TAxisTransformList read FList;
  end;

  { TLinearAxisTransform }

  TLinearAxisTransform = class(TAxisTransform)
  strict private
    FOffset: Double;
    FScale: Double;
    function OffsetIsStored: Boolean;
    function ScaleIsStored: Boolean;
    procedure SetOffset(AValue: Double);
    procedure SetScale(AValue: Double);
  public
    constructor Create(AOwner: TComponent); override;
  public
    procedure Assign(ASource: TPersistent); override;

    function AxisToGraph(AX: Double): Double; override;
    function GraphToAxis(AX: Double): Double; override;
  published
    property Offset: Double read FOffset write SetOffset stored OffsetIsStored;
    property Scale: Double read FScale write SetScale stored ScaleIsStored;
  end;

  { TAutoScaleAxisTransform }

  TAutoScaleAxisTransform = class(TAxisTransform)
  strict private
    FMaxValue: Double;
    FMinValue: Double;
    function MaxValueIsStored: Boolean;
    function MinValueIsStored: Boolean;
    procedure SetMaxValue(AValue: Double);
    procedure SetMinValue(AValue: Double);
  protected
    procedure ClearBounds; override;
    function GetDrawDataClass: TDrawDataItemClass; override;
    procedure UpdateBounds(var AMin, AMax: Double); override;
  public
    constructor Create(AOwner: TComponent); override;
  public
    procedure Assign(ASource: TPersistent); override;

    function AxisToGraph(AX: Double): Double; override;
    function GraphToAxis(AX: Double): Double; override;
  published
    property MaxValue: Double
      read FMaxValue write SetMaxValue stored MaxValueIsStored;
    property MinValue: Double
      read FMinValue write SetMinValue stored MinValueIsStored;
  end;

  { TLogarithmAxisTransform }

  TLogarithmAxisTransform = class(TAxisTransform)
  strict private
    FBase: Double;
    procedure SetBase(AValue: Double);
  public
    constructor Create(AOwner: TComponent); override;
  public
    procedure Assign(Source: TPersistent); override;

    function AxisToGraph(AX: Double): Double; override;
    function GraphToAxis(AX: Double): Double; override;
  published
    property Base: Double read FBase write SetBase;
  end;

  TCumulNormDistrAxisTransform = class(TAxisTransform)
  public
    function AxisToGraph(AX: Double): Double; override;
    function GraphToAxis(AX: Double): Double; override;
  end;

  TTransformEvent = procedure (AX: Double; out AT: Double) of object;

  { TUserDefinedAxisTransform }

  TUserDefinedAxisTransform = class(TAxisTransform)
  private
    FOnAxisToGraph: TTransformEvent;
    FOnGraphToAxis: TTransformEvent;
    procedure SetOnAxisToGraph(AValue: TTransformEvent);
    procedure SetOnGraphToAxis(AValue: TTransformEvent);
  public
    procedure Assign(ASource: TPersistent); override;

    function AxisToGraph(AX: Double): Double; override;
    function GraphToAxis(AX: Double): Double; override;
  published
    property OnAxisToGraph: TTransformEvent read FOnAxisToGraph write SetOnAxisToGraph;
    property OnGraphToAxis: TTransformEvent read FOnGraphToAxis write SetOnGraphToAxis;
  end;

  procedure Register;

  procedure RegisterAxisTransformClass(AAxisTransformClass: TAxisTransformClass;
    const ACaption: String); overload;
  procedure RegisterAxisTransformClass(AAxisTransformClass: TAxisTransformClass;
    ACaptionPtr: PStr); overload;

implementation

uses
  ComponentEditors, Forms, Math, PropEdits,
  TAChartStrConsts, TAMath, TASubcomponentsEditor;

type
  { TAxisTransformsComponentEditor }

  TAxisTransformsComponentEditor = class(TSubComponentListEditor)
  protected
    function MakeEditorForm: TForm; override;
  public
    function GetVerb(Index: Integer): string; override;
  end;

  { TAxisTransformsPropertyEditor }

  TAxisTransformsPropertyEditor = class(TComponentListPropertyEditor)
  protected
    function GetChildrenCount: Integer; override;
    function MakeEditorForm: TForm; override;
  end;

  { TAxisTransformsEditorForm }

  TAxisTransformsEditorForm = class(TComponentListEditorForm)
  protected
    procedure AddSubcomponent(AParent, AChild: TComponent); override;
    procedure BuildCaption; override;
    function ChildClass: TComponentClass; override;
    procedure EnumerateSubcomponentClasses; override;
    function GetChildrenList: TFPList; override;
    function MakeSubcomponent(
      AOwner: TComponent; ATag: Integer): TComponent; override;
  end;

  TAutoScaleTransformData = class (TDrawDataItem)
  private
    FMin, FMax, FOffset, FScale: Double;
  end;

var
  AxisTransformsClassRegistry: TClassRegistry;

procedure Register;
var
  i: Integer;
begin
  with AxisTransformsClassRegistry do
    for i := 0 to Count - 1 do
      RegisterNoIcon([TAxisTransformClass(GetClass(i))]);
  RegisterComponents(CHART_COMPONENT_IDE_PAGE, [TChartAxisTransformations]);
  RegisterPropertyEditor(
    TypeInfo(TAxisTransformList), TChartAxisTransformations,
    'List', TAxisTransformsPropertyEditor);
  RegisterComponentEditor(
    TChartAxisTransformations, TAxisTransformsComponentEditor);
end;

procedure RegisterAxisTransformClass(AAxisTransformClass: TAxisTransformClass;
  const ACaption: String);
begin
  RegisterClass(AAxisTransformClass);
  with AxisTransformsClassRegistry do
    if IndexOfClass(AAxisTransformClass) < 0 then
      Add(TClassRegistryItem.Create(AAxisTransformClass, ACaption));
end;

procedure RegisterAxisTransformClass(AAxisTransformClass: TAxisTransformClass;
  ACaptionPtr: PStr);
begin
  RegisterClass(AAxisTransformClass);
  with AxisTransformsClassRegistry do
    if IndexOfClass(AAxisTransformClass) < 0 then
      Add(TClassRegistryItem.CreateRes(AAxisTransformClass, ACaptionPtr));
end;

{ TAxisTransformList }

function TAxisTransformList.GetEnumerator: TAxisTransformEnumerator;
begin
  Result := TAxisTransformEnumerator.Create(Self);
end;

{ TAxisTransformsComponentEditor }

function TAxisTransformsComponentEditor.GetVerb(Index: Integer): string;
begin
  if Index = 0 then
    Result := tasAxisTransformsEditorTitle
  else
    Result := '';
end;

function TAxisTransformsComponentEditor.MakeEditorForm: TForm;
begin
  Result := TAxisTransformsEditorForm.Create(Application, GetComponent, Self, nil);
end;

{ TAxisTransformsPropertyEditor }

function TAxisTransformsPropertyEditor.GetChildrenCount: Integer;
begin
  Result := (GetObjectValue as TAxisTransformList).Count;
end;

function TAxisTransformsPropertyEditor.MakeEditorForm: TForm;
begin
  with TAxisTransformsEditorForm do
    Result := Create(Application, GetComponent(0) as TComponent, nil, Self);
end;

{ TAxisTransformsEditorForm }

procedure TAxisTransformsEditorForm.AddSubcomponent(
  AParent, AChild: TComponent);
begin
  (AChild as TAxisTransform).Transformations :=
    AParent as TChartAxisTransformations;
end;

procedure TAxisTransformsEditorForm.BuildCaption;
begin
  Caption := tasAxisTransformsEditorTitle + ' - ' + Parent.Name;
end;

function TAxisTransformsEditorForm.ChildClass: TComponentClass;
begin
  Result := TAxisTransform;
end;

procedure TAxisTransformsEditorForm.EnumerateSubcomponentClasses;
var
  i: Integer;
begin
  for i := 0 to AxisTransformsClassRegistry.Count - 1 do
    AddSubcomponentClass(AxisTransformsClassRegistry.GetCaption(i), i);
end;

function TAxisTransformsEditorForm.GetChildrenList: TFPList;
begin
  Result := (Parent as TChartAxisTransformations).List;
end;

function TAxisTransformsEditorForm.MakeSubcomponent(
  AOwner: TComponent; ATag: Integer): TComponent;
begin
  with AxisTransformsClassRegistry do
    Result := TAxisTransformClass(GetClass(ATag)).Create(AOwner);
end;

{ TAxisTransform }

procedure TAxisTransform.Assign(ASource: TPersistent);
begin
  if ASource is TAxisTransform then
    with TAxisTransform(ASource) do
      Self.FEnabled := Enabled
  else
    inherited Assign(ASource);
end;

function TAxisTransform.AxisToGraph(AX: Double): Double;
begin
  Result := AX;
end;

procedure TAxisTransform.Changed;
begin
  if Transformations <> nil then
    Transformations.Broadcaster.Broadcast(Self);
end;

procedure TAxisTransform.ClearBounds;
begin
  // empty
end;

constructor TAxisTransform.Create(AOwner: TComponent);
begin
  FEnabled := true;
  inherited Create(AOwner);
end;

destructor TAxisTransform.Destroy;
begin
  Transformations := nil;
  DrawData.DeleteByOwner(Self);
  inherited;
end;

function TAxisTransform.GetDrawDataClass: TDrawDataItemClass;
begin
  Result := nil;
end;

function TAxisTransform.GetIndex: Integer;
begin
  if Transformations = nil then
    Result := -1
  else
    Result := Transformations.List.IndexOf(Self);
end;

function TAxisTransform.GetParentComponent: TComponent;
begin
  Result := Transformations;
end;

function TAxisTransform.GraphToAxis(AX: Double): Double;
begin
  Result := AX;
end;

function TAxisTransform.HasParent: Boolean;
begin
  Result := true;
end;

procedure TAxisTransform.ReadState(Reader: TReader);
begin
  inherited ReadState(Reader);
  if Reader.Parent is TChartAxisTransformations then
    Transformations := TChartAxisTransformations(Reader.Parent);
end;

procedure TAxisTransform.SetChart(AChart: TObject);
begin
  if GetDrawDataClass = nil then exit;
  FDrawData := DrawData.Find(AChart, Self);
  if FDrawData <> nil then exit;
  FDrawData := GetDrawDataClass.Create(AChart, Self);
  DrawData.Add(FDrawData);
end;

procedure TAxisTransform.SetEnabled(AValue: Boolean);
begin
  if FEnabled = AValue then exit;
  FEnabled := AValue;
  Changed;
end;

procedure TAxisTransform.SetIndex(AValue: Integer);
begin
  with Transformations.List do
    Move(Index, EnsureRange(AValue, 0, Count - 1));
end;

procedure TAxisTransform.SetParentComponent(AParent: TComponent);
begin
  if not (csLoading in ComponentState) then
    Transformations := AParent as TChartAxisTransformations;
end;

procedure TAxisTransform.SetTransformations(AValue: TChartAxisTransformations);
begin
  if FTransformations = AValue then exit;
  if FTransformations  <> nil then
    FTransformations.List.Remove(Self);
  FTransformations := AValue;
  if FTransformations <> nil then
    FTransformations.List.Add(Self);
end;

procedure TAxisTransform.UpdateBounds(var AMin, AMax: Double);
begin
  if not IsInfinite(AMin) then
    AMin := AxisToGraph(AMin);
  if not IsInfinite(AMax) then
    AMax := AxisToGraph(AMax);
end;

{ TChartAxisTransformations }

function TChartAxisTransformations.AxisToGraph(AX: Double): Double;
var
  t: TAxisTransform;
begin
  Result := AX;
  if IsNan(Result) then exit;
  for t in List do
    if t.Enabled then
      Result := t.AxisToGraph(Result);
end;

procedure TChartAxisTransformations.ClearBounds;
var
  t: TAxisTransform;
begin
  for t in List do
    if t.Enabled then
      t.ClearBounds;
end;

constructor TChartAxisTransformations.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FBroadcaster := TBroadcaster.Create;
  FList := TAxisTransformList.Create;
end;

destructor TChartAxisTransformations.Destroy;
begin
  while List.Count > 0 do
    TAxisTransform(List[List.Count - 1]).Free;
  FreeAndNil(FList);
  FreeAndNil(FBroadcaster);
  inherited;
end;

procedure TChartAxisTransformations.GetChildren(
  Proc: TGetChildProc; Root: TComponent);
var
  t: TAxisTransform;
begin
  for t in List do
    if t.Owner = Root then
      Proc(t);
end;

function TChartAxisTransformations.GraphToAxis(AX: Double): Double;
var
  i: Integer;
begin
  Result := AX;
  for i := List.Count - 1 downto 0 do
    with TAxisTransform(List[i]) do
      if Enabled then
        Result := GraphToAxis(Result);
end;

procedure TChartAxisTransformations.SetChart(AChart: TObject);
var
  t: TAxisTransform;
begin
  for t in List do
    if t.Enabled then
      t.SetChart(AChart);
end;

procedure TChartAxisTransformations.SetChildOrder(
  Child: TComponent; Order: Integer);
var
  i: Integer;
begin
  i := List.IndexOf(Child);
  if i >= 0 then
    List.Move(i, Order);
end;

procedure TChartAxisTransformations.SetName(const AValue: TComponentName);
var
  oldName: String;
begin
  if Name = AValue then exit;
  oldName := Name;
  inherited SetName(AValue);
  if csDesigning in ComponentState then
    List.ChangeNamePrefix(oldName, AValue);
end;

procedure TChartAxisTransformations.UpdateBounds(var AMin, AMax: Double);
var
  t: TAxisTransform;
begin
  for t in List do
    if t.Enabled then
      t.UpdateBounds(AMin, AMax);
end;

{ TLinearAxisTransform }

procedure TLinearAxisTransform.Assign(ASource: TPersistent);
begin
  if ASource is TLinearAxisTransform then
    with TLinearAxisTransform(ASource) do begin
      Self.FOffset := Offset;
      Self.FScale := Scale;
    end;
  inherited Assign(ASource);
end;

function TLinearAxisTransform.AxisToGraph(AX: Double): Double;
begin
  Result := AX * Scale + Offset;
end;

constructor TLinearAxisTransform.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FScale := 1.0;
end;

function TLinearAxisTransform.GraphToAxis(AX: Double): Double;
begin
  Result := (AX - Offset) / Scale;
end;

function TLinearAxisTransform. OffsetIsStored: Boolean;
begin
  Result := not SameValue(Offset, 0.0);
end;

function TLinearAxisTransform.ScaleIsStored: Boolean;
begin
  Result := not SameValue(Scale, 1.0);
end;

procedure TLinearAxisTransform.SetOffset(AValue: Double);
begin
  if SameValue(FOffset, AValue) then exit;
  FOffset := AValue;
  Changed;
end;

procedure TLinearAxisTransform.SetScale(AValue: Double);
begin
  if SameValue(FScale, AValue) then exit;
  FScale := AValue;
  if SameValue(FScale, 0.0) then FScale := 1.0;
  Changed;
end;

{ TLogarithmAxisTransform }

procedure TLogarithmAxisTransform.Assign(Source: TPersistent);
begin
  if Source is TLogarithmAxisTransform then
    with Source as TLogarithmAxisTransform do
      Self.FBase := Base
  else
    inherited Assign(Source);
end;

function TLogarithmAxisTransform.AxisToGraph(AX: Double): Double;
begin
  if AX > 0 then
    Result := LogN(Base, AX)
  else
    Result := NegInfinity;
end;

constructor TLogarithmAxisTransform.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FBase := Exp(1);
end;

function TLogarithmAxisTransform.GraphToAxis(AX: Double): Double;
begin
  Result := Power(Base, AX);
end;

procedure TLogarithmAxisTransform.SetBase(AValue: Double);
begin
  if FBase = AValue then exit;
  if (AValue <= 0) or (AValue = 1.0) then
    raise Exception.Create(rsInvalidLogBase);
  FBase := AValue;
  Changed;
end;

{ TAutoScaleAxisTransform }

procedure TAutoScaleAxisTransform.Assign(ASource: TPersistent);
begin
  if ASource is TAutoScaleAxisTransform then
    with TAutoScaleAxisTransform(ASource) do begin
      Self.FMinValue := FMinValue;
      Self.FMaxValue := FMaxValue;
    end;
  inherited Assign(ASource);
end;

function TAutoScaleAxisTransform.AxisToGraph(AX: Double): Double;
begin
  with TAutoScaleTransformData(FDrawData) do
    Result := AX * FScale + FOffset;
end;

procedure TAutoScaleAxisTransform.ClearBounds;
begin
  inherited ClearBounds;

  // Avoid crashing when called too early, e.g. when a TNavPanel is on the form
  // https://forum.lazarus.freepascal.org/index.php/topic,47429.0.html
  if FDrawData = nil then
    exit;

  with TAutoScaleTransformData(FDrawData) do begin
    FMin := SafeInfinity;
    FMax := NegInfinity;
    FOffset := 0.0;
    FScale := 1.0;
  end;
end;

constructor TAutoScaleAxisTransform.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FMaxValue := 1.0;
end;

function TAutoScaleAxisTransform.GetDrawDataClass: TDrawDataItemClass;
begin
  Result := TAutoScaleTransformData;
end;

function TAutoScaleAxisTransform.GraphToAxis(AX: Double): Double;
begin
  with TAutoScaleTransformData(FDrawData) do
    Result := (AX - FOffset) / FScale;
end;

function TAutoScaleAxisTransform.MaxValueIsStored: Boolean;
begin
  Result := MaxValue <> 1.0;
end;

function TAutoScaleAxisTransform.MinValueIsStored: Boolean;
begin
  Result := MinValue <> 0.0;
end;

procedure TAutoScaleAxisTransform.SetMaxValue(AValue: Double);
begin
  if FMaxValue = AValue then exit;
  FMaxValue := AValue;
  Changed;
end;

procedure TAutoScaleAxisTransform.SetMinValue(AValue: Double);
begin
  if FMinValue = AValue then exit;
  FMinValue := AValue;
  Changed;
end;

procedure TAutoScaleAxisTransform.UpdateBounds(var AMin, AMax: Double);
begin
  // Auto-scale is only defined for finite bounds.
  if IsInfinite(AMin) or IsInfinite(AMax) then exit;
  with TAutoScaleTransformData(FDrawData) do begin
    UpdateMinMax(AMin, FMin, FMax);
    UpdateMinMax(AMax, FMin, FMax);
    if FMax = FMin then
      FScale := 1.0
    else
      FScale := (MaxValue - MinValue) / (FMax - FMin);
    FOffset := MinValue - FMin * FScale;
  end;
  AMin := MinValue;
  AMax := MaxValue;
end;

{ TCumulNormDistrAxisTransform }

function TCumulNormDistrAxisTransform.AxisToGraph(AX: Double): Double;
begin
  Result := InvCumulNormDistr(AX);
end;

function TCumulNormDistrAxisTransform.GraphToAxis(AX: Double): Double;
begin
  Result := CumulNormDistr(AX);
end;

{ TUserDefinedAxisTransform }

procedure TUserDefinedAxisTransform.Assign(ASource: TPersistent);
begin
  if ASource is TUserDefinedAxisTransform then
    with TUserDefinedAxisTransform(ASource) do begin
      Self.FOnAxisToGraph := FOnAxisToGraph;
      Self.FOnGraphToAxis := FOnGraphToAxis;
    end;
  inherited Assign(ASource);
end;

function TUserDefinedAxisTransform.AxisToGraph(AX: Double): Double;
begin
  if Assigned(OnAxisToGraph) then
    OnAxisToGraph(AX, Result)
  else
    Result := AX;
end;

function TUserDefinedAxisTransform.GraphToAxis(AX: Double): Double;
begin
  if Assigned(OnGraphToAxis) then
    OnGraphToAxis(AX, Result)
  else
    Result := AX;
end;

procedure TUserDefinedAxisTransform.SetOnAxisToGraph(AValue: TTransformEvent);
begin
  if TMethod(FOnAxisToGraph) = TMethod(AValue) then exit;
  FOnAxisToGraph := AValue;
  Changed;
end;

procedure TUserDefinedAxisTransform.SetOnGraphToAxis(AValue: TTransformEvent);
begin
  if TMethod(FOnGraphToAxis) = TMethod(AValue) then exit;
  FOnGraphToAxis := AValue;
  Changed;
end;

initialization

  AxisTransformsClassRegistry := TClassRegistry.Create;
  RegisterAxisTransformClass(TAutoScaleAxisTransform, @rsAutoScale);
  RegisterAxisTransformClass(
    TCumulNormDistrAxisTransform, @rsCumulativeNormalDistribution);
  RegisterAxisTransformClass(TLinearAxisTransform, @rsLinear);
  RegisterAxisTransformClass(TLogarithmAxisTransform, @rsLogarithmic);
  RegisterAxisTransformClass(TUserDefinedAxisTransform, @rsUserDefined);

finalization

  FreeAndNil(AxisTransformsClassRegistry);

end.

