unit ASVisualization;

interface

uses
  SysUtils, Types, Classes, Generics.Defaults, Generics.Collections, ASNum,
  ASObjects, ASKernelDefs, VisCtl2D, ASPixmap, DoublePoint;

type
  TCategoryData = record
    Name: string;
    Value: TASR;
  end;
  TCategoryDataList = TList<TCategoryData>;

  PDataRange = ^TDataRange;
  TDataRange = record
    Min, Max: Double;
    function Span: Double; inline;
    function SpanOrUnit: Double; inline;
  end;

  THistogramData = class
    Numbers: TArray<TASR>;
  end;

  TScatterDataR2 = class
    Points: TArray<TASR2>;
  end;

  TRegionDataR2 = class
    Slices: TArray<TSlice>;
    Axis: TCartesianAxis;
    UnboundedMin,
    UnboundedMax: Boolean;
  end;

  THeatmapData = class
    Pixmap: TASPixmap;
    x0, x1,
    y0, y1: TASR;
  end;

  TVectorFieldData = class
    Vectors: TArray<TPair<TPointD, TVectorD>>;
  end;

  TLineDataR2 = class
    a, b: TPointD;
  end;

  TRectangleData = class
    a: TPointD;
    w, h: TASR;
  end;

  TCircleData = class
    a: TPointD;
    r: TASR;
  end;

  TPolygonData = TScatterDataR2;

  TTextData = class
    Position: TPointD;
    Text: string;
  end;

  TPixmapData = class
    Rect: TRectD;
    Pixmap: TASPixmap;
  end;

function GetCategoryData(AList: TArray<TAlgosimObject>; ACatCount: PInteger = nil;
  AYStats: PDataRange = nil): TCategoryDataList;

function GetHistogramData(ANumbers: TArray<TASR>; AXStats: PDataRange = nil): THistogramData;

function GetScatterDataR2(APoints: TArray<TASR2>; AXStats: PDataRange = nil;
  AYStats: PDataRange = nil): TScatterDataR2;

function GetRegionDataR2(ASlices: TArray<TSlice>; AUnboundedMin, AUnboundedMax: Boolean;
  AXStats: PDataRange = nil; AYStats: PDataRange = nil): TRegionDataR2;

type
  TVisualKind = (vkNull, vkBarChart, vkPieChart, vkHistogram, vkXYPlot, vkRegion,
    vkHeatmap, vkVectorField, vkPixmap, vkText, vkLine, vkRectangle, vkCircle,
    vkPolygon);

  TVisSetupProc = reference to procedure(ACtl: TVisCtl2D; ADrawable: TDrawable);

  TVisual = class
    Kind: TVisualKind;
    Data: TObject;
    ViewSetupProc,
    OwnSetupProc: TVisSetupProc;
    constructor Create(AKind: TVisualKind; AData: TObject;
      AViewSetupProc: TVisSetupProc = nil;
      AOwnSetupProc: TVisSetupProc = nil);
    destructor Destroy; override;
  end;

implementation

uses
  Math;

function GetCategoryData(AList: TArray<TAlgosimObject>; ACatCount: PInteger;
  AYStats: PDataRange): TCategoryDataList;

  procedure Inv;
  begin
    raise Exception.Create('Invalid category data list.');
  end;

var
  i: Integer;
  elem: TAlgosimObject;
  cd: TCategoryData;
begin
  if Assigned(ACatCount) then
    ACatCount^ := Length(AList);
  if Assigned(AYStats) then
    FillChar(AYStats^, SizeOf(AYStats^), 0);
  Result := TCategoryDataList.Create;
  try
    for i := 0 to High(AList) do
    begin
      elem := AList[i];
      if elem.IsObjectContainer and (elem.ElementCount = 2) and elem.Elements[2].TryToASR(cd.Value) then
      begin
        cd.Name := elem.Elements[1].ToString;
        Result.Add(cd);
        if Assigned(AYStats) then
        begin
          if i = 1 then
          begin
            AYStats.Min := cd.Value;
            AYStats.Max := cd.Value;
          end
          else
          begin
            if cd.Value < AYStats.Min then
              AYStats.Min := cd.Value;
            if cd.Value > AYStats.Max then
              AYStats.Max := cd.Value;
          end
        end;
      end
      else
        Inv;
    end;
  except
    Result.Free;
    raise;
  end;
end;

function GetHistogramData(ANumbers: TArray<TASR>; AXStats: PDataRange): THistogramData;
begin

  if Assigned(AXStats) then
  begin
    FillChar(AXStats^, SizeOf(AXStats^), 0);
    if Length(ANumbers) > 0 then
    begin
      AXStats.Min := MinValue(ANumbers);
      AXStats.Max := MaxValue(ANumbers);
    end;
  end;

  Result := THistogramData.Create;
  try
    Result.Numbers := ANumbers;
  except
    Result.Free;
    raise;
  end;

end;

function GetScatterDataR2(APoints: TArray<TASR2>; AXStats: PDataRange = nil;
  AYStats: PDataRange = nil): TScatterDataR2;
var
  i: Integer;
begin

  if Assigned(AXStats) then
  begin
    FillChar(AXStats^, SizeOf(AXStats^), 0);
    if Length(APoints) > 0 then
    begin
      AXStats.Min := APoints[0].X;
      AXStats.Max := APoints[0].X;
      for i := 0 to High(APoints) do
      begin
        if APoints[i].X < AXStats.Min then
          AXStats.Min := APoints[i].X;
        if APoints[i].X > AXStats.Max then
          AXStats.Max := APoints[i].X;
      end;
    end;
  end;

  if Assigned(AYStats) then
  begin
    FillChar(AYStats^, SizeOf(AYStats^), 0);
    if Length(APoints) > 0 then
    begin
      AYStats.Min := APoints[0].Y;
      AYStats.Max := APoints[0].Y;
      for i := 0 to High(APoints) do
      begin
        if APoints[i].Y < AYStats.Min then
          AYStats.Min := APoints[i].Y;
        if APoints[i].Y > AYStats.Max then
          AYStats.Max := APoints[i].Y;
      end;
    end;
  end;

  Result := TScatterDataR2.Create;
  try
    Result.Points := APoints;
  except
    Result.Free;
    raise;
  end;

end;

function GetRegionDataR2(ASlices: TArray<TSlice>; AUnboundedMin, AUnboundedMax: Boolean;
  AXStats: PDataRange = nil; AYStats: PDataRange = nil): TRegionDataR2;
var
  i: Integer;
begin

  if Assigned(AXStats) then
  begin
    FillChar(AXStats^, SizeOf(AXStats^), 0);
    if Length(ASlices) > 0 then
    begin
      AXStats.Min := ASlices[0].t;
      AXStats.Max := ASlices[0].t;
      for i := 0 to High(ASlices) do
      begin
        if ASlices[i].t < AXStats.Min then
          AXStats.Min := ASlices[i].t;
        if ASlices[i].t > AXStats.Max then
          AXStats.Max := ASlices[i].t;
      end;
    end;
  end;

  if Assigned(AYStats) then
  begin
    FillChar(AYStats^, SizeOf(AYStats^), 0);
    if Length(ASlices) > 0 then
    begin
      if not AUnboundedMin then
      begin
        AYStats.Min := ASlices[0].a;
        AYStats.Max := ASlices[0].a;
      end;
      if not AUnboundedMax then
      begin
        AYStats.Min := ASlices[0].b;
        AYStats.Max := ASlices[0].b;
      end;
      for i := 0 to High(ASlices) do
      begin
        if not AUnboundedMin then
        begin
          if ASlices[i].a < AYStats.Min then
            AYStats.Min := ASlices[i].a;
          if ASlices[i].a > AYStats.Max then
            AYStats.Max := ASlices[i].a;
        end;
        if not AUnboundedMax then
        begin
          if ASlices[i].b < AYStats.Min then
            AYStats.Min := ASlices[i].b;
          if ASlices[i].b > AYStats.Max then
            AYStats.Max := ASlices[i].b;
        end;
      end;
    end;
  end;

  Result := TRegionDataR2.Create;
  try
    Result.Slices := ASlices;
  except
    Result.Free;
    raise;
  end;

end;

{ TVisual }

constructor TVisual.Create(AKind: TVisualKind; AData: TObject;
  AViewSetupProc, AOwnSetupProc: TVisSetupProc);
begin
  Kind := AKind;
  Data := AData;
  ViewSetupProc := AViewSetupProc;
  OwnSetupProc := AOwnSetupProc;
end;

destructor TVisual.Destroy;
begin
  FreeAndNil(Data);
  inherited;
end;

{ TDataRange }

function TDataRange.Span: Double;
begin
  Result := Max - Min;
end;

function TDataRange.SpanOrUnit: Double;
begin
  if Min < Max then
    Result := Max - Min
  else
    Result := 1;
end;

end.
