{********************************************}
{ TSeriesTextSource Editor                   }
{ Copyright (c) 2000-2020 by Steema Software }
{    All Rights Reserved                     }
{********************************************}
unit TeeSeriesTextEd;
{$I TeeDefs.inc}

interface

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

     {$IFDEF D9}
     Types,
     {$ENDIF}
     
     Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, ComCtrls,
     Grids, Buttons,

     TeeURL, TeeProcs, TeEngine, TeCanvas, TeeSourceEdit, TeeStringsEditor
     {$IFDEF AXUNICODE}
     ,Unicode, TeeUnicode
     {$ENDIF}
     ;

Const
  TeeDefaultFieldSeparator=',';

type
  TSeriesTextField=class;

  TSeriesTextGetValue=procedure(Field:TSeriesTextField;
                                Const Text:String; Var Value:Double) of object;

  TSeriesTextField=class(TCollectionItem)
  private
    FFieldIndex : Integer;
    FFieldName  : String;
    FFieldType  : TTeeFieldType;
    FOnGetValue : TSeriesTextGetValue;

    ISeries     : TChartSeries;

    procedure SetFieldIndex(const Value: Integer);
  protected
    { Protected declarations }
    Data : TObject;
  public
    property FieldType:TTeeFieldType read FFieldType write FFieldType;
  published
    property FieldIndex:Integer read FFieldIndex write SetFieldIndex;
    property FieldName:String read FFieldName write FFieldName;
    property OnGetValue:TSeriesTextGetValue read FOnGetValue write FOnGetValue;
  end;

  TSeriesTextFields=class(TOwnedCollection)
  private
    Function Get(Index:Integer):TSeriesTextField;
    Procedure Put(Index:Integer; Const Value:TSeriesTextField);
  protected
    function AddField(ANum:Integer; const AName:String; ASeries:TChartSeries=nil):TSeriesTextField;
  public
    property Items[Index:Integer]:TSeriesTextField read Get write Put; default;
  end;

  {$IFDEF D16}
  [ComponentPlatformsAttribute(pidWin32 or pidWin64)]
  {$ENDIF}
  TSeriesTextSource=class(TTeeSeriesSourceFile)
  private
    FAutomatic  : Boolean;
    FDataSource : TComponent;
    FFields     : TSeriesTextFields;
    FHeader     : Integer;
    FSeparator  : String;
    FText       : TStrings;
    FTrim       : Boolean;

    procedure SetAutomatic(const Value:Boolean);
    procedure SetDataSource(const Value:TComponent);
    procedure SetFields(const Value: TSeriesTextFields);
    procedure SetText(const Value: TStrings);
  protected
    CanCreateSeries : Boolean;

    procedure GuessFields(const AFields:TSeriesTextFields; const AStrings:TStrings;
                          out ASeparator:String);
    class function GuessNumFields(S:String; out ASeparator:String):Integer;
    procedure Loaded; override;
    procedure Notification( AComponent: TComponent;
                            Operation: TOperation); override;
  public
    Constructor Create(AOwner:TComponent); override;
    Destructor Destroy; override;

    Function AddField(Const AName:String; AIndex:Integer):TSeriesTextField;

    class Function Description:String; override;
    class Function Editor:TComponentClass; override;
    class function FindProperty(const S:TObject; const AClass:TClass):TObject; {$IFDEF D10}static;{$ENDIF}

    procedure Load; override;
    procedure LoadFromStream(AStream: TStream); override;
    Procedure LoadFromStrings(const AStrings:TStrings);
    {$IFDEF AXUNICODE}
    Procedure LoadFromWideStrings(const AStrings:TWideStringList);
    {$ENDIF}

    class function TryLocalStrToFloat(const S:String; out Value:{$IFNDEF D7}Double{$ELSE}Extended{$ENDIF}):Boolean;
  published
    property Active;
    property Automatic:Boolean read FAutomatic write SetAutomatic default True;
    property Chart;
    property DataSource:TComponent read FDataSource write SetDataSource;
    property HeaderLines:Integer read FHeader write FHeader default 0;
    property Fields:TSeriesTextFields read FFields write SetFields;
    property FieldSeparator:String read FSeparator write FSeparator;
    property FileName;
    property Series;
    property Text:TStrings read FText write SetText;
    property TrimSpaces:Boolean read FTrim write FTrim default True;
  end;

  TSeriesTextEditor = class(TBaseSourceEditor)
    OpenDialog1: TOpenDialog;
    Button1: TButton;
    Button2: TButton;
    PageControl1: TPageControl;
    TabFields: TTabSheet;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Edit1: TEdit;
    UpDown1: TUpDown;
    StringGrid1: TStringGrid;
    CBSeries: TComboFlat;
    CBSep: TComboFlat;
    TabSheet2: TTabSheet;
    BBrowse: TSpeedButton;
    RBFile: TRadioButton;
    EFile: TEdit;
    RBWeb: TRadioButton;
    EWeb: TEdit;
    ButtonLoad: TButton;
    PanBot: TPanel;
    TabText: TTabSheet;
    MemoText: TMemo;
    RBManual: TRadioButton;
    BEditText: TButton;
    CBAutoFields: TCheckBox;
    RBComponent: TRadioButton;
    CBComponent: TComboFlat;
    procedure FormShow(Sender: TObject);
    procedure CBSeriesChange(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure ButtonLoadClick(Sender: TObject);
    procedure RBFileClick(Sender: TObject);
    procedure SpeedButton1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure BApplyClick(Sender: TObject);
    procedure EFileChange(Sender: TObject);
    procedure EWebChange(Sender: TObject);
    procedure CBSepChange(Sender: TObject);
    procedure Edit1Change(Sender: TObject);
    procedure StringGrid1SetEditText(Sender: TObject; ACol, ARow: Integer;
      const Value: String);
    procedure MemoTextChange(Sender: TObject);
    procedure RBManualClick(Sender: TObject);
    procedure PageControl1Change(Sender: TObject);
    procedure BEditTextClick(Sender: TObject);
    procedure CBAutoFieldsClick(Sender: TObject);
  private
    { Private declarations }

    procedure FillComponents;
    procedure HideSeriesCombo;
    Function SelectedSeries:TChartSeries;
    Procedure SetOptions;
  public
    { Public declarations }
    procedure RefreshSource(const ASource:TSeriesTextSource);
  end;

function TeeEditSeriesTextSource(const ASource:TSeriesTextSource):Boolean;

implementation

{$IFNDEF LCL}
{$R *.DFM}
{$ELSE}
{$R *.lfm}
{$ENDIF}

Uses
  TypInfo, Math,
  TeeConst, TeePenDlg, TeeProCo, Chart;

{$IFDEF D15}
{$DEFINE TEEFORMAT}
{$ENDIF}

{$IFDEF LCL}
{$DEFINE TEEFORMAT}
{$ENDIF}

{ create a Form to edit the Series Text source }
function TeeEditSeriesTextSource(const ASource:TSeriesTextSource):Boolean;
var tmp : TSeriesTextEditor;
begin
  tmp:=TSeriesTextEditor(TeeCreateForm(TSeriesTextEditor,nil));
  with tmp do
  try
    TeeTranslateControl(tmp);
    Tag:=ObjectToTag(ASource);
    result:=ShowModal=mrOk;
  finally
    Free;
  end
end;

procedure TSeriesTextEditor.HideSeriesCombo;
begin
  { hide series combobox }
  CBSeries.Visible:=False;
  Label3.Visible:=False;
  PageControl1.Align:=alClient;
end;

procedure TSeriesTextEditor.FormShow(Sender: TObject);
begin
  inherited;

  StringGrid1.ColWidths[1]:=50;

  CheckDataSource(TSeriesTextSource);

  { find Text Source object }
  if TObject(Tag) is TSeriesTextSource then
  begin
    Pan.Visible:=False;
    BEditText.Parent:=PanBot;
  end
  else
  if TObject(Tag) is TChartSeries then
     HideSeriesCombo;

  RefreshSource(TSeriesTextSource(IDataSource));

  BApply.Enabled:=Assigned(TheSeries) and (TheSeries.DataSource<>IDataSource);

  PanBot.Visible:=not Assigned(Parent);
end;

procedure TSeriesTextEditor.RefreshSource(const ASource:TSeriesTextSource);
var t : Integer;
    tmp : TChartSeries;
begin
  IDataSource:=ASource;

  { set form controls }
  with TSeriesTextSource(IDataSource) do
  begin
    UpDown1.Position:=HeaderLines;

    CBAutoFields.Checked:=Automatic;

    if FieldSeparator=',' then CBSep.ItemIndex:=0
    else
    if FieldSeparator=' ' then CBSep.ItemIndex:=1
    else
    if FieldSeparator=TeeTabDelimiter then CBSep.ItemIndex:=2
    else
    begin
      CBSep.ItemIndex:=-1;
      CBSep.Text:=FieldSeparator;
    end;

    if Assigned(Owner) then
    begin
      if CBSeries.Visible then
      begin
        With Owner do
        for t:=0 to ComponentCount-1 do
        if Components[t] is TChartSeries then
        begin
          tmp:=TChartSeries(Components[t]);
          CBSeries.Items.AddObject(SeriesTitleOrName(tmp),tmp);
        end;

        if Assigned(Series) and (CBSeries.Items.IndexOfObject(Series)=-1) then
           CBSeries.Items.AddObject(SeriesTitleOrName(Series),Series);

        CBSeries.ItemIndex:=CBSeries.Items.IndexOfObject(Series);
      end
      else
      begin
        CBSeries.Items.AddObject(SeriesTitleOrName(TheSeries),TheSeries);
        CBSeries.ItemIndex:=CBSeries.Items.IndexOfObject(TheSeries);
      end;

      if CBSeries.ItemIndex<>-1 then
         CBSeriesChange(Self)
      else
      begin
        StringGrid1.Enabled:=False;
        ButtonLoad.Enabled:=False;
      end;
    end;

    { set text filename }
    if FileName='' then
       if Assigned(DataSource) then
       begin
         RBComponent.Checked:=True;
         FillComponents;
         CBComponent.ItemIndex:=CBComponent.Items.IndexOfObject(DataSource);
       end
       else
          RBManual.Checked:=True
    else
      if Copy(Uppercase(FileName),1,7)='HTTP://' then
      begin
        EWeb.Text:=FileName;
        RBWeb.Checked:=True;
        RBFileClick(RBWeb);
      end
      else
      begin
        EFile.Text:=FileName;
        RBFile.Checked:=True;
        RBFileClick(RBFile);
      end;

    MemoText.Lines:=Text;
  end;
end;

procedure TSeriesTextEditor.CBSeriesChange(Sender: TObject);

  { return the "field index" corresponding to AName value list }
  Function GetFieldIndexSt(Const AName:String):String;
  var t : Integer;
      tmpS: ShortString;
  begin
    result:='';

    with TSeriesTextSource(IDataSource) do
    for t:=0 to Fields.Count-1 do
    if Uppercase(Fields[t].FieldName)=Uppercase(AName) then
    begin
      Str(Fields[t].FieldIndex,tmpS);
      result:=String(tmpS);
      Exit;
    end;
  end;

var t   : Integer;
    tmp : TChartSeries;
begin
  if CBSeries.ItemIndex<>-1 then
  begin
    StringGrid1.Enabled:=True;
    ButtonLoad.Enabled:=True;

    tmp:=SelectedSeries;

    if Assigned(tmp) then
    begin
      StringGrid1.RowCount:=2+tmp.ValuesList.Count;
      StringGrid1.Cells[1,0]:=TeeMsg_Column;
      StringGrid1.Cells[0,1]:=TeeMsg_Text;
      StringGrid1.Cells[1,1]:=GetFieldIndexSt(TeeMsg_Text);

      With tmp do
      for t:=0 to ValuesList.Count-1 do
      begin
        StringGrid1.Cells[0,2+t]:=ValuesList[t].Name;
        StringGrid1.Cells[1,2+t]:=GetFieldIndexSt(ValuesList[t].Name);
      end;
    end;
  end;
end;

Procedure TSeriesTextEditor.SetOptions;
var t: Integer;
begin
  with TSeriesTextSource(IDataSource) do
  begin
    if RBManual.Checked or RBComponent.Checked then
    begin
      FileName:='';

      if RBManual.Checked then
      begin
        DataSource:=nil;
        Text:=MemoText.Lines;
      end
      else
         DataSource:=TComponent(CBComponent.Items.Objects[CBComponent.ItemIndex]);
    end
    else
    if RBWeb.Checked then FileName:=EWeb.Text
                     else FileName:=EFile.Text;

    HeaderLines:=UpDown1.Position;

    Case CBSep.ItemIndex of
      0: FieldSeparator:=',';
      1: FieldSeparator:=' ';
      2: FieldSeparator:=TeeTabDelimiter;
    else
         FieldSeparator:=CBSep.Text;
    end;

    Fields.Clear;

    if CBSeries.ItemIndex<>-1 then
    begin
      Series:=SelectedSeries;

      for t:=1 to StringGrid1.RowCount-1 do
      begin
        if StringGrid1.Cells[1,t]<>'' then
           AddField(StringGrid1.Cells[0,t],StrToInt(StringGrid1.Cells[1,t]));
      end;
    end;
  end;
end;

Function TSeriesTextEditor.SelectedSeries:TChartSeries;
begin
  result:=TChartSeries(CBSeries.SelectedObject);
end;

procedure TSeriesTextEditor.Button1Click(Sender: TObject);
begin
  if BApply.Enabled then
     BApplyClick(Self);
end;

procedure TSeriesTextEditor.ButtonLoadClick(Sender: TObject);
begin
  Screen.Cursor:=crHourGlass;
  try
    SetOptions;

    IDataSource.Close;
    IDataSource.Open;

    MemoText.Lines:=TSeriesTextSource(IDataSource).Text;
  finally
    Screen.Cursor:=crDefault;
  end;
end;

procedure TSeriesTextEditor.RBFileClick(Sender: TObject);
begin
  EFile.Enabled:=RBFile.Checked;

  if RBFile.Checked then
     RBManual.Checked:=False;

  if (Sender<>RBManual) and (Sender<>RBComponent) then
     if (not RBManual.Checked) and (not RBComponent.Checked) then
        RBWeb.Checked:=not RBFile.Checked;

  EWeb.Enabled:=RBWeb.Checked;
  BBrowse.Enabled:=EFile.Enabled;

  EFileChange(Sender);
end;

procedure TSeriesTextEditor.SpeedButton1Click(Sender: TObject);
begin
  if OpenDialog1.Execute then EFile.Text:=OpenDialog1.FileName;
end;

{ TSeriesTextSource }
Constructor TSeriesTextSource.Create(AOwner: TComponent);
begin
  inherited;
  FAutomatic:=True;
  FFields:=TSeriesTextFields.Create(Self,TSeriesTextField);
  FSeparator:=TeeDefaultFieldSeparator;
  FText:=TStringList.Create;
  FTrim:=True;
end;

Destructor TSeriesTextSource.Destroy;
begin
  FText.Free;
  FFields.Free;
  inherited;
end;

function TSeriesTextSource.AddField(const AName: String; AIndex: Integer):TSeriesTextField;
begin
  result:=FFields.AddField(AIndex,AName);
  FAutomatic:=False;
end;

procedure TSeriesTextSource.SetDataSource(const Value:TComponent);
var tmp : String;
begin
  if FDataSource<>Value then
  begin
    if Assigned(FDataSource) then
       FDataSource.RemoveFreeNotification(Self);

    if Assigned(Value) then
       if not Assigned(FindProperty(Value,TStrings)) then
       begin
         tmp:=Value.Name;
         if tmp='' then
            tmp:=Value.ClassName;

         raise ChartException.CreateFmt(TeeMsg_SeriesInvDataSource,[tmp]);
       end;

    FDataSource:=Value;

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

    if Active then
    begin
      Close;
      Open;
    end;
  end;
end;

procedure TSeriesTextSource.LoadFromStream(AStream: TStream);
begin
  inherited;

  AStream.Position:=0;

  {$IFDEF AXUNICODE}
  tmp:=TWideStringList.Create;
  try
    tmp.LoadFromStream(AStream);
    LoadFromWideStrings(tmp);
  finally
    tmp.Free;
  end;
  {$ELSE}
  Text.LoadFromStream(AStream);
  LoadFromStrings(Text);
  {$ENDIF}
end;

procedure TSeriesTextSource.SetFields(const Value: TSeriesTextFields);
begin
  FFields.Assign(Value);
end;

procedure TSeriesTextSource.SetText(const Value: TStrings);
begin
  FText.Assign(Value);
end;

{$IFDEF AXUNICODE}
procedure TSeriesTextSource.LoadFromWideStrings(const AStrings: TWideStringList);
Var St,StLabel        : String;
    t,
    tt, lastPart        : Integer;
    tmpLabel,
    tmpText,tmpLabel2   : String;
    tmpHasNot : Boolean;
    tmpX      : Double;
    tmpValue  : Double;
    tmpValueList : TChartValueList;

    function getLabel(const inStr:WideString) : WideString;
    var i: Integer;
        tmpStr:WideString;
    Begin
      i:=0;
      tmpStr:=inStr;
      while Pos(FSeparator,tmpStr)<>0 do
      Begin
        i:=Pos(FSeparator,tmpStr);
        tmpStr:=Copy(tmpStr,i+1,Length(tmpStr));
      end;
      result:=tmpStr;
    end;

begin
  if Assigned(Series) then
  begin
    { by default, series has no X values }
    tmpHasNot:=False;

    { prepare list of fields }
    for tt:=0 to FFields.Count-1 do
    With FFields[tt] do
    begin
      if Uppercase(FieldName)=Uppercase(TeeMsg_Text) then { text labels }
         Data:=Series.Labels
      else
      begin
        { values }
        if Uppercase(FieldName)=Uppercase(Series.NotMandatoryValueList.Name) then
        begin
          tmpHasNot:=True;
          Data:=Series.NotMandatoryValueList;
        end
        else Data:=Series.GetYValueList(FieldName);
      end;
    end;

    OldSep:=TeeFieldsSeparator;
    TeeFieldsSeparator:=FSeparator;
    try
      { empty series }
      Series.Clear;

      { loop all strings }
      for t:=HeaderLines to AStrings.Count-1 do
      begin
        StLabel:=Unicoding.CodeUTF8String(getLabel(AStrings[t]));
        St:=AStrings[t];

        if St<>'' then
        begin
          { clear temporary values }
          for tt:=0 to Series.ValuesList.Count-1 do
              Series.ValuesList[tt].TempValue:=0;

          tmpLabel:='';

          { set temporary values from loaded text string }
          for tt:=0 to FFields.Count-1 do
          With FFields[tt] do
          begin
            tmpText:=TeeExtractField(St,FieldIndex);

            if Data=Series.Labels then
              tmpLabel:=StLabel //tmpText
            else
            begin
              tmpValueList:=TChartValueList(Data);

              if Assigned(FOnGetValue) then
                 FOnGetValue(FFields[tt],tmpText,tmpValue)
              else
              if tmpValueList.DateTime then tmpValue:=StrToDateTime(tmpText)
                                       else tmpValue:=StrToFloat(tmpText);

              tmpValueList.TempValue:=tmpValue;
            end;
          end;

          { add new point to series }
          With Series do
          begin
            if not tmpHasNot then tmpX:=Count
                             else tmpX:=NotMandatoryValueList.TempValue;

            AddXY(tmpX,MandatoryValueList.TempValue,tmpLabel);
          end;

        end;
      end;

    finally
      TeeFieldsSeparator:=OldSep;
    end;

    { tell changes to other series }
    Series.RefreshSeries;
  end;
end;
{$ENDIF}

{$IFNDEF D6}
function TryStrToDateTime(const S: string; out Value: TDateTime): Boolean;
begin
  result:=False;

  try
    Value:=StrToDateTime(S);
    result:=True;
  except
    on EConvertError do ;  // Skip exception
  end;
end;
{$ENDIF}

class function TSeriesTextSource.GuessNumFields(S:String; out ASeparator:String):Integer;
var NewSep : String;
    tmp    : Integer;
begin
  result:=TeeNumFields(S,' ');
  NewSep:=' ';

  tmp:=TeeNumFields(S,',');
  if tmp>result then
  begin
    result:=tmp;
    NewSep:=',';
  end;

  tmp:=TeeNumFields(S,';');
  if tmp>result then
  begin
    result:=tmp;
    NewSep:=';';
  end;

  tmp:=TeeNumFields(S,TeeTabDelimiter);
  if tmp>result then
  begin
    result:=tmp;
    NewSep:=TeeTabDelimiter;
  end;

  ASeparator:=NewSep;
end;

function TryLocalStrToFloatDouble(const S:String; out Value:Double):Boolean;
begin
  result:=TryStrToFloat(S,Value);

  if not result then
     result:=TryStrToFloat(StringReplace(S, {$IFDEF TEEFORMAT}FormatSettings.{$ENDIF}ThousandSeparator, '',[rfReplaceAll]), Value);
end;

class function TSeriesTextSource.TryLocalStrToFloat(const S:String; out Value:{$IFNDEF D7}Double{$ELSE}Extended{$ENDIF}):Boolean;
begin
  result:=TryStrToFloat(S,Value);

  if not result then
     result:=TryStrToFloat(StringReplace(S, {$IFDEF TEEFORMAT}FormatSettings.{$ENDIF}ThousandSeparator, '',[rfReplaceAll]), Value);
end;

type
  TSeriesAccess=class(TChartSeries);

procedure TSeriesTextSource.GuessFields(const AFields:TSeriesTextFields; const AStrings:TStrings;
                                        out ASeparator:String);

  function AnyFieldIsValue(NumFields:Integer; const s:String):Boolean;
  var t: Integer;
      tmpValue : {$IFNDEF D7}Double{$ELSE}Extended{$ENDIF};
  begin
    result:=False;

    for t:=1 to NumFields do
    if TryLocalStrToFloat(TeeExtractField(S,t,ASeparator),tmpValue) then
    begin
      result:=True;
      break;
    end;
  end;

  function AllFieldsAreValues(NumFields:Integer; const s:String):Boolean;
  var t: Integer;
      tmpValue : {$IFNDEF D7}Double{$ELSE}Extended{$ENDIF};
  begin
    result:=True;

    for t:=1 to NumFields do
    if not TryLocalStrToFloat(TeeExtractField(S,t,ASeparator),tmpValue) then
    begin
      result:=False;
      break;
    end;
  end;

  procedure GuessHeaderLines(NumFields:Integer);
  var t : Integer;
      tmpAny : Boolean;
  begin
    HeaderLines:=0;

    if AStrings.Count>0 then
    begin
      tmpAny:=AnyFieldIsValue(NumFields, AStrings[0]);

      if not tmpAny then
      begin
        Inc(FHeader);

        for t:=1 to Min(10,AStrings.Count-1) do
        begin
          tmpAny:=AnyFieldIsValue(NumFields, AStrings[0]);
          if tmpAny then
             break;
        end;
      end
      else
      begin
        tmpAny:=AllFieldsAreValues(NumFields, AStrings[0]);
        if tmpAny then
           Inc(FHeader);
      end;
    end;
  end;

var t,
    tmpIndex,
    tmpNum : Integer;
    tmpHeader : String;
    tmp    : String;
    tmpField : String;
    tmpValue : {$IFNDEF D7}Double{$ELSE}Extended{$ENDIF};
    tmpHasText : Boolean;
    tmpHasValue : Boolean;
    tmpSeries   : TChartSeries;
begin
  if AStrings.Count>0 then
  begin
    tmpNum:=GuessNumFields(AStrings[0], ASeparator);
    GuessHeaderLines(tmpNum);
  end
  else
    HeaderLines:=0;

  AFields.Clear;

  if AStrings.Count>HeaderLines then
  begin
    tmp:=AStrings[HeaderLines];

    if HeaderLines=0 then
       tmpHeader:=''
    else
       tmpHeader:=AStrings[0];

    tmpNum:=GuessNumFields(tmp, ASeparator);

    tmpHasText:=False;
    tmpHasValue:=False;
    tmpSeries:=Series;

    if AllFieldsAreValues(tmpNum,tmp) and
        ( (not Assigned(Series)) or (Series.ValuesList.Count=tmpNum) ) then
    begin
      // All fields are numeric values

      for t:=1 to tmpNum do
          if Assigned(Series) then
             AFields.AddField(t,Series.ValuesList[t-1].Name).FFieldType:=tftNumber
          else
             AFields.AddField(t,TeeExtractField(tmpHeader,t,ASeparator)).FFieldType:=tftNumber;
    end
    else
    begin
      // Normal Guess

      for t:=1 to tmpNum do
      begin
        tmpField:=TeeExtractField(tmp,t,ASeparator);

        if TryLocalStrToFloat(tmpField,tmpValue) then
        begin
          if not tmpHasValue then
          begin
            if Assigned(Series) then
               AFields.AddField(t,Series.MandatoryValueList.Name).FFieldType:=tftNumber
            else
               AFields.AddField(t,TeeExtractField(tmpHeader,t,ASeparator)).FFieldType:=tftNumber;

            tmpHasValue:=True;
          end
          else
          if CanCreateSeries then
          begin
            if Assigned(tmpSeries) then
            begin
              tmpIndex:=tmpSeries.SeriesIndex;

              if tmpIndex=Series.ParentChart.SeriesCount-1 then
              begin
                tmpSeries:=CloneChartSeries(Series);

                tmpSeries.Name:=GetNewSeriesName(Series.Owner);
                tmpSeries.Title:=TeeExtractField(tmpHeader,t,ASeparator);

                TSeriesAccess(tmpSeries).DontSerialize:=True;
                tmpSeries.Tag:=ObjectToTag(Self);
              end
              else
                 tmpSeries:=Series.ParentChart.SeriesList[tmpIndex+1];

              AFields.AddField(t,tmpSeries.MandatoryValueList.Name,tmpSeries).FFieldType:=tftNumber;
            end
            else
              AFields.AddField(t,TeeExtractField(tmpHeader,t,ASeparator)).FFieldType:=tftNumber;

          end
          else
            AFields.AddField(t,TeeExtractField(tmpHeader,t,ASeparator)).FFieldType:=tftNumber;

        end
        else
        if tmpHasText then
        begin
          if not Assigned(Series) then
             AFields.AddField(t,TeeMsg_Text).FFieldType:=tftText;
        end
        else
        begin
          AFields.AddField(t,TeeMsg_Text).FFieldType:=tftText;
          tmpHasText:=True;
        end;
      end;
    end;
  end;
end;

function RemoveDuplicateSpaces(const S:String):String;
var i : Integer;
begin
  result:=Trim(S);

  i:=1;

  while i<Length(result) do
  begin
    if Copy(result,i,1)=' ' then
       while Copy(result,i+1,1)=' ' do
             Delete(result,i+1,1);

    Inc(i);
  end;
end;

procedure TSeriesTextSource.LoadFromStrings(const AStrings: TStrings);
var
  tmpHasNot : Boolean;

  procedure PrepareFields(const AFields:TSeriesTextFields);

    procedure PrepareField(const AField:TSeriesTextField; const ASeries:TChartSeries);
    begin
      with AField do
      if Uppercase(FieldName)=Uppercase(TeeMsg_Text) then { text labels }
         Data:=ASeries.Labels
      else
      begin
        { values }
        if Uppercase(FieldName)=Uppercase(ASeries.NotMandatoryValueList.Name) then
        begin
          tmpHasNot:=True;
          Data:=ASeries.NotMandatoryValueList;
        end
        else
          Data:=ASeries.GetYValueList(FieldName);
      end;
    end;

  var t : Integer;
  begin
    { prepare list of fields }
    for t:=0 to AFields.Count-1 do
    with AFields[t] do
      if Assigned(ISeries) then
         PrepareField(AFields[t],ISeries)
      else
         PrepareField(AFields[t],Series);
  end;

  procedure LoopStrings(const AFields:TSeriesTextFields);

    procedure ClearTempValues(const ASeries:TChartSeries);
    var t : Integer;
    begin
      { clear temporary values }
      for t:=0 to ASeries.ValuesList.Count-1 do
          ASeries.ValuesList[t].TempValue:=0;
    end;

    var
      tmpLabel : String;

    procedure AddPoint(const ASeries:TChartSeries; ANull:Boolean);
    var tmpX : Double;
    begin
      with ASeries do
      begin
        if not tmpHasNot then tmpX:=Count
                         else tmpX:=NotMandatoryValueList.TempValue;

        if YMandatory then
           if ANull then
              AddNullXY(tmpX,MandatoryValueList.TempValue,tmpLabel)
           else
              AddXY(tmpX,MandatoryValueList.TempValue,tmpLabel)
        else
           if ANull then
              AddNullXY(MandatoryValueList.TempValue,tmpX,tmpLabel)
           else
              AddXY(MandatoryValueList.TempValue,tmpX,tmpLabel);
      end;
    end;

  var St        : String;
      t,
      tt        : Integer;
      tmpText   : String;
      tmpNull   : Boolean;
      tmpOk     : Boolean;
      tmpValue  : Double;
      tmpDate   : TDateTime;
      tmpValueList : TChartValueList;
  begin
    { loop all strings }
    for t:=HeaderLines to AStrings.Count-1 do
    begin
      St:=AStrings[t];

      if FTrim then
         St:=RemoveDuplicateSpaces(St);

      if St<>'' then
      begin
        ClearTempValues(Series);

        if Automatic and CanCreateSeries and Assigned(Series.ParentChart) then
        with Series.ParentChart do
        for tt:=0 to SeriesList.Count-1 do
          if (SeriesList[tt]<>Self.Series) and (SeriesList[tt].Tag=ObjectToTag(Self)) then
             ClearTempValues(SeriesList[tt]);

        tmpLabel:='';
        tmpNull:=True;

        { set temporary values from loaded text string }
        for tt:=0 to AFields.Count-1 do
        With AFields[tt] do
        begin
          tmpText:=TeeExtractField(St,FieldIndex);

          if (Assigned(ISeries) and (Data=ISeries.Labels)) or
             (Data=Series.Labels) then
               tmpLabel:=tmpText
          else
          begin
            tmpValueList:=TChartValueList(Data);

            if Assigned(FOnGetValue) then
               FOnGetValue(FFields[tt],tmpText,tmpValue)
            else
            begin
              if Automatic then
              begin
                if tmpValueList.DateTime then
                begin
                  tmpOk:=TryStrToDateTime(tmpText,tmpDate);

                  if tmpOk then
                     tmpValue:=tmpDate;
                end
                else
                   tmpOk:=TryStrToFloat(tmpText,tmpValue);

                if tmpOk then
                begin
                  tmpValueList.TempValue:=tmpValue;
                  tmpNull:=False;
                end
                else
                   if tmpLabel='' then
                      tmpLabel:=tmpText
                   else
                      tmpLabel:=tmpLabel+' '+tmpText;
              end
              else
              begin
                if tmpText='' then
                   tmpNull:=True
                else
                begin
                  if tmpValueList.DateTime then
                  begin
                    tmpValue:=StrToDateTime(tmpText);
                    tmpNull:=False;
                  end
                  else
                  if TryLocalStrToFloatDouble(tmpText,tmpValue) then
                     tmpNull:=False
                  else
                  begin
                    tmpValue:=0;
                    tmpNull:=True;
                  end;

                  tmpValueList.TempValue:=tmpValue;
                end;
              end;
            end;
          end;
        end;

        { add new point to series }
        AddPoint(Series,tmpNull);

        if Automatic then
        for tt:=0 to AFields.Count-1 do
        With AFields[tt] do
             if Assigned(ISeries) and (ISeries<>Self.Series) then
                AddPoint(ISeries,False);
      end;
    end;
  end;

  procedure ClearSeries(const AFields:TSeriesTextFields);
  var t : Integer;
  begin
    Series.Clear;

    if Automatic and CanCreateSeries and Assigned(Series.ParentChart) then
    with Series.ParentChart do
    for t:=0 to SeriesList.Count-1 do
         if (SeriesList[t]<>Self.Series) and
            (SeriesList[t].Tag=ObjectToTag(Self)) then
               SeriesList[t].Clear;
  end;

  function TrimCount(const AStrings:TStrings):Integer;
  var t : Integer;
  begin
    result:=0;

    for t:=0 to AStrings.Count-1 do
        if Trim(AStrings[t])<>'' then
           Inc(result);
  end;

  function ValidString(const AStrings:TStrings; Index:Integer):String;
  var t : Integer;
  begin
    t:=0;

    while (t<=Index) and (AStrings.Count>t) do
    begin
      result:=Trim(AStrings[t]);
      Inc(t);

      if result<>'' then
         Dec(Index);
    end;
  end;

  function HasOneString:Boolean;
  var S : String;
      n : Integer;
      t : Integer;
      tmpField : String;
      tmpValue : {$IFNDEF D7}Double{$ELSE}Extended{$ENDIF};
  begin
    result:=False;

    if TrimCount(AStrings)=1 then
    begin
      S:=ValidString(AStrings,0);
      n:=GuessNumFields(S, FSeparator);

      if n>1 then
      begin
        result:=True;

        for t:=1 to n do
        begin
          tmpField:=TeeExtractField(S,t,FSeparator);

          if not TryLocalStrToFloat(tmpField,tmpValue) then
          begin
            result:=False;
            break;
          end;
        end;

        if result then
        for t:=1 to n do
        begin
          tmpField:=TeeExtractField(S,t,FSeparator);
          Series.Add(StrToFloat(tmpField));
        end;
      end;
    end
  end;

var tmpFields : TSeriesTextFields;
    OldSep    : String;
    tmpSeparator : String;
begin
  if Assigned(Series) then
  begin
    OldSep:=TeeFieldsSeparator;
    try

      { by default, series has no X values }
      tmpHasNot:=False;

      if Automatic then //DB: TV52015285
      begin
        tmpFields:=TSeriesTextFields.Create(Self,TSeriesTextField);
        GuessFields(tmpFields,AStrings,tmpSeparator);
        TeeFieldsSeparator:=tmpSeparator;
      end
      else
      begin
        tmpFields:=FFields;
        TeeFieldsSeparator:=FSeparator;
      end;

      PrepareFields(tmpFields);

      { empty series }
      if LoadMode=lmClear then
         ClearSeries(tmpFields);

      if Automatic then
      begin
        if not HasOneString then
            LoopStrings(tmpFields)
      end
      else
        LoopStrings(tmpFields);

      if tmpFields<>FFields then
         tmpFields.Free;

    finally
      TeeFieldsSeparator:=OldSep;
    end;

    { tell changes to other series }
    Series.RefreshSeries;
  end;
end;

class function TSeriesTextSource.Description: String;
begin
  result:=TeeMsg_TextFile;
end;

class function TSeriesTextSource.Editor: TComponentClass;
begin
  result:=TSeriesTextEditor;
end;

class function TSeriesTextSource.FindProperty(const S:TObject; const AClass:TClass):TObject;
var n,
    t  : Integer;
    L  : PPropList;
    o : TObject;
begin
  result:=nil;

  if S.ClassInfo=nil then
     Exit;

  {$IFNDEF D6}
  L:=nil;
  {$ENDIF}

  n:=GetPropList(S{$IFNDEF D6}.ClassInfo,tkProperties{$ENDIF},L);

  if n>0 then
  try
    {$IFDEF D6}
    {$IFNDEF LCL}
    SortPropList(L,n);
    {$ENDIF}
    {$ENDIF}

    for t:=0 to n-1 do
    begin
      if L[t].PropType^.Kind=tkClass then
      begin
        o:=GetObjectProp(S,L[t]);

        if Assigned(o) and (o is AClass) then
        begin
          result:=o;
          exit;
        end;
      end;
    end;

  finally
    FreeMemory(L);
  end
  else
  if S is TCollection then
  begin
    with TCollection(S) do
    for t:=0 to Count-1 do
    begin
      o:=FindProperty(S,AClass);

      if Assigned(o) then
      begin
        result:=o;
        exit;
      end;
    end;
  end;
end;

procedure TSeriesTextSource.Load;
var tmp : TObject;
begin
  if Assigned(Series) then
     if FileName<>'' then
        inherited
     else
     begin
       if Assigned(FDataSource) then
       begin
         tmp:=FindProperty(DataSource,TStrings);

         if Assigned(tmp) then
            Text:=TStrings(tmp);
       end;

       LoadFromStrings(Text);
     end;
end;

procedure TSeriesTextSource.Loaded;
begin
  inherited;

  if Fields.Count>0 then
     FAutomatic:=False;
end;

procedure TSeriesTextSource.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited;

  if (Operation=opRemove) and (AComponent=DataSource) then
     DataSource:=nil
end;

procedure TSeriesTextSource.SetAutomatic(const Value: Boolean);
begin
  if FAutomatic<>Value then
  begin
    FAutomatic:=Value;

    if Active then
    begin
      Close;
      Open;
    end;
  end;
end;

{ TSeriesTextField }
procedure TSeriesTextField.SetFieldIndex(const Value: Integer);
begin
  if Value>0 then FFieldIndex:=Value
             else Raise Exception.Create(TeeMsg_SeriesTextFieldZero);
end;

{ TSeriesTextFields }

function TSeriesTextFields.AddField(ANum: Integer; const AName: String;
                                    ASeries:TChartSeries=nil):TSeriesTextField;
begin
  result:=Add as TSeriesTextField;

  with result do
  begin
    FFieldIndex:=ANum;
    FFieldName:=AName;
    ISeries:=ASeries;
  end;
end;

function TSeriesTextFields.Get(Index: Integer): TSeriesTextField;
begin
  result:=TSeriesTextField(inherited Items[Index]);
end;

procedure TSeriesTextFields.Put(Index: Integer; const Value: TSeriesTextField);
begin
  Items[Index].Assign(Value);
  TSeriesTextSource(GetOwner).FAutomatic:=False;
end;

procedure TSeriesTextEditor.FormCreate(Sender: TObject);
begin
  inherited;

  Align:=alClient;
  BorderStyle:=TeeBorderStyle;
  LLabel.Visible:=False;
  CBSources.Visible:=False;
  BBrowse.Flat:=True;

  PageControl1.ActivePage:=TabFields;
end;

procedure TSeriesTextEditor.BApplyClick(Sender: TObject);
begin
  inherited;
  CheckReplaceSource(IDataSource);
  SetOptions;
  BApply.Enabled:=False;

  IDataSource.Close;
  IDataSource.Open;
end;

procedure TSeriesTextEditor.EFileChange(Sender: TObject);
begin
  inherited;
  if Pan.Visible then BApply.Enabled:=True;
end;

procedure TSeriesTextEditor.EWebChange(Sender: TObject);
begin
  inherited;
  EFileChange(Sender);
end;

procedure TSeriesTextEditor.CBSepChange(Sender: TObject);
begin
  inherited;
  EFileChange(Sender);
end;

procedure TSeriesTextEditor.Edit1Change(Sender: TObject);
begin
  inherited;
  EFileChange(Sender);
end;

procedure TSeriesTextEditor.StringGrid1SetEditText(Sender: TObject; ACol,
  ARow: Integer; const Value: String);
begin
  TSeriesTextSource(IDataSource).FAutomatic:=False;
  CBAutoFields.Checked:=False;
  EFileChange(Sender);
end;

procedure TSeriesTextEditor.MemoTextChange(Sender: TObject);
begin
  BApply.Enabled:=True;
end;

procedure TSeriesTextEditor.RBManualClick(Sender: TObject);
begin
  RBFile.Checked:=False;
  RBWeb.Checked:=False;
  RBComponent.Checked:=Sender=RBComponent;
  RBManual.Checked:=Sender=RBManual;

  RBFileClick(Sender);

  if RBComponent.Checked then
     if CBComponent.Items.Count=0 then
        FillComponents;
end;

procedure TSeriesTextEditor.FillComponents;

  procedure FillOwner(AOwner:TComponent);
  var t : Integer;
  begin
    if Assigned(AOwner) then 
    with AOwner do
    for t:=0 to ComponentCount-1 do
    if (Components[t]<>IDataSource) and
       (TSeriesTextSource(IDataSource).FindProperty(Components[t],TStrings)<>nil) then
          CBComponent.Items.AddObject(Components[t].Name,Components[t]);
  end;

begin
  CBComponent.Items.Clear;

  if (SelectedSeries<>nil) and Assigned(SelectedSeries.Owner) then
     FillOwner(SelectedSeries.Owner)
  else
  if Assigned(IDataSource.Owner) then
     FillOwner(IDataSource.Owner)
  else
     FillOwner(Owner);

  CBComponent.Sorted:=True;
  CBComponent.Enabled:=CBComponent.Items.Count>0;
end;

procedure TSeriesTextEditor.PageControl1Change(Sender: TObject);
begin
  inherited;

  MemoText.ReadOnly:=RBComponent.Checked;
  BEditText.Visible:=(PageControl1.ActivePage=TabText) and (not MemoText.ReadOnly);
end;

procedure TSeriesTextEditor.BEditTextClick(Sender: TObject);
begin
  TeeEditStrings(Self,MemoText.Lines);
end;

procedure TSeriesTextEditor.CBAutoFieldsClick(Sender: TObject);
begin
  TSeriesTextSource(IDataSource).Automatic:=CBAutoFields.Checked;
end;

initialization
  TeeActivateGroup;

  RegisterClass(TSeriesTextSource);
  {$IFNDEF TEEOCX}
  TeeSources.Add(TSeriesTextSource);
  {$ENDIF}
finalization
  TeeActivateGroup;

  {$IFNDEF TEEOCX}
  TeeSources.Remove(TSeriesTextSource);
  {$ENDIF}
  UnRegisterClass(TSeriesTextSource);
end.
