unit ResultsUnit;
// --------------------------------------------
// PICViewer - Measurement results display form
// --------------------------------------------
// 11.3.04
// 11.5.04 SaveTableToFile added
// 27.7.04 FNumRecords cannot exceed table limit
// 05.10.04 ReadFromFile added
// 23.10.04 Object tracks now saved in own results form

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, Grids, StdCtrls, maths, ExtCtrls, ClipBrd, StrUtils ;

const
    cMaxVariables = 100 ;

type
  TTable = Array[0..1000000] of Single ;
  PTable = ^TTable ;
  TUseRecord = Array[0..1000000] of ByteBool ;
  PUseRecord = ^TUseRecord ;

  TResultsFrm = class(TForm)
    sgTable: TStringGrid;
    VariablesGrp: TGroupBox;
    ckVar0: TCheckBox;
    ckVar1: TCheckBox;
    ckVar2: TCheckBox;
    ckVar3: TCheckBox;
    ckVar4: TCheckBox;
    ckVar5: TCheckBox;
    ckVar6: TCheckBox;
    ckVar7: TCheckBox;
    ckVar8: TCheckBox;
    ckVar9: TCheckBox;
    ckVar10: TCheckBox;
    ckVar11: TCheckBox;
    ckVar12: TCheckBox;
    ckVar13: TCheckBox;
    ckVar14: TCheckBox;
    ckVar15: TCheckBox;
    TrackingGrp: TGroupBox;
    rbObjectTrackingOff: TRadioButton;
    rbObjectTrackingOn: TRadioButton;
    bClearTrack: TButton;
    Timer: TTimer;
    SaveDialog: TSaveDialog;
    bSaveTrack: TButton;
    procedure FormShow(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormResize(Sender: TObject);
    procedure ckVar0Click(Sender: TObject);
    procedure bClearTrackClick(Sender: TObject);
    procedure TimerTimer(Sender: TObject);
    procedure rbObjectTrackingOffClick(Sender: TObject);
    procedure bSaveTrackClick(Sender: TObject);
    procedure rbObjectTrackingOnClick(Sender: TObject);
  private
    { Private declarations }
    VarNames : Array[0..cMaxVariables-1] of String ;   // Variable names
    FNumVariables : Integer ;                          // No. variables in use
    FNumRecords : Integer ;                            // No. of records in table
    FMaxRecords : Integer ;                            // Max. no. of records
    FRecordNum : Integer ;                             // Record # selected for update
    Table : PTable ;                                   // Data table pointer
    UseRecord : PUseRecord ;
    OldDblClickPixelIntensity : Integer ;
    OldDblClickStackNum : Integer ;

    procedure SetMaxRecords( Value : Integer ) ;
    procedure SetRecordNum( Value : Integer ) ;
    procedure SetVarCheckBox( CheckBox : TCheckBox ; Name : String ) ;

    function UseVariable( VarNum : Integer ) : Boolean ;
    procedure AllocateTable ;
    procedure SetTrackingControls ;


  public
    { Public declarations }
    FileName : String ;     // Results file name
    Index : Integer ;       // Form index number (into ResultsFrmsInUse)
    ViewFrmNum : Integer ;  // View form # (source of data)
    TrackFrmNum : Integer ;

    procedure ClearVariables ;
    function AddVariable( Name : String ) : Integer ;
    procedure SetVariable( iVar : Integer ; Value : Single ) ;
    function GetVariable( iVar : Integer ) : Single ;
    procedure ShowTable ;
    procedure CopyDataToClipboard ;
    procedure SaveTableToFile ;
    procedure ReadFromFile( NewFileName : String ) ;
    procedure WriteToFile( FileName : String ) ;
    procedure WriteObjectTrack ;
    function FindVariable( VarTitle : String ) : Integer ;

    property NumVariables : Integer read FNumVariables ;
    property MaxRecords : Integer read FMaxRecords Write SetMaxRecords ;
    property RecordNum : Integer read FRecordNum Write SetRecordNum ;
    property NumRecords : Integer read FNumRecords ;
  end;

var
  ResultsFrm: TResultsFrm;

implementation

uses PicViewMain;

{$R *.dfm}


procedure TResultsFrm.FormShow(Sender: TObject);
// --------------------------------------
// Initialisations when form is displayed
// --------------------------------------
begin

     FNumVariables := 0 ;
     FNumRecords := 0 ;
     FMaxRecords := 1000 ;
     FRecordNum := 1 ;
     OldDblClickStackNum := -1 ;

     // Allocate default table
     Table:= Nil ;
     UseRecord := Nil ;
     AllocateTable ;

     // Remove variables
     ClearVariables ;

     ClientHeight := TrackingGrp.Top + TrackingGrp.Height + 10 ;

     Resize ;

     SetTrackingControls ;

     end;

procedure TResultsFrm.ClearVariables ;
// -----------------------------------
// Erase defined measurement variables
// -----------------------------------
begin

    FNumVariables := 0 ;

    // Remove check boxes
    ckVar0.Visible := False ;
    ckVar1.Visible := False ;
    ckVar2.Visible := False ;
    ckVar3.Visible := False ;
    ckVar4.Visible := False ;
    ckVar5.Visible := False ;
    ckVar6.Visible := False ;
    ckVar7.Visible := False ;
    ckVar8.Visible := False ;
    ckVar9.Visible := False ;
    ckVar10.Visible := False ;
    ckVar11.Visible := False ;
    ckVar12.Visible := False ;
    ckVar13.Visible := False ;
    ckVar14.Visible := False ;
    ckVar15.Visible := False ;

    end ;


function TResultsFrm.AddVariable( Name : String ) : Integer ;
// -----------------------------------
// Add a new variable to results table
// -----------------------------------
begin
     if FNumVariables < cMaxVariables then begin
        VarNames[FNumVariables] := Name ;
        Result := FNumVariables ;
        Inc(FNumVariables) ;
        Case (FNumVariables-1) of
              0 : SetVarCheckBox( ckVar0, Name ) ;
              1 : SetVarCheckBox( ckVar1, Name ) ;
              2 : SetVarCheckBox( ckVar2, Name ) ;
              3 : SetVarCheckBox( ckVar3, Name ) ;
              4 : SetVarCheckBox( ckVar4, Name ) ;
              5 : SetVarCheckBox( ckVar5, Name ) ;
              6 : SetVarCheckBox( ckVar6, Name ) ;
              7 : SetVarCheckBox( ckVar7, Name ) ;
              8 : SetVarCheckBox( ckVar8, Name ) ;
              9 : SetVarCheckBox( ckVar9, Name ) ;
              10 : SetVarCheckBox( ckVar10, Name ) ;
              11 : SetVarCheckBox( ckVar11, Name ) ;
              12 : SetVarCheckBox( ckVar12, Name ) ;
              13 : SetVarCheckBox( ckVar13, Name ) ;
              14 : SetVarCheckBox( ckVar14, Name ) ;
              15 : SetVarCheckBox( ckVar15, Name ) ;
              end ;
        AllocateTable ;
        end
     else Result := -1 ;
     end ;


procedure TResultsFrm.SetVariable(
          iVar : Integer ;
          Value : Single ) ;
// -------------------------------
// Set variable value within table
// -------------------------------
begin
     if (iVar >= 0) and (iVar < FNumVariables) then
         Table[FRecordNum*FNumVariables + iVar] := Value ;
     end ;


function TResultsFrm.GetVariable(
         iVar : Integer
          ) : Single ;
// -------------------------------
// Get variable value within table
// -------------------------------
begin
     if (iVar >= 0) and (iVar < FNumVariables) then
         Result := Table[FRecordNum*FNumVariables + iVar]
      else Result := 0.0 ;
     end ;



procedure TResultsFrm.FormResize(Sender: TObject);
// ------------------------
// Re-size controls on form
// ------------------------
begin
     sgTable.Width := ClientWidth - sgTable.Left - 5 ;
     sgTable.Height := ClientHeight - sgTable.Top - 5 ;
     end;


procedure TResultsFrm.SetMaxRecords( Value : Integer ) ;
// -------------------------------------------
// Set maximum no. of records allowed in table
// -------------------------------------------
begin
      if Table <> Nil then begin
         FMaxRecords := Value ;
         AllocateTable ;
         end ;
      end ;

procedure TResultsFrm.SetRecordNum( Value : Integer ) ;
// ----------------------------
// Set data table record number
// ----------------------------
begin
      FRecordNum := Value ;
      FNumRecords := MaxInt([FNumRecords,FRecordNum]) ;
      FNumRecords := MinInt( [FNumRecords,FMaxRecords] ) ;
      sgTable.RowCount := FNumRecords + 1 ;
      end ;



procedure TResultsFrm.AllocateTable ;
// --------------------------------------
// Allocate space for variable data table
// --------------------------------------
var
    i : Integer ;
begin

     if Table <> Nil then FreeMem( Table ) ;
     GetMem( Table, (FMaxRecords+1)*MaxInt([FNumVariables,1])*4 ) ;

     if UseRecord <> Nil then FreeMem( UseRecord ) ;
     GetMem( UseRecord, (FMaxRecords+1)*SizeOf(ByteBool) ) ;
     for i := 1 to FMaxRecords do UseRecord[i] := True ;

     end ;


procedure TResultsFrm.SetVarCheckBox(
          CheckBox : TCheckBox ;      // Check box to be updated
          Name : String               // Check box caption
          ) ;
begin
    CheckBox.Caption := Name ;
    CheckBox.Visible := True ;
    CheckBox.Checked := True ;
    end ;


function TResultsFrm.UseVariable(
          VarNum : Integer
          ) : Boolean ;
// ---------------------------------------------------------
// Return TRUE if variable available and checked for display
// ---------------------------------------------------------
begin
    case VarNum of
        0 : Result := ckVar0.Checked and ckVar0.Visible ;
        1 : Result := ckVar1.Checked and ckVar1.Visible ;
        2 : Result := ckVar2.Checked and ckVar2.Visible ;
        3 : Result := ckVar3.Checked and ckVar3.Visible ;
        4 : Result := ckVar4.Checked and ckVar4.Visible ;
        5 : Result := ckVar5.Checked and ckVar5.Visible ;
        6 : Result := ckVar6.Checked and ckVar6.Visible ;
        7 : Result := ckVar7.Checked and ckVar7.Visible ;
        8 : Result := ckVar8.Checked and ckVar8.Visible ;
        9 : Result := ckVar9.Checked and ckVar9.Visible ;
        10 : Result := ckVar10.Checked and ckVar10.Visible ;
        11 : Result := ckVar11.Checked and ckVar11.Visible ;
        12 : Result := ckVar12.Checked and ckVar12.Visible ;
        13 : Result := ckVar13.Checked and ckVar13.Visible ;
        14 : Result := ckVar14.Checked and ckVar14.Visible ;
        15 : Result := ckVar15.Checked and ckVar15.Visible ;
        else Result := False ;
        end ;
    end ;


procedure TResultsFrm.ShowTable ;
// ------------------------------------------
// Display selected results variable in table
// ------------------------------------------
const
    TitleRow = 0 ;
var
    iRec : Integer ;          // Record counter
    iVar : Integer ;          // Variable counter
    Row : Integer ;           // Row counter
    Column : Integer ;        // Column counter
    NumColumns : Integer ;    // No. columns
    i : Integer ;
begin

      // Define columns
      sgTable.ColCount := 1 ;
      NumColumns := 0 ;
      for iVar := 0 to FNumVariables-1 do if UseVariable( iVar ) then begin
          Inc(NumColumns) ;
          sgTable.ColCount := NumColumns ;
          sgTable.Cells[sgTable.ColCount-1,TitleRow] := VarNames[iVar] ;
          end ;
      // Define rows
      sgTable.RowCount := FNumRecords + 1 ;

      // Fill table

      Row := 1 ;
      for iRec := 1 to FNumRecords do if UseRecord[iRec] or rbObjectTrackingOff.Checked then begin
          Column := 0 ;
          for iVar := 0 to FNumVariables-1 do if UseVariable( iVar ) then begin
              i := iRec*FNumVariables + iVar ;
              sgTable.Cells[Column,Row] := format( '%.6g', [Table^[i]] )  ;
              Inc(Column) ;
              end ;
          Inc(Row) ;
          end ;

      sgTable.RowCount := Row ;
      end ;


procedure TResultsFrm.FormClose(Sender: TObject; var Action: TCloseAction);
// ---------------------------
// Tidy up when form is closed
// ---------------------------
begin

     Action := caFree ;
     if Table <> Nil then FreeMem(Table) ;
     if UseRecord <> Nil then FreeMem(UseRecord) ;

     end;



procedure TResultsFrm.ckVar0Click(Sender: TObject);
// ------------------------------
// Use variable check box changed
// ------------------------------
begin
     // Re-display table
     ShowTable ;
     end;

procedure TResultsFrm.bClearTrackClick(Sender: TObject);
// ---------------------------
// Clear record selection list
// ---------------------------
var
    i : Integer ;
begin
     for i := 1 to FNumRecords do UseRecord[i] := False ;

     if MainFrm.ViewFrmsInUse[ViewFrmNum] then begin
        MainFrm.ViewFrms[ViewFrmNum].DblClickStackNum := 0 ;
        end ;
     ShowTable ;
     end;


procedure TResultsFrm.TimerTimer(Sender: TObject);
var
    iRec,i : Integer ;
    ObjectID, ObjectIDVar : Integer ;
    StackNum, StackNumVar : Integer ;
begin

     if not MainFrm.ViewFrmsInUse[ViewFrmNum] then Exit ;
     if MainFrm.ViewFrms[ViewFrmNum].DblClickStackNum <= 0 then Exit ;

     ObjectIDVar := -1 ;
     for i := 0 to FNumVariables-1 do if VarNames[i] = 'Tracking ID' then ObjectIDVar := i ;
     StackNumVar := -1 ;
     for i := 0 to FNumVariables-1 do if VarNames[i] = 'Timepoint' then StackNumVar := i ;
     if (ObjectIDVar < 0) or (StackNumVar <0) then Exit ;

     if (MainFrm.ViewFrms[ViewFrmNum].DblClickPixelIntensity <> OldDblClickPixelIntensity) or
        (MainFrm.ViewFrms[ViewFrmNum].DblClickStackNum <> OldDblClickStackNum) then begin

        for iRec := 1 to FNumRecords do begin
            ObjectID := Round(Table[iRec*FNumVariables + ObjectIDVar]) ;
            StackNum := Round(Table[iRec*FNumVariables + StackNumVar]) ;
            if (ObjectID = MainFrm.ViewFrms[ViewFrmNum].DblClickPixelIntensity) and
               (StackNum = MainFrm.ViewFrms[ViewFrmNum].DblClickStackNum) then
               UseRecord[iRec] := True ;
            end ;
        OldDblClickPixelIntensity := MainFrm.ViewFrms[ViewFrmNum].DblClickPixelIntensity ;
        OldDblClickStackNum := MainFrm.ViewFrms[ViewFrmNum].DblClickStackNum ;
        ShowTable ;
        end ;
     end;


procedure TResultsFrm.rbObjectTrackingOffClick(Sender: TObject);
// -------------------------------
// Disable object tracking facility
// -------------------------------
var
    i : Integer ;
begin
     SetTrackingControls ;
     //for i := 1 to FNumRecords do UseRecord[i] := True ;
     ShowTable ;
     end;


procedure TResultsFrm.CopyDataToClipboard ;
// -----------------------------------------------
//  Copy the contents of table to clipboard
// -----------------------------------------------
var
   Row,Col,TopRow,BottomRow,LeftCol,RightCol : Integer ;
   TabText : String ;
   First : Boolean ;
begin

     { Set row,column range to be copied }

     if (sgTable.Selection.Top = sgTable.Selection.Bottom) and
        (sgTable.Selection.Left = sgTable.Selection.Right) then begin
        { Whole table if no block selected }
        TopRow := 1 ;
        BottomRow := sgTable.RowCount - 1 ;
        LeftCol := 0 ;
        RightCol := sgTable.ColCount - 1 ;
        end
     else begin
        { Selected block }
        TopRow := sgTable.Selection.Top ;
        BottomRow := sgTable.Selection.Bottom ;
        LeftCol := sgTable.Selection.Left ;
        RightCol := sgTable.Selection.Right ;
        end ;

     { Copy table to tab-text string buffer }
     TabText := '' ;
     for Row := TopRow to BottomRow do begin
         First := True ;
         for Col := LeftCol to RightCol do begin
             if First then begin
                TabText := TabText + sgTable.Cells[Col,Row] ;
                First := False ;
                end
             else TabText := TabText + chr(9) + sgTable.Cells[Col,Row] ;
             end ;
         TabText := TabText + chr(13) + chr(10) ;
         end ;

     { Copy string buffer to clipboard }
     ClipBoard.SetTextBuf( PChar(TabText) ) ;

     end ;


procedure TResultsFrm.SaveTableToFile ;
// --------------------------
// Save results table to file
// --------------------------
begin

{     SaveDialog.options := [ofPathMustExist] ;
     SaveDialog.DefaultExt := '.txt' ;
     if MainFrm.DataDirectory <> '' then SaveDialog.InitialDir := MainFrm.DataDirectory ;

     SaveDialog.Filter :=
     'Text Files (*.txt)|*.txt|' ;
     SaveDialog.Title := 'Save Data Table' ;}

     WriteToFile( FileName ) ;

     end ;


procedure TResultsFrm.WriteToFile(
          FileName : String ) ;
// ---------------------------
// Write results table to file
// ---------------------------
var
     Row,Col : Integer ;
     FOut : TextFile ;
     TextLine : String ;
begin

     if FileExists(FileName) then DeleteFile(PChar(FileName)) ;

     // Open text file for writing
     AssignFile( FOut, FileName ) ;
     ReWrite( FOut ) ;

     // Write results table as <tab> separated text

     for Row := 0 to sgTable.RowCount - 1 do begin
         TextLine := '' ;
         for Col := 0 to sgTable.ColCount - 1 do begin
             if TextLine = '' then begin
                TextLine := sgTable.Cells[Col,Row] ;
                end
             else TextLine := TextLine + #9 + sgTable.Cells[Col,Row] ;
             end ;
         WriteLn( FOut, TextLine ) ;
         end ;

     CloseFile(FOut) ;

     end ;


procedure TResultsFrm.ReadFromFile(
          NewFileName : String ) ;
// -------------------------------
// Read results from file to table
// -------------------------------
var
    F : TextFile ;
    FileHandle : Integer ;
    Line : String ;
    cBuf : Byte ;
    NumRead  : Integer ;
    Row  : Integer ;
    Col  : Integer ;
    i  : Integer ;
    s : String ;
    c : Char ;
    MaxWidth : Integer ;
    NumBytesInFile : Integer ;
    NumBytesRead : Integer ;
    SourceImageFile : String ;
    ObjectIDCol : Integer ;
begin

     // Clear variables list
     ClearVariables ;

     FileName := NewFileName ;
     FileHandle := FileOpen( FileName, fmOpenRead ) ;
     NumBytesInFile := FileSeek( FileHandle, 0, 2 ) ;
     NumBytesRead := FileSeek( FileHandle, 0, 0 ) ;

     // Read table of results into data table

     Line := '' ;
     Row := 0 ;
     Col := 0 ;
     sgTable.ColCount := 1 ;
     sgTable.RowCount := 1 ;
     Repeat
         // Read character from file
         NumRead := FileRead( FileHandle, cBuf, 1 ) ;

         if (cBuf <> 13) and (cBuf <> 10) then begin
            // Add character to line (if not CR or LF)
            Line := Line + chr(cBuf)
            end
         else begin
            // End of Line (CR or LF character) - add line as table row
            if Line <> '' then begin
               // Split up tab-separated data values into table columns
               Col := 0 ;
               for i := 1 to Length(Line) do begin
                   c := Line[i] ;
                   if c <> #9 then s := s + c ;
                   if (c = #9) or (i=Length(Line)) then begin
                      if Col >= sgTable.ColCount then sgTable.ColCount := Col + 1 ;
                      if Row >= sgTable.RowCount then sgTable.RowCount := Row + 1 ;
                      sgTable.Cells[Col,Row] := s ;
                      s := '' ;
                      Inc(Col) ;
                      end ;
                   end ;

               // Get variable names from first row
               if Row = 0 then begin
                  for Col := 0 to sgTable.ColCount-1 do AddVariable( sgTable.Cells[Col,0] ) ;
                  end ;

               Inc(Row) ;
               NumBytesRead := NumBytesRead + Length(Line) + 1 ;
               Line := '' ;
               MainFrm.StatusBar.Simpletext := format(' Loading Measurements Table %.0f%%',
                                       [(100.0*NumBytesRead)/NumBytesInFile]) ;

               end ;
            end ;
         until NumRead <= 0 ;

     // Close file
     FileClose(FileHandle) ;

     // Set data table column widths to accommodate contents
     for Col := 0 to sgTable.ColCount-1 do begin
         MaxWidth := 0 ;
         for Row := 0 to sgTable.RowCount-1 do
             if MaxWidth < Canvas.TextWidth(sgTable.Cells[Col,Row]) then
                MaxWidth := Canvas.TextWidth(sgTable.Cells[Col,Row]) ;
         sgTable.ColWidths[Col] := MaxWidth + Canvas.TextWidth(' ') ;
         end ;

     // Set maximum records in table
     if FMaxRecords <= sgTable.RowCount then SetMaxRecords( sgTable.RowCount*2 ) ;
     FNumRecords := sgTable.RowCount - 1 ;

     // Convert from string to float
     i := 0 ;
     for Row := 1 to sgTable.RowCount-1 do begin
         for Col := 0 to sgTable.ColCount-1 do begin
            Table[Row*FNumVariables+Col] := StrToFloatDef( sgTable.Cells[Col,Row], 0.0 ) ;
            end ;
         end ;

     // Display all records (rows)
     for i := 1 to FNumRecords do UseRecord[i] := True ;

     // Load associated image file

     // Find ViewFrm associated with results
     SourceImageFile := ChangeFileExt( FileName, '.pic' ) ;
     ViewFrmNum := -1 ;
     for i := 0 to High(MainFrm.ViewFrmsInUse) do if MainFrm.ViewFrmsInUse[i] then begin
         if MainFrm.ViewFrms[i].FileName = SourceImageFile then ViewFrmNum := i ;
         end ;

     // If no current ViewFrm, open associated image in new one
     if (ViewFrmNum < 0) and FileExists(SourceImageFile) then begin
        ViewFrmNum := MainFrm.CreateNewViewFrm(SourceImageFile) ;
        end ;

     end ;


procedure TResultsFrm.WriteObjectTrack ;
// -------------------------------------------
// Create a results form to hold object tracks
type
   TFrameBuf = Array[0..4095*4095] of Integer ;
   PFrameBuf = ^TFrameBuf ;
var
    i : Integer ;
    TrackFileName : String ;
    FileName : String ;
    TracksImageFile : String ;
    ObjectIDVar : Integer ;
    StackVar : Integer ;
    XCentroidVar : Integer ;
    YCentroidVar : Integer ;
    ZCentroidVar : Integer ;
    TimeVar : Integer ;
    VelocityVar : Integer ;
    StackNum : Integer ;
    TrackID : Integer ;
    ObjectID : Integer ;
    iRec, PreviousRec : Integer ;
    iVar : Integer ;
    NumTrackRecords : Integer ;
    TrackViewFrmNum : Integer ;
    ObjectsBuf : PFrameBuf ;
    TracksBuf : PFrameBuf ;
    iFrame : Integer ;
    NumPixelsPerFrame : Integer ;
    x0,y0,z0,t0 : single ;
    x1,y1,z1,t1 : single ;
    V : Single ;

begin

     ObjectsBuf := Nil ;
     TracksBuf := Nil ;

     // Create file name
     TrackFileName := ANSIReplaceStr( MainFrm.ViewFrms[ViewFrmNum].FileName,
                                      '[Objects',
                                      '[Tracks' ) ;
     TrackFileName := ChangeFileExt(TrackFileName, '.res' ) ;

     // Find open Results form contain object tracks
     TrackFrmNum := -1 ;
     for i := 0 to High(MainFrm.ResultsFrmsInUse) do if MainFrm.ResultsFrmsInUse[i] then begin
         if MainFrm.ResultsFrms[i].FileName = TrackFileName then TrackFrmNum := i ;
         end ;

     // Create new Results form for track results
     if TrackFrmNum < 0 then begin
        // Create results form
        TrackFrmNum := MainFrm.CreateNewResultsFrm( TrackFileName ) ;
        for iVar := 0 to NumVariables-1 do
            MainFrm.ResultsFrms[TrackFrmNum].AddVariable( VarNames[iVar] ) ;
        // Add velocity column
        VelocityVar := MainFrm.ResultsFrms[TrackFrmNum].AddVariable( 'Velocity (µm/sec)' ) ;
        // Load data
        if FileExists(TrackFileName) then MainFrm.ResultsFrms[TrackFrmNum].ReadFromFile( TrackFileName ) ;
        end ;

     // Find variable within table
     ObjectIDVar := FindVariable( 'Tracking ID' ) ;
     StackVar := FindVariable( 'Timepoint' ) ;
     XCentroidVar := FindVariable( 'Centroid X (µm)') ;
     YCentroidVar := FindVariable( 'Centroid Y (µm)') ;
     ZCentroidVar := FindVariable( 'Centroid Z (µm)') ;
     TimeVar := FindVariable( 'Time (s)' ) ;
     VelocityVar := MainFrm.ResultsFrms[TrackFrmNum].FindVariable( 'Velocity (µm/sec)' ) ;

     // No. of tracks
     NumTrackRecords := MainFrm.ResultsFrms[TrackFrmNum].NumRecords ;

     // Set TrackID (previous TrackID + 1)
     if NumTrackRecords > 0 then begin
        MainFrm.ResultsFrms[TrackFrmNum].RecordNum := NumTrackRecords ;
        TrackID := Round(MainFrm.ResultsFrms[TrackFrmNum].GetVariable( ObjectIDVar )) + 1 ;
        end
     else TrackID := 1 ;

     // Write track points to track results form
     PreviousRec := -1 ;
     for iRec := 1 to FNumRecords do if UseRecord[iRec] then begin
         Inc(NumTrackRecords) ;
         MainFrm.ResultsFrms[TrackFrmNum].RecordNum := NumTrackRecords ;
         // Copy existing variables to table
         i := iRec*NumVariables ;
         for iVar := 0 to NumVariables-1 do begin
             MainFrm.ResultsFrms[TrackFrmNum].SetVariable( iVar, Table[i] ) ;
             Inc(i) ;
             end ;
         // Write track #
         MainFrm.ResultsFrms[TrackFrmNum].SetVariable( ObjectIDVar, TrackID ) ;
         // Write velocity
         V := -1.0 ;
         if PreviousRec > 0 then begin
            t0 := Table[PreviousRec*FNumVariables + TimeVar] ;
            x0 := Table[PreviousRec*FNumVariables + XCentroidVar] ;
            y0 := Table[PreviousRec*FNumVariables + YCentroidVar] ;
            z0 := Table[PreviousRec*FNumVariables + ZCentroidVar] ;
            t1 := Table[iRec*FNumVariables + TimeVar] ;
            x1 := Table[iRec*FNumVariables + XCentroidVar] ;
            y1 := Table[iRec*FNumVariables + YCentroidVar] ;
            z1 := Table[iRec*FNumVariables + ZCentroidVar] ;
            if t1 > t0 then
               V := sqrt( (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) + (z1-z0)*(z1-z0))/(t1-t0)
            end ;
         PreviousRec := iRec ;
         MainFrm.ResultsFrms[TrackFrmNum].SetVariable( VelocityVar, V ) ;
         end ;

     // Update table
     MainFrm.ResultsFrms[TrackFrmNum].ShowTable ;
     MainFrm.ResultsFrms[TrackFrmNum].SaveTableToFile ;

     // Find ViewFrm associated with results
     TracksImageFile := ChangeFileExt( TrackFileName, '.pic' ) ;
     TrackViewFrmNum := -1 ;
     for i := 0 to High(MainFrm.ViewFrmsInUse) do if MainFrm.ViewFrmsInUse[i] then begin
         if MainFrm.ViewFrms[i].FileName = TracksImageFile then TrackViewFrmNum := i ;
         end ;

     // If no current ViewFrm, open tracks image in new one
     // -------------------------------------------------------

     if (TrackViewFrmNum < 0) then begin
        if not FileExists(TracksImageFile) then begin
           // Create empty file of same size as objects file
           MainFrm.ViewFrms[ViewFrmNum].SaveToFile( TracksImageFile,
                                                    MainFrm.ViewFrms[ViewFrmNum].PixelDepth);
           TrackViewFrmNum := MainFrm.CreateNewViewFrm(TracksImageFile) ;
           NumPixelsPerFrame := MainFrm.ViewFrms[TrackViewFrmNum].FrameWidth*
                                MainFrm.ViewFrms[TrackViewFrmNum].FrameHeight*
                                MainFrm.ViewFrms[TrackViewFrmNum].NumComponentsPerPixel ;
           GetMem( TracksBuf, NumPixelsPerFrame*4 ) ;
           for i := 0 to NumPixelsPerFrame-1 do TracksBuf^[i] := 0 ;

           for iFrame := 1 to MainFrm.ViewFrms[ViewFrmNum].NumFrames do begin
               MainFrm.ViewFrms[TrackViewFrmNum].SaveFrame( iFrame, TracksBuf ) ;
               end ;
           end
        else begin
           // Open existing file
           TrackViewFrmNum := MainFrm.CreateNewViewFrm(TracksImageFile) ;
           end ;
        end ;

     // Write tracked objects to tracks image series

     NumPixelsPerFrame := MainFrm.ViewFrms[TrackViewFrmNum].FrameWidth*
                          MainFrm.ViewFrms[TrackViewFrmNum].FrameHeight*
                          MainFrm.ViewFrms[TrackViewFrmNum].NumComponentsPerPixel ;

     if TracksBuf = Nil then GetMem( TracksBuf, NumPixelsPerFrame*4 ) ;
     if ObjectsBuf = Nil then GetMem( ObjectsBuf, NumPixelsPerFrame*4 ) ;

     for iRec := 1 to FNumRecords do if UseRecord[iRec] then begin
         MainFrm.ResultsFrms[TrackFrmNum].RecordNum := NumTrackRecords ;
         i := iRec*NumVariables ;
         StackNum := Round(Table[i+StackVar]) ;
         ObjectID := Round(Table[i+ObjectIDVar]) ;
         MainFrm.ViewFrms[ViewFrmNum].LoadFrame( StackNum, ObjectsBuf ) ;
         MainFrm.ViewFrms[TrackViewFrmNum].LoadFrame( StackNum, TracksBuf ) ;
         for i := 0 to NumPixelsPerFrame do begin
             if ObjectsBuf[i] = ObjectID then TracksBuf[i] := TrackID ;
             end ;
         MainFrm.ViewFrms[TrackViewFrmNum].SaveFrame( StackNum, TracksBuf ) ;
         end ;


     if TracksBuf <> Nil then FreeMem( TracksBuf ) ;
     if ObjectsBuf <> Nil then FreeMem( ObjectsBuf ) ;

     end ;


function TResultsFrm.FindVariable( VarTitle : String ) : Integer ;
// ------------------------------
// Find column number of variable
// ------------------------------
var
    i : Integer ;
begin

     Result := -1 ;
     for i := 0 to FNumVariables-1 do
        if VarNames[i] = VarTitle then Result := i ;
     end ;

procedure TResultsFrm.bSaveTrackClick(Sender: TObject);
begin
     WriteObjectTrack ;
     end;

procedure TResultsFrm.SetTrackingControls ;
// --------------------------------
// Enable/disable tracking controls
// --------------------------------
begin
    bClearTrack.Enabled := rbObjectTrackingOn.Checked ;
    bSaveTrack.Enabled := rbObjectTrackingOn.Checked ;
    end ;

procedure TResultsFrm.rbObjectTrackingOnClick(Sender: TObject);
// -------------------------------
// Enable object tracking facility
// -------------------------------
begin
     SetTrackingControls ;
     bClearTrack.Click ;
     end;


end.
