unit Measure;
{ ==========================================================================
  WinWCP - Waveform measurement module
  (c) John Dempster, University of Strathclyde, 1997.  All Rights Reserved.
  V1.6f 16/7/97 ... Blocks of data in table and Summary grid can now be
                    selected for copying to clipboard
  V1.7a 31/8/97 ... Availability status for records not selected for
                    measurement now left unchanged
  V1.7c 11/9/97 ... X-Y plots now hold 6000 points
                    Histograms now copied to clipboard as Bin Mid,Y pairs
  V1.7d 1/12/97 ... Ctrl-R changes Accepted/Rejected, Record types can be set by key
  V1.8 17/12/97 ... Rise time 10%-90% limits now defined as
                    10-90% of Peak-Baseline (instead of PeakPos-PeakNeg)
  24/2/98 ... +/- keys now step forward/back through records
  2/4/98 ... 2.0 Interval for calculating rate of rise can now be
                 set by user.
                 Rate of rise limits now set by data cursors
                 (used to be Cursor 0 to Peak)
  11/7/99 ... V3.0 32 bit version
  4/9/99 ... Display grid added
  3/11/99 ... NewFile now closes form if no records available
  12/3/00 ... Rate of rise now computed using forw. difference or quadratic
  7/1/01 ... Records designated as FAIL now always return zero peak measurements
  26/4/01 ... Exponential and gaussian curves fits added to Measurements plots
  17/5/01 ... Lines joining data points in X/Y graph now removed when curve fitted
  21/5/01 ... Ctrl+1,2,3 shortcuts for TYP1, TYP2, TYP3 added
  12/8/01 ... Baseline variable now derived from record-baseline average
              and now visible in results box
  29/8/01 ... Fixed zero level now saved when changed
              From Record zero area indicated by pair of small vertical bars
  3/12/01 ... NewFile now retains displayed record number position
  6/12/01 ... Peak amplitude can now be calculated from average of
              points before or after peak signal
  25/2/02 ... Record type matching criteria added to record selection filter
  26/2/02 ... Rise time limits can now be set by user
              Progress is reported in main status bar
  12/02/03 ... T.50% replaced with user-selectable % decay time
               Boltzmann function added to X-Y plot fitting
               X-Y function & histogram cursor readout tidied up
  24.6.03 ... No. of display grid lines can be changed
  04.02.04 .. Peak - last point exceeding x% level in Peak-90% decay range
              is designateded as decay duration. This biases the decay time to
              longer intervals in the presence of noise. (Problem noticed by
              Delia Beleli. V3.3.9 now gives same T.50% result as V2.3.7)
  01.01.04 .. Out of memory error blocked when windows resized to tiny size
  19.05.05 .. Only channels selected for display now shown.
              x% value of T.(x%) variable decay time measurement now retained in file
  20.05.05 .. Abort button now works
              Correct number of records analysed reported in status line
  23.05.06 .. No. of points per line now increased > 4096
  28.11.06 .. A/D samples now comverted to floating point before measurement
              to avoid 16 bit rollover when zero baseline close to top or bottom
  28.01.08 .. Analysis cursors now initially placed at 2% and 98% of record time course
  04.09.08 .. Get Cursors button added. Places cursors within displayed waveform area
              analysis limits cursors no longer reset when t.x% or peak mode changed
  12.11.08 .. V4.0.6 Rise time and decay time precision increased by interpolating
              between samples to find exact time of threshold transition.
              T.X% decay can now be 100%
              Histograms with zero bin width now prevented
  8.05.09 .. Summary and results tables can now be printed.
              Results table now includes variable units in title
  18.05.09 .. Minor change to rise time calculation. Interpolation
              at upper and lower limits now has separate zero divide protection
  ==========================================================================}
interface

uses
  SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls, Grids, ExtCtrls, TabNotBk, Global, Shared, FileIo,
  Zero, SetAxes, SetVar, Printers, ClipBrd, maths,
  ScopeDisplay, XYPlotDisplay, ComCtrls, ValEdit, RangeEdit, Spin,
  ValidatedEdit, math, seslabio  ;

type
  TMeasureFrm = class(TForm)
    Page: TPageControl;
    AnalysisTab: TTabSheet;
    XYPlotTab: TTabSheet;
    HistogramTab: TTabSheet;
    SummaryTab: TTabSheet;
    TablesTab: TTabSheet;
    XYPlotGrp: TGroupBox;
    bNewXYPlot: TButton;
    XGroup: TGroupBox;
    Label15: TLabel;
    cbXVariable: TComboBox;
    cbXChannel: TComboBox;
    YGroup: TGroupBox;
    Label16: TLabel;
    cbYVariable: TComboBox;
    cbYChannel: TComboBox;
    bSetPlotAxes: TButton;
    plPlot: TXYPlotDisplay;
    HistGrp: TGroupBox;
    bNewHistogram: TButton;
    GroupBox3: TGroupBox;
    Label10: TLabel;
    Label11: TLabel;
    cbHistVariable: TComboBox;
    cbHistChannel: TComboBox;
    bSetHistAxes: TButton;
    plHist: TXYPlotDisplay;
    SummaryGrp: TGroupBox;
    cbSummaryChannel: TComboBox;
    Summary: TStringGrid;
    RecordGrp: TGroupBox;
    Label2: TLabel;
    cbRecordType: TComboBox;
    ckBadRecord: TCheckBox;
    sbRecordNum: TScrollBar;
    AnalysisGrp: TGroupBox;
    bDoAnalysis: TButton;
    bAbort: TButton;
    GroupBox8: TGroupBox;
    rbAllRecords: TRadioButton;
    rbThisRecord: TRadioButton;
    rbRange: TRadioButton;
    scDisplay: TScopeDisplay;
    ResultsGrp: TGroupBox;
    sgResults: TStringGrid;
    Table: TStringGrid;
    edRecordNum: TRangeEdit;
    edRecRange: TRangeEdit;
    edNumBins: TValidatedEdit;
    RecordsGrp: TGroupBox;
    Label12: TLabel;
    edPlotRecType: TEdit;
    edPlotRange: TRangeEdit;
    VariablesGrp: TGroupBox;
    ckVariable0: TCheckBox;
    ckVariable1: TCheckBox;
    ckVariable2: TCheckBox;
    ckVariable3: TCheckBox;
    ckVariable4: TCheckBox;
    ckVariable5: TCheckBox;
    ckVariable6: TCheckBox;
    ckVariable7: TCheckBox;
    ckVariable8: TCheckBox;
    ckVariable9: TCheckBox;
    ckVariable10: TCheckBox;
    ckVariable11: TCheckBox;
    ckVariable12: TCheckBox;
    ckVariable13: TCheckBox;
    Label3: TLabel;
    ckPercentage: TCheckBox;
    ckCumulative: TCheckBox;
    HistFitGrp: TGroupBox;
    bHistFit: TButton;
    cbHistEquation: TComboBox;
    erHistResults: TRichEdit;
    GroupBox1: TGroupBox;
    bFilter: TButton;
    prFilterRecords: TProgressBar;
    GroupBox2: TGroupBox;
    bClearTable: TButton;
    edFilterMatches: TEdit;
    XYFitGrp: TGroupBox;
    bXYFit: TButton;
    cbXYEquation: TComboBox;
    erXYResults: TRichEdit;
    GroupBox4: TGroupBox;
    cbPeakMode: TComboBox;
    edPeakAvg: TValidatedEdit;
    Label4: TLabel;
    GroupBox5: TGroupBox;
    cbRateofRiseMode: TComboBox;
    GroupBox6: TGroupBox;
    cbTypeToBeAnalysed: TComboBox;
    GroupBox7: TGroupBox;
    edRiseTime: TRangeEdit;
    GroupBox9: TGroupBox;
    edDecayTimePercentage: TValidatedEdit;
    Label1: TLabel;
    bGetCursors: TButton;
    GroupBox10: TGroupBox;
    edHistMin: TValidatedEdit;
    Label18: TLabel;
    Label19: TLabel;
    edHistMax: TValidatedEdit;
    procedure bDoAnalysisClick(Sender: TObject);
    procedure bAbortClick(Sender: TObject);
    procedure bNewXYPlotClick(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure bSetPlotAxesClick(Sender: TObject);
    procedure bNewHistogramClick(Sender: TObject);
    procedure cbHistVariableChange(Sender: TObject);
    procedure bSetHistAxesClick(Sender: TObject);
    procedure cbSummaryChannelChange(Sender: TObject);
    procedure TableDblClick(Sender: TObject);
    procedure TableMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure ckBadRecordClick(Sender: TObject);
    procedure cbRecordTypeChange(Sender: TObject);
    procedure edRecordNumKeyPress(Sender: TObject; var Key: Char);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure FormShow(Sender: TObject);
    procedure sbRecordNumChange(Sender: TObject);
    procedure PageChange(Sender: TObject);
    procedure edPlotRangeKeyPress(Sender: TObject; var Key: Char);
    procedure ckVariable0Click(Sender: TObject);
    procedure scDisplayMouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure scDisplayCursorChange(Sender: TObject);
    procedure cbRateofRiseModeChange(Sender: TObject);
    procedure bHistFitClick(Sender: TObject);
    procedure bFilterClick(Sender: TObject);
    procedure bClearTableClick(Sender: TObject);
    procedure bXYFitClick(Sender: TObject);
    procedure cbPeakModeChange(Sender: TObject);
    procedure edDecayTimePercentageKeyPress(Sender: TObject; var Key: Char);
    procedure bGetCursorsClick(Sender: TObject);

  private

    { Private declarations }
    procedure DisplayRecord ;
    procedure CalculateSummary ;
    procedure FillSummaryTable ;
    procedure FillTable ;
    procedure SetDefaultHistogramLimits ;
    procedure SetCheckBoxCaption( CheckBox : TCheckBox ; VarNum : Integer ) ;
    function UseVariableInSummary( VarNum : Integer ) : Boolean ;
    procedure EMFit(
              var Mean : Array of single ;
              var SD : Array of single ;
              var A : Array of single ;
              var nData : Integer ;
              var NumIterations : Integer ) ;
    function GaussianPDF( x : single ; Mean : single ; SD : single ) : single ;

    function UseRecord ( const RecH : TRecHeader ; RecType : string ) : Boolean ;
    procedure CreateVariable( iNum : Integer ;
                         var Analysis : TAnalysis ;
                         VName : string ;
                         VUnits : Array of String ;
                         VScale :single ) ;
    procedure HeapBuffers( Operation : THeapBufferOp ) ;
  public
    { Public declarations }
    procedure NewFile ;
    procedure CopyDataToClipboard ;
    procedure CopyImageToClipboard ;
    procedure Print ;
    procedure ChangeDisplayGrid ;
    procedure ZoomOut ;
    Function DataAvailable : Boolean ;
    Function ImageAvailable : Boolean ;
  end;


var
  MeasureFrm: TMeasureFrm;

implementation

uses          MDIForm,PrintGra, Printrec, Filter, Setfitpa ,
  PrintTableUnit;

{$R *.DFM}
const
     ForwardDifference = 0 ;
     Quadratic5point = 1 ;
     Quadratic7point = 2 ;
     XYDataLine = 0 ;
     FittedLine = 1 ;
     MaxGaussians = 2 ;
     NumFitPoints = 500 ;

type
    TPeakMode = ( PositivePeaks,
                  NegativePeaks,
                  AbsPeaks ) ;

TTableVars = record
           Num : Integer ;
           Chan : Integer ;
           InUse : Boolean ;
           end ;
TCursors = Record
         T0 :   Integer ;
         C0 :   array[0..WCPChannelLimit] of Integer ;
         C1 :   array[0..WCPChannelLimit] of Integer ;
         Base : array[0..WCPChannelLimit] of Integer ;
         PlotRead : Integer ;
         PlotFit0 : Integer ;
         PlotFit1 : Integer ;
         PlotT0 : Integer ;
         HistRead : Integer ;
         HistFit0 : Integer ;
         HistFit1 : Integer ;
         end ;

var
   Cursors : TCursors ;
   ADC : ^TSmallIntArray ;
   Hist : THistogram ;
   VarNames : TStringList ;
   HistResults : TStringList ;
   XYResults : TStringList ;
   TableVariables : array[0..7] of TTableVars ;
   RH : TRecHeader ; { Record header }
   { Warning/request flags }
   BuffersAllocated : boolean ;{ Indicates if memory buffers have been allocated }
   NewSummaryNeeded : Boolean ;
   NewTableNeeded : Boolean ;
   HistogramLimitsNeeded : Boolean ;
   MathFunc : TMathFunc ;
   KeepCursors : Boolean ;
   
procedure TMeasureFrm.HeapBuffers( Operation : THeapBufferOp ) ;
{ -----------------------------------------------
  Allocate/deallocation dynamic buffers from heap
  -----------------------------------------------}
begin
     case Operation of
          Allocate : begin
             if not BuffersAllocated then begin
                New(ADC) ;
                Hist := THistogram.Create ;
                VarNames := TStringList.Create ;
                XYResults := TStringList.Create ;
                HistResults := TStringList.Create ;
                MathFunc := TMathFunc.Create ;
                MathFunc.Setup( None, ' ', ' ' ) ;
                BuffersAllocated := True ;
                end ;
             end ;
          Deallocate : begin
             if BuffersAllocated then begin
                Dispose(ADC) ;
                VarNames.Free ;
                Hist.Free ;
                XYResults.Free ;
                HistResults.Free ;
                MathFunc.Free ;
                BuffersAllocated := False ;
                end ;
             end ;
          end ;
     end ;


procedure TMeasureFrm.FormShow(Sender: TObject);
{ --------------------------------------
  Initialisations when form is displayed
  --------------------------------------}
begin

     { Allocate dynamic buffers }
     HeapBuffers( Allocate ) ;

     cbRecordType.items := RecordTypes ;
     cbRecordType.items.delete(0) ; {Remove 'ALL' item}

     cbTypeToBeAnalysed.items := RecordTypes ;
     if cbTypeToBeAnalysed.itemIndex < 0 then cbTypeToBeAnalysed.itemIndex := 0 ;

     cbRateofRiseMode.ItemIndex := Settings.DifferentiationMode ;

     // X-Y plot fitting equation options
     cbXYEquation.Clear ;
     cbXYEquation.Items.AddObject( 'None', TObject(None)) ;
     cbXYEquation.Items.AddObject( 'Linear', TObject(Linear)) ;
     cbXYEquation.Items.AddObject( 'Exponential', TObject(Exponential)) ;
     cbXYEquation.Items.AddObject( '2 Exponentials', TObject(Exponential2)) ;
     cbXYEquation.Items.AddObject( 'Boltzmann', TObject(Boltzmann)) ;

     // Histogram fitting equation options
     cbHistEquation.Clear ;
     cbHistEquation.Items.AddObject( 'None', TObject(None)) ;
     cbHistEquation.Items.AddObject( 'Gaussian', TObject(Gaussian)) ;
     cbHistEquation.Items.AddObject( '2 Gaussians', TObject(Gaussian2)) ;
     cbHistEquation.Items.AddObject( '3 Gaussians', TObject(Gaussian3)) ;

     Width := Page.Width + 50 ;
     Height := Page.Height + 50 ;
     RecordsGrp.Visible := False ;
     KeepCursors := False ;

     NewFile ;

     // Always start on analysis page
     Page.ActivePage := AnalysisTab ;

     end;


procedure TMeasureFrm.NewFile ;
{ -----------------------------------------------------
  Reset variables and channel when a new file is loaded
  -----------------------------------------------------}
var
   i,ch,v : Integer ;
   VarUnits : Array[0..WCPChannelLimit] of String ;
begin

     if FH.NumRecords > 0 then begin

        // T.(x%) decay time percentage of peak value
        edDecayTimePercentage.Value := FH.DecayTimePercentage ;

        Caption := Main.UpdateCaption( FH, 'Waveform Measurements ' ) ;

        { Create waveform analysis record }
        for ch := 0 to WCPChannelLimit do VarUnits[ch] := ' ' ;
        CreateVariable(  vRecord, rH.Analysis, 'Record', VarUnits, 1. ) ;

        for ch := 0 to WCPChannelLimit do VarUnits[ch] := ' ' ;
        CreateVariable( vGroup, rH.Analysis, 'Group', VarUnits, 1. ) ;

        for ch := 0 to WCPChannelLimit do VarUnits[ch] := 's' ;
        CreateVariable( vTime, rH.Analysis, 'Time', VarUnits, 1. ) ;

        for ch := 0 to WCPChannelLimit do VarUnits[ch] := Channel[ch].ADCUnits ;
        CreateVariable(vAverage,rH.Analysis,'Average',VarUnits,1. ) ;

        for ch := 0 to WCPChannelLimit do VarUnits[ch] := Channel[ch].ADCUnits + '.'
                                                    + Settings.TUnits ;
        CreateVariable(vArea,rH.Analysis,'Area',VarUnits,Settings.TScale ) ;

        for ch := 0 to WCPChannelLimit do VarUnits[ch] := Channel[ch].ADCUnits ;

       { Type of peak analysis required }
       if cbPeakMode.ItemIndex = 2 then
          CreateVariable( vPeak,rH.Analysis,'Peak(-)',VarUnits,1. )
       else if cbPeakMode.ItemIndex = 1 then
          CreateVariable( vPeak,rH.Analysis,'Peak(+)',VarUnits,1. )
       else
          CreateVariable( vPeak,rH.Analysis,'Peak(a)',VarUnits,1. ) ;

        for ch := 0 to WCPChannelLimit do VarUnits[ch] := Channel[ch].ADCUnits + '^2' ;
        CreateVariable(vVariance,rH.Analysis,'Variance',VarUnits, 1. ) ;

        for ch := 0 to WCPChannelLimit do VarUnits[ch] := Settings.TUnits ;
        CreateVariable(vRiseTime,rH.Analysis,'Rise Time',VarUnits, Settings.TScale ) ;

        for ch := 0 to WCPChannelLimit do VarUnits[ch] := Channel[ch].ADCUnits +
                                                     '/' + Settings.TUnits ;
        CreateVariable( vRateofRise,rH.Analysis,'Rate of Rise',VarUnits,
                                                       1./Settings.TScale ) ;

        for ch := 0 to WCPChannelLimit do VarUnits[ch] := Settings.TUnits ;
        CreateVariable(vLatency,rH.Analysis,'Latency',VarUnits, Settings.TScale ) ;

        for ch := 0 to WCPChannelLimit do VarUnits[ch] := Settings.TUnits ;
        CreateVariable( vtDecay,rH.Analysis,
                        format('T.%d%%',[Round(edDecayTimePercentage.Value)]),
                        VarUnits, Settings.TScale ) ;

        for ch := 0 to WCPChannelLimit do VarUnits[ch] := Settings.TUnits ;
        CreateVariable(vT90,rH.Analysis,'T.90%',VarUnits, Settings.TScale ) ;

        for ch := 0 to WCPChannelLimit do VarUnits[ch] := 's' ;
        CreateVariable(vInterval,rH.Analysis,'Interval',VarUnits, 1. ) ;

        for ch := 0 to WCPChannelLimit do VarUnits[ch] := Channel[ch].ADCUnits ;
        CreateVariable(vBaseline,rH.Analysis,'Baseline',VarUnits, 1. ) ;

        rH.Analysis.NumVariables := vBaseline + 1 ;

        { Set up captions of variables to be summarised }
        SetCheckBoxCaption( ckVariable0, 0 ) ;
        SetCheckBoxCaption( ckVariable1, 1 ) ;
        SetCheckBoxCaption( ckVariable2, 2 ) ;
        SetCheckBoxCaption( ckVariable3, 3 ) ;
        SetCheckBoxCaption( ckVariable4, 4 ) ;
        SetCheckBoxCaption( ckVariable5, 5 ) ;
        SetCheckBoxCaption( ckVariable6, 6 ) ;
        SetCheckBoxCaption( ckVariable7, 7 ) ;
        SetCheckBoxCaption( ckVariable8, 8 ) ;
        SetCheckBoxCaption( ckVariable9, 9 ) ;
        SetCheckBoxCaption( ckVariable10, 10 ) ;
        SetCheckBoxCaption( ckVariable11, 11 ) ;
        SetCheckBoxCaption( ckVariable12, 12 ) ;
        SetCheckBoxCaption( ckVariable13, 13 ) ;

        VarNames.Clear ;
        for i := 0 to rH.Analysis.NumVariables-1 do
            VarNames.Add( rH.Analysis.VarName[i] ) ;

        { Create variable name list for X/Y plot }
        cbXVariable.items := VarNames  ;
        if cbXVariable.ItemIndex < 0  then cbXVariable.ItemIndex := 0 ;
        cbYVariable.items := VarNames  ;
        if cbYVariable.ItemIndex < 0  then cbYVariable.ItemIndex := Max(1,FH.NumChannels-1 );
        cbHistVariable.items := VarNames  ;
        if cbHistVariable.ItemIndex < 0  then cbHistVariable.ItemIndex := 0 ;

        UpdateScrollBar( sbRecordNum, FH.RecordNum, 1, fH.NumRecords ) ;

        edRecRange.LoLimit := 1 ;
        edRecRange.LoValue := 1 ;
        edRecRange.HiLimit := Max(1,fH.NumRecords) ;
        edRecRange.HiValue := Max(1,fH.NumRecords) ;

        edPlotRange.LoLimit := 1 ;
        edPlotRange.LoValue := 1 ;
        edPlotRange.HiLimit := Max(1,fH.NumRecords) ;
        edPlotRange.HiValue := Max(1,fH.NumRecords) ;
        edPlotRecType.Text := 'ALL' ;

        { Set up display channels }
        scDisplay.MaxADCValue := MaxADCValue ;
        scDisplay.MinADCValue := MinADCValue ;
        scDisplay.DisplayGrid := Settings.DisplayGrid ;
        scDisplay.NumHorizontalGridLines := Settings.NumHorizontalGridLines ;
        scDisplay.NumVerticalGridLines := Settings.NumVerticalGridLines ;

        scDisplay.MaxPoints := FH.NumSamples ;
        scDisplay.NumPoints := scDisplay.MaxPoints ;
        scDisplay.NumChannels := FH.NumChannels ;

        scDisplay.xMax := Min(Round(Channel[0].xMax),FH.NumSamples-1)  ;
        scDisplay.xMin := Min(Round(Channel[0].xMin),scDisplay.xMax) ;
        if scDisplay.xMin = scDisplay.xMax then begin
           scDisplay.xMin := 0 ;
           scDisplay.xMax := FH.NumSamples-1 ;
           end ;

        { Set channel information }
        for ch := 0 to FH.NumChannels-1 do begin
            scDisplay.ChanUnits[ch] := Channel[ch].ADCUnits ;
            scDisplay.ChanName[ch] := Channel[ch].ADCName ;
            scDisplay.ChanScale[ch] := Channel[ch].ADCScale ;
            scDisplay.ChanUnits[ch] := Channel[ch].ADCUnits ;
            scDisplay.ChanZero[ch] := Channel[ch].ADCZero ;
            scDisplay.ChanOffsets[ch] := Channel[ch].ChannelOffset ;
            scDisplay.yMin[ch] := Channel[ch].yMin ;
            scDisplay.yMax[ch] := Channel[ch].yMax ;
            scDisplay.ChanVisible[ch] := Channel[ch].InUse ;
            scDisplay.ChanColor[ch] := clBlue ;
            end ;

        scDisplay.TScale := FH.dt*Settings.TScale ;
        scDisplay.TFormat := ' %.2f ' + Settings.TUnits + ' ' ;

        { Set initial position of alignment cursors }
        if not KeepCursors then begin
           scDisplay.ClearVerticalCursors ;
           for ch := 0 to FH.NumChannels-1 do begin
               Cursors.C0[ch] := scDisplay.AddVerticalCursor(ch,clOlive,'a') ;
               scDisplay.VerticalCursors[Cursors.C0[ch]] := FH.NumSamples div 50 ;
               Cursors.C1[ch] := scDisplay.AddVerticalCursor(ch,clOlive,'a') ;
               scDisplay.VerticalCursors[Cursors.C1[ch]] := FH.NumSamples -
                                      scDisplay.VerticalCursors[Cursors.C0[ch]] ;
               scDisplay.LinkVerticalCursors( Cursors.C0[ch], Cursors.C1[ch] ) ;
               end ;

           Cursors.T0 := scDisplay.AddVerticalCursor(AllChannels,clGreen,'t=0') ;
           scDisplay.VerticalCursors[Cursors.T0] := 0 ;
           KeepCursors := False ;
           end ;

        scDisplay.ClearHorizontalCursors ;
        for ch := 0 to FH.NumChannels-1 do begin
               Cursors.Base[ch] := scDisplay.AddHorizontalCursor(
                                   ch,
                                   Settings.Colors.Cursors,
                                   True,
                                   'z' ) ;
               scDisplay.HorizontalCursors[ch] := Channel[ch].ADCZero ;
               end ;

        { Update channel selector boxes }
        cbXChannel.items := ChannelNames ;
        if cbXChannel.ItemIndex < 0 then cbXChannel.ItemIndex := 0 ;
        cbYChannel.items := ChannelNames ;
        if cbYChannel.ItemIndex < 0 then cbYChannel.ItemIndex := 0 ;
        cbHistChannel.items := ChannelNames ;
        if cbHistChannel.ItemIndex < 0 then cbHistChannel.ItemIndex := 0 ;
        { Set up Summary channel selector box }
        cbSummaryChannel.items := ChannelNames ;
        if cbSummaryChannel.ItemIndex < 0 then cbSummaryChannel.ItemIndex := 0 ;

        sgResults.RowCount := VarLimit + 2 ;
        sgResults.ColCount := FH.NumChannels + 1 ;

        { Variable name column }
        for v := 0 to VarLimit do begin
            sgResults.Cells[0,v+1] := RH.Analysis.VarName[v] ;
            sgResults.ColWidths[0] := Max(
                                      sgResults.Canvas.TextWidth(RH.Analysis.VarName[v]),
                                      sgResults.ColWidths[0] ) ;
            end ;

        { Channel name row }
        for Ch := 0 to FH.NumChannels-1 do begin
            sgResults.Cells[Ch+1,0] := Channel[Ch].ADCName ;
            end ;

        { Initialise peak mode combo box }
        if cbPeakMode.ItemIndex < 0 then cbPeakMode.ItemIndex := 0 ;

        { Initialise Table }
        Table.ColCount := high(TableVariables) + 1 ;
        for i := 0 to high(TableVariables) do begin
            TableVariables[i].Num := 0 ;
            TableVariables[i].Chan := 0 ;
            TableVariables[i].InUse := False
            end ;
        TableVariables[0].InUse := True ;
        TableVariables[1].Num := vPeak ;
        TableVariables[1].InUse := True ;

        NewSummaryNeeded := True ;
        NewTableNeeded := True ;
        HistogramLimitsNeeded := True ;

        DisplayRecord ;
        end
     else Close ;

     end ;


procedure TMeasureFrm.DisplayRecord ;
{ ========================================================
  Display digitised signal record on Page 0 of notebook
  ========================================================}
var
   ch,v,ColWidth : Integer ;
   s : string ;
begin
     sbRecordNum.Max := fH.NumRecords ;
     sbRecordNum.Min := 1 ;
     sbRecordNum.Enabled := True ;
     fH.RecordNum := SbRecordNum.position ;
     fH.CurrentRecord := SbRecordNum.position ;

     edRecordNum.LoValue := SbRecordNum.position ;
     edRecordNum.HiValue := fH.NumRecords ;

     { Read record data from file }
     GetRecord( fH, rH, fH.RecordNum, ADC^ ) ;

     for ch := 0 to FH.NumChannels-1 do begin
         // Zero level
         scDisplay.HorizontalCursors[Cursors.Base[ch]] := Channel[ch].ADCZero ;
         // Start of area from which zero level was computed (-1 indicates fixed zero level)
         scDisplay.ChanZeroAt[ch] := Channel[ch].ADCZeroAt ;
         // Signal scaling factor
         scDisplay.ChanScale[ch] := Channel[ch].ADCScale ;
         end ;
     // No. of samples in zero level area
     scDisplay.ChanZeroAvg := FH.NumZeroAvg ;

     scDisplay.TScale := RH.dt*Settings.TScale ;
     scDisplay.SetDataBuf( ADC ) ;

     { Show whether record has been rejected by operator }
     if rh.Status = 'ACCEPTED' then ckBadRecord.checked := False
                               else ckBadRecord.checked := True ;
     { Show type of record }
     if cbRecordType.items.indexOf(rh.RecType) >= 0 then
     cbRecordType.ItemIndex := cbRecordType.items.indexOf(rh.RecType);

     { Display waveform measurements for this record }
     if RH.Analysis.Available then begin

        for Ch := 0 to FH.NumChannels-1 do if Channel[Ch].InUse then begin
            ColWidth := 0 ;
            for v := 0 to VarLimit do begin
                s := format(' %.4g %s ',
                     [RH.Analysis.Value[Ch,v]*rH.Analysis.UnitsScale[Ch,v],
                      RH.Analysis.Units[Ch,v]]) ;
                sgResults.Cells[Ch+1,v+1] := s ;
                if ColWidth < sgResults.Canvas.TextWidth(s) then
                   ColWidth := sgResults.Canvas.TextWidth(s)
                end ;
            sgResults.ColWidths[Ch+1] := ColWidth ;
            end ;
        end ;
     end ;


procedure TMeasureFrm.ChangeDisplayGrid ;
{ --------------------------------------------
  Update grid pattern on oscilloscope display
  -------------------------------------------- }
begin
     scDisplay.MaxADCValue := MaxADCValue ;
     scDisplay.MinADCValue := MinADCValue ;
     scDisplay.DisplayGrid := Settings.DisplayGrid ;
     scDisplay.NumHorizontalGridLines := Settings.NumHorizontalGridLines ;
     scDisplay.NumVerticalGridLines := Settings.NumVerticalGridLines ;

     scDisplay.Invalidate ;
     end ;


procedure  TMeasureFrm.ZoomOut ;
{ ---------------------------------
  Set minimum display magnification
  --------------------------------- }
begin
     scDisplay.MaxADCValue := MaxADCValue ;
     scDisplay.MinADCValue := MinADCValue ;
     scDisplay.ZoomOut ;
     end ;


procedure TMeasureFrm.bDoAnalysisClick(Sender: TObject);
{ ===================================================
  Analyse parameters of waveforms stored in data file
  ===================================================}
var
   StartAtRec,EndAtRec,NumRecAnalysed,Rec : Integer ;
   i,j,k,ch,ChOffset : Integer ;
   x,y,dx,Peak,Temp : single ;
   PeakLo,PeakHi : single ;
   PeakAt,Invert : Integer ;

   PeakPositive,PeakNegative : single ;
   PeakPositiveAt, PeakNegativeAt : Integer ;
   // T.90% and T.X% duration
   Peak10 : single ;  // 10% of peak amplitude threshold
   TXAmplitude : single ;  // X% of peak amplitude threshold
   TXAmplitudeAt : Integer ; // Sample index when y=TXLevel

   Baseline : Integer ;
   Diff,MaxDiff : single ;
   A : array[-15..15] of single ;
   ASum : single ;
   iStart,iEnd,jLow,jHigh,iTimeZero : Integer ;
   row,col : Integer ;
   iY,i0,i1,j0,j1 : Integer ;
   EndPoint : Integer ;
   Sum,Residual,Avg,NumAvg : Single ;
   TestInterval : Integer ;
   Done,RateDone : Boolean ;
   PeakMode : TPeakMode ;
   PreviousTime : single ;

   YHiPrev, YLoNext, dRiseTime, YHi, YLo, YPrev, dDecay : Single ;

   InBuf : ^TSmallIntArray ; // 16 bit integer A/D buffer
   ADC : ^TSingleArray ;     // Floating point A/D buffer
begin

     // Create buffers
     New(InBuf) ;
     New(ADC) ;

     bDoAnalysis.enabled := False ;
     bAbort.enabled := True ;

     { Determine range of records to be analysed }

     if rbAllRecords.checked then begin
        { Analyse all records in file }
        StartAtRec := 1 ;
        EndAtRec := FH.NumRecords ;
        end
     else if rbThisRecord.checked then begin
        { Analyse the currently displayed record }
        StartAtRec := FH.RecordNum ;
        EndAtRec := FH.RecordNum ;
        end
     else begin
          { Analyse the user entered range of records }
          StartAtRec := Round(edRecRange.LoValue) ;
          EndAtRec := Round(edRecRange.HiValue) ;
          end ;

     { Type of peak analysis required }
     if cbPeakMode.ItemIndex = 2 then begin
        PeakMode := NegativePeaks ;
        end
     else if cbPeakMode.ItemIndex = 1 then begin
        PeakMode := PositivePeaks ;
        end
     else begin
        PeakMode := AbsPeaks ;
        end ;

     { Set previous time variable negative to indicate that it is invalid }
     PreviousTime := -1. ;

     Rec := StartAtRec ;
     NumRecAnalysed := 0 ;
     Done := False ;

     while Not Done do begin

         { Read record data from file }
         GetRecord( fH, rH, Rec, InBuf^ ) ;

         // Copy to float array
         for i := 0 to fH.NumSamples*fH.NumChannels-1 do ADC^[i] := InBuf^[i] ;

         { Analyse records selected according to UseRecord's criteria }
         rH.Analysis.Available := True ;
         if UseRecord( rH, cbTypeToBeAnalysed.text ) then begin

            if PreviousTime < 0. then PreviousTime := rH.Time ;

            { Analyse each signal channel in turn }
            for ch := 0 to fH.NumChannels-1 do begin

                rH.Analysis.Value[ch,vRecord] := Rec ;
                rH.Analysis.Value[ch,vGroup] := Rec ;
                rH.Analysis.Value[ch,vTime] := rH.Time ;
                rH.Analysis.Value[ch,vInterval] := rH.Time  - PreviousTime ;

                ChOffset := Channel[ch].ChannelOffset ;

                { Subtract zero level }
                j := Channel[ch].ChannelOffset ;
                for i := 0 to fH.NumSamples-1 do begin
                    ADC^[j] := ADC^[j] - Channel[ch].ADCZero ;
                    j := j + fH.NumChannels ;
                    end ;

                { Get range of samples to be analysed from positions of cursors 0 and 1 }
                iStart := Min( scDisplay.VerticalCursors[Cursors.C0[ch]],
                               scDisplay.VerticalCursors[Cursors.C1[ch]] ) ;
                iEnd :=   Max( scDisplay.VerticalCursors[Cursors.C0[ch]],
                               scDisplay.VerticalCursors[Cursors.C1[ch]] ) ;
                { Zero time cursor }
                iTimeZero := scDisplay.VerticalCursors[Cursors.t0] ;

                { Calculate average and area within cursors 0-1 }
                Sum := 0. ;
                j := iStart*fH.NumChannels + Channel[ch].ChannelOffset ;
                for i := iStart to iEnd do begin
                    Sum := Sum + ADC^[j] ;
                    j := j + fH.NumChannels ;
                    end ;
                NumAvg := (iEnd - iStart) + 1 ;
                Avg := Sum / NumAvg ;

                rH.Analysis.Value[ch,vAverage] := Avg * Channel[ch].ADCScale ;
                rH.Analysis.Value[ch,vArea] := Sum * rH.dt * Channel[ch].ADCScale ;

                { Calculate variance within cursors 0-1 }

                Sum := 0. ;
                j := iStart*fH.NumChannels + Channel[ch].ChannelOffset ;
                for i := iStart to iEnd do begin
                    Residual := ADC^[j] - Avg ;
                    Sum := Sum + Residual*Residual ;
                    j := j + fH.NumChannels ;
                    end ;
                Avg := Sum / NumAvg ;
                rH.Analysis.Value[ch,vVariance] := Avg * Channel[ch].ADCScale
                                                   * Channel[ch].ADCScale ;

                { Find peaks within cursor 0-1 region}

                PeakPositive := MinADCValue*2 ;
                PeakNegative := MaxADCValue*2 ;
                j := iStart*fH.NumChannels + Channel[ch].ChannelOffset ;
                for i := iStart to iEnd do begin
                    Y := ADC^[j] ;
                    if Y > PeakPositive then begin
                       PeakPositive := Y ;
                       PeakPositiveAt := i ;
                       end ;
                    if Y < PeakNegative then begin
                       PeakNegative := Y ;
                       PeakNegativeAt := i ;
                       end ;
                    j := j + fH.NumChannels ;
                    end ;

                case PeakMode of
                     PositivePeaks : begin
                         PeakAt := PeakPositiveAt ;
                         Invert := 1 ;
                         end ;
                     NegativePeaks : begin
                         PeakAt := PeakNegativeAt ;
                         Invert := -1 ;
                         end ;
                     AbsPeaks : begin
                         if Abs(PeakPositive) < Abs(PeakNegative) then begin
                            PeakAt := PeakNegativeAt ;
                            Invert := -1 ;
                            end
                         else begin
                            PeakAt := PeakPositiveAt ;
                            Invert := 1 ;
                            end ;
                         end ;
                     end ;

                // Determine range of points round peak to average
                if edPeakAvg.Value >= 1.0 then begin
                   // Average points after peak
                   i0 := PeakAt ;
                   i1 := Min(i0 + Trunc(edPeakAvg.Value) - 1,FH.NumSamples-1) ;
                   end
                else if edPeakAvg.Value <=  -1.0 then begin
                   // (negative) Average points before peak
                   i1 := PeakAt ;
                   i0 := Max(i1 + Trunc(edPeakAvg.Value) + 1,0);
                   end
                else  begin
                   // Single point at peak
                   i0 := PeakAt ;
                   i1 := PeakAt ;
                   end ;

                // Calculate average of points around peak
                Peak := 0.0 ;
                for i := i0 to i1 do begin
                    j := i*FH.NumChannels + Channel[ch].ChannelOffset ;
                    Peak := Peak + ADC^[j] ;
                    end ;
                rH.Analysis.Value[ch,vPeak] := (Peak*Channel[ch].ADCScale)/
                                               (i1 - i0 + 1)  ;

                { Special exception for records designated as transmission
                  failures (FAIL) peak value is always set to zero }
                if rH.RecType = 'FAIL' then rH.Analysis.Value[ch,vPeak] := 0.0 ;

                // Get lo-hi limits (ensuring lo < hi)
                PeakLo := Round(Abs(Peak)*edRiseTime.LoValue) ;
                PeakHi := Round(Abs(Peak)*edRiseTime.HiValue) ;
                if PeakLo > PeakHi then begin
                   Temp := PeakLo ;
                   PeakLo := PeakHi ;
                   PeakHi := temp ;
                   end ;

                { Calculate lo% - hi% rise time }
                i0 := PeakAt + 1;
                repeat

                  i0 := i0 - 1 ;
                  j := i0*fH.NumChannels + ChOffset ;
                  Y := ADC^[j]*Invert ;

                  // Save amplitude and last point above upper limit.
                  if Y >= PeakHi then begin
                     i1 := i0 ;
                     YHi := Y ;
                     end ;

                  // Save first point below limit
                  YLo := Y ;

                  // Exit when amplitude drops below lower limit
                  until (Y <= PeakLo) or (i0 <= iStart) ;

                // V4.0.5 Interpolate to find exact time of lo, hi transitions
                YHiPrev := ADC^[(i1-1)*fH.NumChannels + ChOffset]*Invert ;
                YLoNext := ADC^[(i0+1)*fH.NumChannels + ChOffset]*Invert ;
                dRiseTime := 0.0 ;
                if YHi <> YHiPrev then dRiseTime := dRiseTime + ((PeakHi - YHi) /(YHi - YHiPrev)) ;
                if YLoNext <> YLo then dRiseTime := dRiseTime - ((PeakLo - YLo) /(YLoNext - YLo)) ;
                dRiseTime := dRiseTime*rH.dt ;

                rH.Analysis.Value[ch,vRiseTime] := (i1-i0)*rH.dt + dRiseTime ;

                { Calculate latency as time from zero time cursor to 10% of peak }
                rH.Analysis.Value[ch,vLatency] := (i0 - iTimeZero )*rH.dt ;

                { Calculate max. rate of rise (over range from iStart ... peak ) }

                if Settings.DifferentiationMode = ForwardDifference then begin
                   jLow := 0 ;
                   jHigh := 1 ;
                   A[0] := -1.0 ;
                   A[1] := 1.0 ;
                   ASum := 1.0 ;
                   end
                else if Settings.DifferentiationMode = Quadratic5Point then begin
                   jLow := -2 ;
                   jHigh := 2 ;
                   for j := jLow to jHigh do A[j] := j ;
                   ASum := 10.0 ;
                   end
                else if Settings.DifferentiationMode = Quadratic7Point then begin
                   jLow := -3 ;
                   jHigh := 3 ;
                   for j := jLow to jHigh do A[j] := j ;
                   ASum := 28.0 ;
                   end ;

                MaxDiff := 0 ;
                for i := iStart to iEnd do begin
                    Diff := 0.0 ;
                    for j := jLow to jHigh do begin
                        k := Min(Max(i+j,0),fH.NumSamples-1) *
                             FH.NumChannels + Channel[ch].ChannelOffset ;
                        Diff := Diff + Invert*A[j]*ADC^[k] ;
                        end ;
                    Diff := Diff / ASum ;
                    if Diff > MaxDiff then MaxDiff := Diff ;
                    end ;

                rH.Analysis.Value[ch,vRateOfRise] := (MaxDiff * Invert *
                                                  Channel[ch].ADCSCale) / rH.dt ;

                { Calculate time to 90% decay }
                // V4.0.5 Exact decay point determined by interpolation between
                // first point below threshold and last point above threshold

                Peak10 := Round(Abs(Peak)*0.1) ;

                i := PeakAt - 1;
                repeat
                  Inc(i) ;
                  j := i*fH.NumChannels + Channel[ch].ChannelOffset ;
                  Y := ADC^[j]*Invert ;
                  until (Y < Peak10) or (i > iEnd) ;

                YPrev := ADC^[(i-1)*fH.NumChannels + ChOffset]*Invert ;
                if (Y <> YPrev) then begin
                   dDecay := -((Peak10 - Y) /(Y - YPrev)) *rH.dt ;
                   end
                else dDecay := 0.0 ;

                rH.Analysis.Value[ch,vT90] := (i-PeakAt)*rH.dt + dDecay ;

                // T.x% Duration from Peak - user selected % decay from peak
                // V4.0.5 Exact decay point determined by interpolation between
                // first point below threshold and last point above threshold
                TXAmplitude := Round(Abs(Peak)*(100.0-edDecayTimePercentage.Value)*0.01);
                i := PeakAt - 1 ;
                repeat
                  Inc(i) ;
                  j := i*fH.NumChannels + Channel[ch].ChannelOffset ;
                  Y := ADC^[j]*Invert ;
                  if Y >= TXAmplitude then TXAmplitudeAt := i ;
                  until (Y < 0) or (i > iEnd) ;

                YPrev := ADC^[(TXAmplitudeAt-1)*fH.NumChannels + ChOffset]*Invert ;
                if (Y <> YPrev) then begin
                   dDecay := -((TXAmplitude - Y) /(Y - YPrev)) *rH.dt ;
                   end
                else dDecay := 0.0 ;

                rH.Analysis.Value[ch,vtDecay] := (TXAmplitudeAt-PeakAt+1)*rH.dt + dDecay ;

                { Baseline level determined from average of samples at TZero cursor
                (Note. TZero cursor always taken from channel 0) }
                i0 := Min(Max(Channel[ch].ADCZeroAt,0),FH.NumSamples-1) ;
                i1 := Min(i0 + FH.NumZeroAvg - 1,FH.NumSamples-1) ;
                Sum := 0. ;
                j := i0*fH.NumChannels + Channel[ch].ChannelOffset ;
                for i := i0 to i1 do begin
                    Sum := Sum + (ADC^[j] + Channel[ch].ADCZero) ;
                    j := j + fH.NumChannels ;
                    end ;
                NumAvg := (i1 - i0) + 1 ;
                Avg := Sum / NumAvg ;
                rH.Analysis.Value[ch,vBaseline] := Avg*Channel[ch].ADCScale ;

                end ;

            { Update time of previous record }
            PreviousTime := rH.Time ;

            end ;

         // No. of records analysed
         Inc(NumRecAnalysed) ;

         { Save record header containing analysis results back to file  }
         PutRecordHeaderOnly( fH, rH, Rec ) ;

         // Report progress
         Main.StatusBar.SimpleText := format(
         ' Waveform Measurement : Analysing record %d/%d',
         [Rec,EndAtRec] ) ;

         { Exit the loop when the last record has been done
         or the user has pressed the abort button (DoAnalysis.Enabled=True) }

         Rec := Rec + 1;

         Application.ProcessMessages ;
         if (Rec > EndAtRec) or bDoAnalysis.Enabled then Done := True ;

         end ;

     { Reset buttons etc. }
     bDoAnalysis.Enabled := True ;
     bAbort.Enabled := False ;

     // Final report
     Main.StatusBar.SimpleText := format(
     ' Waveform Measurement : %d records analysed from range %d-%d',
     [NumRecAnalysed,StartAtRec,EndAtRec] ) ;

     { Update plot/Histogram record/type range boxes }
     edPlotRange.LoValue := StartAtRec ;
     edPlotRange.HiValue := EndAtRec ;
     edPlotRecType.Text := cbTypeToBeAnalysed.text ;

     { Set flags to warn other functions that data has changed }
     NewSummaryNeeded := True ;
     HistogramLimitsNeeded := True ;

     { Update currently displayed record }
     DisplayRecord ;

     Dispose(ADC) ;
     Dispose(InBuf) ;

     end;


procedure TMeasureFrm.bAbortClick(Sender: TObject);
{ -----------------------
  Abort waveform analysis
  -----------------------}
begin
     bDoAnalysis.enabled := True ;
     end;


procedure TMeasureFrm.bNewXYPlotClick(Sender: TObject);
{ --------------------
  Create an X/Y Plot
  -------------------}
var
   Rec,StartAtRec,EndAtRec,xVar,yVar,xCh,yCh : Integer ;
   x,y : Single ;
begin

     bNewXYPlot.Enabled := False ;
     Main.StatusBar.SimpleText := 'Waveform Analysis: Plotting graph.' ;

     { Plot graph of currently selected variables }
     plPlot.xAxisAutoRange := True ;
     plPlot.yAxisAutoRange := True ;

     plPlot.MaxPointsPerLine := Max( Round(edPlotRange.HiValue - edPlotRange.LoValue) + 1,
                                     NumFitPoints )  ;

     { Get channel and variables to be plotted }
     Xch := cbXChannel.itemIndex ;
     XVar := cbXVariable.ItemIndex ;
     Ych := cbYChannel.itemIndex ;
     YVar := cbYVariable.ItemIndex ;

     { Create X and Y axes labels }
     plPlot.xAxisLabel := '' ;
     if xVar > vTime then plPlot.xAxisLabel := Channel[xCh].ADCName + ' ';
     plPlot.xAxisLabel := plPlot.xAxisLabel + rH.Analysis.VarName[xVar]
                          + ' ' + rH.Analysis.Units[xCh,xVar] ;
     plPlot.yAxisLabel := '' ;
     if yVar > vTime then plPlot.yAxisLabel := Channel[yCh].ADCName + ' ';
     plPlot.yAxisLabel := plPlot.yAxisLabel + rH.Analysis.VarName[yVar]
                          + ' ' + rH.Analysis.Units[yCh,yVar] ;


     { Clear data points line }
     plPlot.CreateLine( XYDataLine , clBlue, msOpenSquare, psSolid ) ;

     StartAtRec := Round(edPlotRange.LoValue) ;
     EndAtRec := Round(edPlotRange.HiValue ) ;
     for Rec := StartAtRec to EndAtRec do begin
         { Read record analysis block from file }
         GetRecordHeaderOnly( fH, rH, Rec ) ;
         { Add to plot if selected and valid }
         if UseRecord(rH,cbTypeToBeAnalysed.text) then begin
            x := rH.Analysis.Value[XCh,XVar]*rH.Analysis.UnitsScale[XCh,XVar] ;
            y := rH.Analysis.Value[YCh,YVar]*rH.Analysis.UnitsScale[YCh,YVar] ;
            plPlot.AddPoint(0, x, y ) ;
            end ;
         end ;
     plPlot.SortByX( 0 ) ;

     { Create cursors for X/Y plot }
     plPlot.ClearVerticalCursors ;
     Cursors.PlotRead := plPlot.AddVerticalCursor( clBlue, '?r' ) ;
     plPlot.GetPoint(0,plPlot.GetNumPointsInLine(0) div 2,x,y) ;
     plPlot.VerticalCursors[Cursors.PlotRead] := x ;

     // Create curve fitting limits cursors
     Cursors.PlotFit0 := plPlot.AddVerticalCursor( clOlive, 'f' ) ;
     plPlot.GetPoint(0,0,x,y) ;
     plPlot.VerticalCursors[Cursors.PlotFit0] := x ;
     // Time zero cursor
//     Cursors.PlotT0 := plPlot.AddVerticalCursor( clGreen, 'x=0' ) ;
//     plPlot.VerticalCursors[Cursors.PlotT0] := x ;
     // Fitting upper limit cursor
     Cursors.PlotFit1 := plPlot.AddVerticalCursor( clOlive, 'f' ) ;
     plPlot.GetPoint(0,plPlot.GetNumPointsInLine(0)-1,x,y) ;
     plPlot.VerticalCursors[Cursors.PlotFit1] := x ;
     plPlot.LinkVerticalCursors(Cursors.PlotFit0,Cursors.PlotFit1);

     // Clear any existing fitted line / results
     plPlot.CreateLine( FittedLine, clRed, msNone, psSolid ) ;
     erXYResults.Lines.Clear ;

     bNewXYPlot.Enabled := True ;
     Main.StatusBar.SimpleText := 'Waveform Analysis: Graph plotted.' ;

     end;


procedure TMeasureFrm.bSetPlotAxesClick(Sender: TObject);
{ ------------------------------
  Set plot axes range/law/labels
  ------------------------------}
begin
     SetAxesFrm.Plot := plPlot ;
     SetAxesFrm.Histogram := False ;
     SetAxesFrm.ShowModal ;
     end;


procedure TMeasureFrm.bNewHistogramClick(Sender: TObject);
{ -------------------------------------
  Request a new Histogram to be plotted
  -------------------------------------}
var
   i,n,VarCh,VarVar,Rec,RecStart,RecEnd : Integer ;
   x,HScale,HLowerLimit : single ;
begin

     Screen.Cursor := crHourglass ;

     { Calculate summary data for all channels so that a suitable
     initial Histogram range can be supplied for the user }
     if NewSummaryNeeded then FillSummaryTable ;

     { Determine records, channels & variables to be plotted }
     VarCh := cbHistChannel.itemIndex ;
     VarVar := cbHistVariable.ItemIndex ;

     { Clear Histogram }
     x := edHistMin.Value ;
     Hist.NumBins := Round( EdNumBins.Value ) ;
     Hist.BinWidth := (edHistMax.Value - x )/Hist.NumBins ;
     if Hist.BinWidth <= 0.0 then Hist.BinWidth := 1.0 ;
     for i := 0 to Hist.NumBins-1 do begin
         Hist.Bins[i].lo := x ;
         Hist.Bins[i].Mid := x + Hist.BinWidth / 2. ;
         Hist.Bins[i].Hi := x + Hist.BinWidth ;
         Hist.Bins[i].y := 0. ;
         x := x + Hist.BinWidth ;
         end ;

     { Fill Histogram }
     n := 0 ;
     HScale := (Hist.NumBins) / ( Hist.Bins[Hist.NumBins-1].Mid
                                         - Hist.Bins[0].Mid ) ;
     HLowerLimit := Hist.Bins[0].Lo ;

     RecStart := Round( edPlotRange.LoValue ) ;
     RecEnd := Round( edPlotRange.HiValue ) ;
     Screen.Cursor := crHourglass ;
     for Rec := RecStart to RecEnd do begin
            { Read record analysis block from file }
            GetRecordHeaderOnly( fH, rH, Rec ) ;
            if UseRecord( rH, cbTypeToBeAnalysed.text ) then begin
               x := rH.Analysis.Value[ VarCh, VarVar  ]
                    * rH.Analysis.UnitsScale[VarCh,VarVar] ;
               i := Round( (x - HLowerLimit) * HScale ) ;
               i := Max( Min(i,Hist.NumBins-1), 0) ;
               Hist.Bins[i].y := Hist.Bins[i].y + 1. ;
               if Hist.Bins[i].y > Hist.yHi then Hist.yHi := Hist.Bins[i].y ;
               n := n + 1 ;
               end ;
            end ;


     { Plot new Histogram }
     plHist.xAxisAutoRange := False ;
     plHist.xAxisMin := Hist.Bins[0].Lo ;
     plHist.xAxisMax := Hist.Bins[Hist.NumBins-1].Hi ;
     plHist.XAxisTick := (plHist.xAxisMax - plHist.xAxisMin) / 5.0 ;

     plHist.HistogramCumulative := ckCumulative.Checked ;
     plHist.HistogramPercentage := ckPercentage.Checked ;

     plPlot.MaxPointsPerLine := Max( Hist.NumBins + 1,NumFitPoints )  ;

     { Create X and Y axes labels }
     plHist.xAxisLabel := '' ;
     if VarVar > vTime then plHist.xAxisLabel := Channel[VarCh].ADCName + ' ';
     plHist.xAxisLabel := plHist.xAxisLabel + rH.Analysis.VarName[VarVar]
                          + ' ' + rH.Analysis.Units[VarCh,VarVar] ;

     plHist.yAxisAutoRange := True ;
     if ckPercentage.Checked then plHist.yAxisLabel := '%'
                             else plHist.yAxisLabel := ' ' ;

     { Create Histogram plot }
     plHist.CreateHistogram( 0 ) ;
     for i := 0 to Hist.NumBins-1 do plHist.AddBin( 0,
                                     Hist.Bins[i].Lo,
                                     Hist.Bins[i].Mid,
                                     Hist.Bins[i].Hi,
                                     Hist.Bins[i].y ) ;

     { Create and set Histogram cursors to initial positions }
     plHist.ClearVerticalCursors ;
     Cursors.HistRead := plHist.AddVerticalCursor( clBlue, '?r' ) ;
     plHist.VerticalCursors[Cursors.HistRead] := Hist.Bins[Hist.NumBins div 2].Mid ;

     Cursors.HistFit0 := plHist.AddVerticalCursor( clOlive, 'f' ) ;
     Cursors.HistFit1 := plHist.AddVerticalCursor( clOlive, 'f' ) ;
     plHist.LinkVerticalCursors(Cursors.HistFit0,Cursors.HistFit1);

     i := 0 ;
     while (Hist.Bins[i].y = 0.0) and (i < (Hist.NumBins-1)) do Inc(i) ;
     plHist.VerticalCursors[Cursors.HistFit0] := Hist.Bins[i].Mid ;


     i := Hist.NumBins - 1 ;
     while (Hist.Bins[i].y = 0.0) and (i > 0) do Dec(i) ;
     plHist.VerticalCursors[Cursors.HistFit1] := Hist.Bins[i].Mid ;

     // Clear all existing lines on plot
     plHist.ClearAllLines ;
     erHistResults.Lines.Clear ;

     Screen.Cursor := crDefault ;
     end;


procedure TMeasureFrm.cbHistVariableChange(Sender: TObject);
begin
   SetDefaultHistogramLimits ;
   end ;


procedure TMeasureFrm.SetDefaultHistogramLimits ;
{ -----------------------------------------------------------
  Set up default Histogram range for selected variable/channel
  ------------------------------------------------------------ }
var
   Ch,iVar : Integer ;
begin
   Ch := cbHistChannel.ItemIndex ;
   iVar := cbHistVariable.ItemIndex ;

   if NewSummaryNeeded then FillSummaryTable ;

   edHistMin.Units := rH.Analysis.Units[Ch,iVar] ;
   edHistMax.Units := rH.Analysis.Units[Ch,iVar] ;

   if rH.Analysis.Num > 0 then begin
      edHistMin.Value := rH.Analysis.MinValue[Ch,iVar]*rH.Analysis.UnitsScale[Ch,iVar] ;
      edHistMax.Value := rH.Analysis.MaxValue[Ch,iVar]*rH.Analysis.UnitsScale[Ch,iVar] ;
      if edHistMax.Value = edHistMin.Value then edHistMax.Value := edHistMin.Value + 1.0 ;
      end ;

   HistogramLimitsNeeded := False ;
   end;


procedure TMeasureFrm.CalculateSummary ;
{ ========================================================
  Calculate summary of waveform analysis data held in file
  ========================================================}
var
   Rec,RecStart,RecEnd,Ch,iVar,n : Integer ;
   y : Single ;
begin

     screen.cursor := crHourGlass ;

     { Determine record range/channels to be plotted }
     RecStart := Round(edPlotRange.LoValue) ;
     RecEnd := Round(edPlotRange.HiValue) ;

     { Update Min/Max range of values in file }
     for Rec :=RecStart to RecEnd do begin

         { Get record header containing analysis results }
         GetRecordHeaderOnly( fH, rH, Rec ) ;

         if Rec = RecStart then begin
            { Initialise Min/Max if this is first record }
            for iVar := 0 to rH.Analysis.NumVariables-1 do begin
                for ch := 0 to fH.NumChannels -1 do begin
                    rH.Analysis.MaxValue[ch,iVar] := -MaxSingle ;
                    rH.Analysis.MinValue[ch,iVar] := MaxSingle ;
                    rH.Analysis.Mean[ch,iVar] := 0. ;
                    rH.Analysis.StDev[ch,iVar] := 0. ;
                    rH.Analysis.StErr[ch,iVar] := 0. ;
                    rH.Analysis.Num := 0 ;
                    end ;
                end ;
            end ;

         { Add record to analysis (if valid and of selected type }
         if UseRecord( rH, cbTypeToBeAnalysed.text ) then begin
            for iVar := 0 to rH.Analysis.NumVariables-1 do begin
                for ch := 0 to fH.NumChannels-1 do begin
                    { Min./Max. values }
                    if rH.Analysis.MaxValue[ch,iVar] < rH.Analysis.Value[ch,iVar] then
                       rH.Analysis.MaxValue[ch,iVar] := rH.Analysis.Value[ch,iVar] ;
                    if rH.Analysis.MinValue[ch,iVar] > rH.Analysis.Value[ch,iVar] then
                       rH.Analysis.MinValue[ch,iVar] := rH.Analysis.Value[ch,iVar] ;
                    { Mean }
                    rH.Analysis.Mean[ch,iVar] := rH.Analysis.Mean[ch,iVar]
                                                 + rH.Analysis.Value[ch,iVar] ;
                    end ;
                end ;
            Inc(rH.Analysis.Num) ;
            end ;

         end ;

     { Calculate mean value for each variable}
     n := Max( rH.Analysis.Num, 1 ) ;
     for ch := 0 to fH.NumChannels-1 do
         for iVar := 0 to rH.Analysis.NumVariables-1 do
                rH.Analysis.Mean[ch,iVar]:= rH.Analysis.Mean[ch,iVar] / n ;

     { Re-read the analysis blocks to get residual deviations from mean }
     for Rec := RecStart to RecEnd do begin
         GetRecordHeaderOnly( fH, rH, Rec ) ;
         if UseRecord( rH, cbTypeToBeAnalysed.text ) then begin
            for iVar := 0 to rH.Analysis.NumVariables-1 do begin
                for ch := 0 to fH.NumChannels-1 do begin
                    y := rH.Analysis.Value[ch,iVar] - rH.Analysis.Mean[ch,iVar] ;
                    rH.Analysis.StDev[ch,iVar] := rH.Analysis.StDev[ch,iVar]
                                                  + y*y ;
                    end ;
                end ;
            end ;
         end ;

     { Calculate standard deviation and error for each variable}
     for ch := 0 to fH.NumChannels-1 do begin
         for iVar := 0 to rH.Analysis.NumVariables-1 do begin
             rH.Analysis.StDev[ch,iVar]:= sqrt( rH.Analysis.StDev[ch,iVar] /
                                          Max(n - 1,1) ) ;
             rH.Analysis.StErr[ch,iVar]:= rH.Analysis.StDev[ch,iVar]/sqrt( n ) ;
             end ;
         end ;

     screen.cursor := crDefault ;
     NewSummaryNeeded := False ;
     end ;


procedure TMeasureFrm.bSetHistAxesClick(Sender: TObject);
{ -----------------------------
  Customise Histogram plot axes
  -----------------------------}
begin
     SetAxesFrm.Plot := plHist ;
     SetAxesFrm.Histogram := True ;
     SetAxesFrm.ShowModal ;
     end;


procedure TMeasureFrm.FillSummaryTable ;
{ =======================================================================
  Fill summary table with mean/stdevs etc of waveform measurement results
  =======================================================================}
const
     cName = 0 ;
     cMean = 1 ;
     cStDev = 2;
     cStErr = 3 ;
     cMin = 4 ;
     cMax = 5 ;
     cNum = 6 ;
var
   SumCh,Row,Col,iVar,MaxWidth,WidthLimit,TotalWidth,ColWidth : Integer ;
begin

     { Calculate summary data if necessary }
     if NewSummaryNeeded then CalculateSummary ;

     { Get channel to be summarised }
     if cbSummaryChannel.itemindex < 0 then cbSummaryChannel.itemindex := 0 ;
     SumCh := cbSummaryChannel.itemindex ;

     { Set column titles }
     Summary.ColCount := cNum + 1 ;
     Summary.RowCount := 2 ;
     Summary.cells[0,0] := 'Variable' ;
     Summary.cells[cMean,0] := 'Mean' ;
     Summary.cells[cStDev,0] := 'St. Dev.' ;
     Summary.cells[cStErr,0] := 'St. Error' ;
     Summary.cells[cMin,0] := 'Min.' ;
     Summary.cells[cMax,0] := 'Max.' ;
     Summary.cells[cNum,0] := '(n)' ;

     { Fill rows of table }
     Row := 0 ;
     for iVar := 0 to rH.Analysis.NumVariables-1 do begin
         if UseVariableInSummary(iVar) then begin
             Row := Row + 1 ;
             Summary.RowCount := Row + 1 ;
             { Enter row of data }
             Summary.cells[cName,Row]  := rH.Analysis.VarName[iVar] ;
             if rH.Analysis.Units[SumCh,iVar] <> ' ' then
                Summary.cells[cName,Row]  := Summary.cells[cName,Row] + ' (' +
                                             rH.Analysis.Units[SumCh,iVar] + ')';
             Summary.cells[cMean,Row]  := format( '%8.4g',
                                          [rH.Analysis.Mean[SumCh,iVar] *
                                           rH.Analysis.UnitsScale[SumCh,iVar]] ) ;
             Summary.cells[cStDev,Row] := format( '%8.4g',
                                          [rH.Analysis.StDev[SumCh,iVar] *
                                          rH.Analysis.UnitsScale[SumCh,iVar]] ) ;
             Summary.cells[cStErr,Row] := format( '%8.4g',
                                          [rH.Analysis.StErr[SumCh,iVar] *
                                          rH.Analysis.UnitsScale[SumCh,iVar]] ) ;
             Summary.cells[cMin,Row]   := format( '%8.4g',
                                          [rH.Analysis.MinValue[SumCh,iVar] *
                                          rH.Analysis.UnitsScale[SumCh,iVar]] ) ;
             Summary.cells[cMax,Row]   := format( '%8.4g',
                                          [rH.Analysis.MaxValue[SumCh,iVar] *
                                          rH.Analysis.UnitsScale[SumCh,iVar]] ) ;
             Summary.cells[cNum,Row]   := format( '%d',[rH.Analysis.Num] ) ;
             end ;
         end ;

     // Process messages to ensure stringgrid properties have changed
     Application.ProcessMessages ;

     { Set widths of columns to maximum of data }
     TotalWidth := 20 ;
     for Col := 0 to Summary.ColCount-1 do begin
         MaxWidth := 0 ;
         for Row := 0 to Summary.RowCount-1 do begin
             Summary.cells[Col,Row] := ' ' + TidyNumber(Summary.cells[Col,Row]) + ' ';
             ColWidth := Summary.canvas.TextWidth(Summary.cells[Col,Row]) ;
             if  ColWidth > MaxWidth then MaxWidth := ColWidth ;
             end ;
        Summary.ColWidths[Col] := MaxWidth ;
        TotalWidth := TotalWidth + MaxWidth ;
        end ;

     { Set width of summary table }
     WidthLimit := Page.ActivePage.Width - Summary.Left - 2 ;
     if TotalWidth < WidthLimit then Summary.Width := TotalWidth
                                else Summary.Width := WidthLimit ;


                                
     end ;


procedure TMeasureFrm.SetCheckBoxCaption(
          CheckBox : TCheckBox ;
          VarNum : Integer
          ) ;
{ --------------------------------------
  Set summary check box caption and tag#
  -------------------------------------- }
begin
     CheckBox.caption := rH.Analysis.VarName[VarNum] ;
     CheckBox.Tag := VarNum ;
     end ;

function TMeasureFrm.UseVariableInSummary(
         VarNum : Integer
         ) : Boolean ;
{ ----------------------------------------------------------------------
  Determine whether variable #VarNum is checked for inclusion in summary
  ---------------------------------------------------------------------- }
begin
     if ckVariable0.Tag = VarNum then Result := ckVariable0.Checked
     else if ckVariable1.Tag = VarNum then Result := ckVariable1.Checked
     else if ckVariable2.Tag = VarNum then Result := ckVariable2.Checked
     else if ckVariable3.Tag = VarNum then Result := ckVariable3.Checked
     else if ckVariable4.Tag = VarNum then Result := ckVariable4.Checked
     else if ckVariable5.Tag = VarNum then Result := ckVariable5.Checked
     else if ckVariable6.Tag = VarNum then Result := ckVariable6.Checked
     else if ckVariable7.Tag = VarNum then Result := ckVariable7.Checked
     else if ckVariable8.Tag = VarNum then Result := ckVariable8.Checked
     else if ckVariable9.Tag = VarNum then Result := ckVariable9.Checked
     else if ckVariable10.Tag = VarNum then Result := ckVariable10.Checked
     else if ckVariable11.Tag = VarNum then Result := ckVariable11.Checked
     else if ckVariable12.Tag = VarNum then Result := ckVariable12.Checked
     else if ckVariable13.Tag = VarNum then Result := ckVariable13.Checked
     else Result := True ;
     end ;


procedure TMeasureFrm.cbSummaryChannelChange(Sender: TObject);
{ -----------------------------------------------
  Channel displayed in summary table has changed
  ---------------------------------------------- }
begin
     { Request that the summary table be updated }
     FillSummaryTable ;
     end;


procedure TMeasureFrm.FillTable ;
{ ===================================================
  Fill table with selected waveform measurement lists
  ===================================================}
var
   StartAt,EndAt,Row,Col,Rec,n,iVar,iChan : Integer ;
begin
     { Create column title }
     for Col := 0 to Table.ColCount-1 do begin
         if TableVariables[Col].InUse then begin
            iVar := TableVariables[Col].Num ;
            iChan := TableVariables[Col].Chan ;
            Table.cells[Col,0] := VarNames[iVar] ;
            case iVar of
               vRecord,vGroup,vTime : Table.cells[Col,1] := '' ;
               else begin
                  Table.cells[Col,1] := ChannelNames[iChan] +
                                     ' (' + rH.Analysis.Units[iChan,iVar] + ')' ;
                  end ;
               end ;
            end
         else begin
            Table.cells[Col,0] := '' ;
            Table.cells[Col,1] := '' ;
            end ;
         end ;

     { Update table with data }

     StartAt := Round(edPlotRange.LoValue) ;
     EndAt := Round(edPlotRange.HiValue) ;
     Table.RowCount := EndAt - StartAt + 3 ;
     n := 0 ;
     Row := 1 ;
     for Rec := StartAt to EndAt do begin
         { Read analysis data from record }
         GetRecordHeaderOnly( fH, rH, Rec ) ;
         { Only use ACCEPTED records of appropriate type }
         if UseRecord( rH, cbTypeToBeAnalysed.text ) then begin
            { Fill row with selected variables }
            Inc(Row) ;
            for Col := 0 to Table.ColCount-1 do begin
                if TableVariables[Col].InUse then
                   Table.cells[Col,Row] := format('%8.4g',
                   [rH.Analysis.Value[TableVariables[Col].Chan,
                                      TableVariables[Col].Num ] *
                    rH.Analysis.UnitsScale[TableVariables[Col].Chan,
                                      TableVariables[Col].Num]] )
                else Table.cells[Col,Row] := '' ;
                end ;
            Inc(n) ;
            end ;
         end ;
     Table.RowCount := n + 2 ;

     { Set width table }
     Table.Width := Page.Width + Page.Left - Table.Left - 30 ;

     NewTableNeeded := False ;

     end ;


procedure TMeasureFrm.TableDblClick(Sender: TObject);
{ =======================================================
  Pop-up dialog box to set variable & channel for column
  =======================================================}
begin
     SetVarFrm.cbChannel.Items := ChannelNames ;
     SetVarFrm.cbChannel.ItemIndex := TableVariables[Table.Col].Chan ;
     setVarFrm.cbVariable.items := VarNames ;
     setVarFrm.cbVariable.Items.add('');

     { Set the combo boxes to the current column variable and channel }
     if TableVariables[Table.Col].InUse then begin
        SetVarFrm.cbVariable.ItemIndex := TableVariables[Table.Col].Num ;
        SetVarFrm.cbChannel.ItemIndex := TableVariables[Table.Col].Chan ;
        end
     else begin
        SetVarFrm.cbVariable.ItemIndex := -1 ;
        SetVarFrm.cbChannel.ItemIndex := 0 ;
        end ;

     SetVarFrm.Left := (Table.Col div Table.DefaultColWidth)
                       + Left + Table.Left + 10 ;
     SetVarFrm.Top := Top + Table.Top + 10 ;
     SetVarFrm.ShowModal ;

     if SetVarFrm.modalResult = mrOK then begin
        { If variable is blank disable this column }
        if SetVarFrm.cbVariable.text = '' then
           TableVariables[Table.Col].InUse := False
        else begin
             TableVariables[Table.Col].InUse := True ;
             TableVariables[Table.Col].Chan := SetVarFrm.cbChannel.ItemIndex ;
             TableVariables[Table.Col].Num := SetVarFrm.cbVariable.ItemIndex ;
             end ;
        FillTable ;     
        end ;
     end;


procedure TMeasureFrm.TableMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
     Table.Col := Table.MouseCoord( X,Y ).X ;
     end ;


procedure TMeasureFrm.ckBadRecordClick(Sender: TObject);
{ ------------------------------------------------
  Save new record ACCEPTED/REJECTED status to file
  ------------------------------------------------}
begin
     if ckBadRecord.checked then RH.Status := 'REJECTED'
                            else RH.Status := 'ACCEPTED' ;
     PutRecordHeaderOnly( fH, RH, fH.RecordNum ) ;
     end;


procedure TMeasureFrm.cbRecordTypeChange(Sender: TObject);
{ -----------------------------
  Save new record type to file
  ----------------------------}
begin
     RH.RecType := cbRecordType.text ;
     PutRecordHeaderOnly( fH, RH, fH.RecordNum ) ;
     end;


procedure TMeasureFrm.edRecordNumKeyPress(Sender: TObject; var Key: Char);
{ ------------------------------------
  Go to record number entered by user
  -----------------------------------}
begin
     if key = chr(13) then begin
        sbRecordNum.Position := Round(edRecordNum.LoValue) ;
        edRecordNum.HiValue := FH.NumRecords ;
        DisplayRecord ;
        end ;
     end;


procedure TMeasureFrm.FormClose(Sender: TObject; var Action: TCloseAction);
{ ---------------------------------------------
  Destroy the form to remove it from the screen
  ---------------------------------------------}
begin
     { Dellocate dynamic buffers }
     HeapBuffers( Deallocate ) ;

     Action := CaFree ;
     end;


function TMeasureFrm.UseRecord ( const RecH : TRecHeader ;
                                 RecType : string ) : Boolean ;
{ -----------------------------------------------------
  Select record for inclusion on graph, Histogram, etc.
  -----------------------------------------------------}
begin
     if (RecH.Status = 'ACCEPTED') and RecH.Analysis.Available
        and ( (RecH.RecType = RecType) or ( RecType = 'ALL') ) then
        UseRecord := True
     else UseRecord := False ;
     end ;


procedure TMeasureFrm.CreateVariable(
          iNum : Integer ;
          var Analysis : TAnalysis ;
          VName : string ;
          VUnits : Array of String ;
          VScale : single
          )  ;
var
   ch : Integer ;
begin
     Analysis.VarName[iNum] := VName ;
     for ch := 0 to WCPChannelLimit do begin
         Analysis.Units[ch,iNum] := VUnits[Min(ch,High(VUnits))] ;
         Analysis.UnitsScale[ch,iNum] := VScale ;
         end ;
    end ;


procedure TMeasureFrm.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
{ ------------------------
  Function key processing
  1/12/97 ... Ctrl-R changes Accepted/Rejected, Record types can be set by key
  -----------------------}
begin
     { function keys only active when a signal record is displayed }
     if (Page.ActivePage = AnalysisTab) then begin

        case Key of
          VK_LEFT : scDisplay.MoveActiveVerticalCursor(-1) ;
          VK_RIGHT : scDisplay.MoveActiveVerticalCursor(1) ;
          VK_SUBTRACT : begin { - key }
                if sbRecordNum.Position > sbRecordNum.Min then begin
                   sbRecordNum.Position := sbRecordNum.Position - 1 ;
                   DisplayRecord ;
                   end ;
                end ;
          VK_ADD : begin { + key }
                if sbRecordNum.Position < sbRecordNum.Max then begin
                   sbRecordNum.Position := sbRecordNum.Position + 1 ;
                   DisplayRecord ;
                   end ;
                end ;
          $31, $32, $33,$54,$4c,$45,$4d,$46 : begin
               if (Shift = [ssCtrl]) then begin
                  { Update record type }
                  case Key of
                     $31 : cbRecordType.ItemIndex := cbRecordType.Items.IndexOf('TYP1') ;
                     $32 : cbRecordType.ItemIndex := cbRecordType.Items.IndexOf('TYP2') ;
                     $33 : cbRecordType.ItemIndex := cbRecordType.Items.IndexOf('TYP3') ;
                     $54 : cbRecordType.ItemIndex := cbRecordType.Items.IndexOf('TEST') ;
                     $4c : cbRecordType.ItemIndex := cbRecordType.Items.IndexOf('LEAK') ;
                     $45 : cbRecordType.ItemIndex := cbRecordType.Items.IndexOf('EVOK') ;
                     $4d : cbRecordType.ItemIndex := cbRecordType.Items.IndexOf('MINI') ;
                     $46 : cbRecordType.ItemIndex := cbRecordType.Items.IndexOf('FAIL') ;
                     end ;
                  RH.RecType := cbRecordType.text ;
                  PutRecordHeaderOnly( fH, RH, fH.RecordNum ) ;
                  end ;
               end ;
          $52 : begin
               if (Shift = [ssCtrl]) then begin
                  ckBadRecord.Checked := not ckBadRecord.Checked ;
                  If ckBadRecord.Checked then RH.Status := 'REJECTED'
                                         else RH.Status := 'ACCEPTED' ;
                  PutRecordHeaderOnly( fH, RH, fH.RecordNum ) ;
                  end;
               end ;
          end ;
        end ;
     end;


procedure TMeasureFrm.sbRecordNumChange(Sender: TObject);
begin
     DisplayRecord ;
     end;


procedure TMeasureFrm.PageChange(Sender: TObject);
{ ----------------------------------------------
  Modifications to controls when page is changed
  ---------------------------------------------- }
var
   TopOfTabPage,LeftOfTabPage : Integer ;
begin
     TopOfTabPage := Page.Top + Page.ActivePage.Top ;
     LeftOfTabPage := Page.Left + Page.ActivePage.Left ;

     if Page.ActivePage = AnalysisTab then begin
        RecordsGrp.Visible := False ;
        end
     else if Page.ActivePage = XYPlotTab then begin
        RecordsGrp.Visible := True ;
        RecordsGrp.Top := TopOfTabPage + XYPlotGrp.Top + 10 ;
        RecordsGrp.Left := LeftOfTabPage + XYPlotGrp.Left + bNewXYPlot.Left ;
        end
     else if Page.ActivePage = HistogramTab then begin
        RecordsGrp.Visible := True ;
        RecordsGrp.Top := TopOfTabPage + HistGrp.Top + 10 ;
        RecordsGrp.Left :=  LeftOfTabPage + HistGrp.Left + bNewHistogram.Left ;
        if HistogramLimitsNeeded then SetDefaultHistogramLimits ;
        end
    else if Page.ActivePage = SummaryTab then begin
        { Place records group }
        RecordsGrp.Visible := True ;
        RecordsGrp.Top := TopOfTabPage + SummaryGrp.Top + 10 ;
        RecordsGrp.Left :=  LeftOfTabPage + SummaryGrp.Left + 8 ;
        FillSummaryTable ;
        end
    else if Page.ActivePage = TablesTab then begin
        RecordsGrp.Visible := True ;
        RecordsGrp.Top := TopOfTabPage + 10 ;
        RecordsGrp.Left :=  LeftOfTabPage + 2 ;
        FillTable ;
        end ;

    end;


function TMeasureFrm.ImageAvailable ;
// ---------------------------------------------------
// Return TRUR if image available for copying/printing
// ---------------------------------------------------
begin

     Result := False ;

     if Page.ActivePage = AnalysisTab then begin
        Result := True ;
        end
     else if Page.ActivePage = XYPlotTab then begin
        Result := plPlot.Available ;
        end
     else if Page.ActivePage = HistogramTab then begin
        Result := plHist.Available ;
        end
     else if Page.ActivePage = SummaryTab then begin
        Result := False ;
        end
     else if Page.ActivePage = TablesTab then begin
        Result := False ;
        end ;

     end ;


function TMeasureFrm.DataAvailable ;
// ---------------------------------------------------
// Return TRUE if data available for copying/printing
// ---------------------------------------------------
begin

     Result := False ;

     if Page.ActivePage = AnalysisTab then begin
        Result := True ;
        end
     else if Page.ActivePage = XYPlotTab then begin
        Result := plPlot.Available ;
        end
     else if Page.ActivePage = HistogramTab then begin
        Result := plHist.Available ;
        end
     else if Page.ActivePage = SummaryTab then begin
        Result := True ;
        end
     else if Page.ActivePage = TablesTab then begin
        Result := True ;
        end ;

     end ;



procedure TMeasureFrm.FormResize(Sender: TObject);
{ ------------------------------------------------------
  Adjust size/position of controls when form is re-sized
  ------------------------------------------------------ }
const
     BottomMargin = 2 ;
begin
     { Set size of tabbed pages }
     Page.Width := ClientWidth - Page.Left - 5 ;
     Page.Height := ClientHeight - Page.Top - 5 ;

     { Analysis page }
     AnalysisGrp.Height := Page.ActivePage.Height - AnalysisGrp.Top - BottomMargin ;

     { Set size of results group }
     ResultsGrp.Top := Page.ActivePage.Height - ResultsGrp.Height - BottomMargin ;
     ResultsGrp.Width := Page.ActivePage.Width - ResultsGrp.Left - 2 ;
     sgResults.Width := ResultsGrp.Width - sgResults.Left - 5 ;

     { Set size of scope display }
     scDisplay.Height := Max( ResultsGrp.Top - scDisplay.Top - 4,2) ;
     scDisplay.Width := Max( ResultsGrp.Width,2) ;

     { X/Y Plot page }

     { Set size of results group }
     XYFitGrp.Top := Page.ActivePage.Height - XYFitGrp.Height - BottomMargin ;
     XYFitGrp.Width := Page.ActivePage.Width - XYFitGrp.Left - 2 ;
     erXYResults.Width := XYFitGrp.Width - erXYResults.Left - 5 ;

     XYPlotGrp.Height := Page.ActivePage.Height - XYPlotGrp.Top - BottomMargin ;
     { Cursor labels }

     plPlot.Height := Max(XYFitGrp.Top - plPlot.Top - 5,2) ;
     plPlot.Width := Max(Page.ActivePage.Width - plPlot.Left - 5,2) ;

     { Histogram page }

     HistGrp.Height := Page.ActivePage.Height - HistGrp.Top - BottomMargin ;
     { Set size of results group }
     HistFitGrp.Top := Page.ActivePage.Height - HistFitGrp.Height - BottomMargin ;
     HistFitGrp.Width := Page.ActivePage.Width - HistFitGrp.Left - 2 ;
     erHistResults.Width := HistFitGrp.Width - erHistResults.Left - 5 ;

     // Plot area size
     plHist.Height := HistFitGrp.Top - plHist.Top  - 5 ;
     plHist.Width := Page.ActivePage.Width - plHist.Left - 5 ;

     { Summary page }
     SummaryGrp.Height := Page.ActivePage.Height - SummaryGrp.Top - BottomMargin ;
     Summary.Height := SummaryGrp.Height - 1 ;
     Summary.Width := Page.ActivePage.Width - Summary.Left - 5 ;

     { Tables page }
     Table.Height := Page.ActivePage.Height - Table.Top - 10 ;
     Table.Width := Page.ActivePage.Width - Table.Left - 5 ;
     
     end;



procedure TMeasureFrm.edPlotRangeKeyPress(Sender: TObject; var Key: Char);
{ -----------------------------------------------
  Update appropriate plot when plot range changed
  ----------------------------------------------- }
begin
     NewSummaryNeeded := True ;
     if key = chr(13) then begin
        if Page.ActivePage = SummaryTab then FillSummaryTable
        else if Page.ActivePage = XYPlotTab then bNewXYPlot.Click
        else if Page.ActivePage = HistogramTab then bNewHistogram.Click
        else if Page.ActivePage = SummaryTab then FillSummaryTable
        else if Page.ActivePage = TablesTab then FillTable ;
        end ;
     end;


procedure TMeasureFrm.ckVariable0Click(Sender: TObject);
begin
     FillSummaryTable ;
     end;


procedure TMeasureFrm.CopyDataToClipboard ;
{ -----------------------------------------------------------
  Copy the data in currently displayed graph to the clipboard
  -----------------------------------------------------------}
begin
     if Page.ActivePage = XYPlotTab then plPlot.CopyDataToClipboard
     else if Page.ActivePage = HistogramTab then plHist.CopyDataToClipboard
     else if Page.ActivePage = SummaryTab then CopyStringGrid(Summary)
     else if Page.ActivePage = TablesTab then CopyStringGrid(Table);
     end ;


procedure TMeasureFrm.Print ;
{ ---------------------------------------------
  Print the currently displayed graph or table
  --------------------------------------------- }
var
   i : Integer ;
begin

     if Page.ActivePage = AnalysisTab then begin
        { Print record on display }
        PrintRecFrm.Destination := dePrinter ;
        PrintRecFrm.DisplayObj := scDisplay ;
        PrintRecFrm.ShowModal ;
        if PrintRecFrm.ModalResult = mrOK then begin
           scDisplay.ClearPrinterTitle ;
           scDisplay.AddPrinterTitleLine( ' File : ' + FH.FileName ) ;
           scDisplay.AddPrinterTitleLine( ' ' + FH.IdentLine ) ;
           scDisplay.Print ;
           end ;
        end

     else if Page.ActivePage = XYPlotTab then begin
       { Print X/Y plot }
       PrintGraphFrm.Plot := plPlot ;
       PrintGraphFrm.ToPrinter := True ;
       PrintGraphFrm.ShowModal ;
       if PrintGraphFrm.ModalResult = mrOK then begin
          { Add title information to plot }
          plPlot.ClearPrinterTitle ;
          plPlot.AddPrinterTitleLine( ' File ... ' + FH.FileName ) ;
          plPlot.AddPrinterTitleLine( ' ' + FH.IdentLine ) ;
          for i := 0 to erXYResults.Lines.Count-1 do
              plPlot.AddPrinterTitleLine(erXYResults.Lines[i]) ;
          { Plot graph to printer }
          plPlot.Print ;
          end ;
       end

     else if Page.ActivePage = HistogramTab then begin
       { Print Histogram }
       PrintGraphFrm.Plot := plHist ;
       PrintGraphFrm.ToPrinter := True ;
       PrintGraphFrm.ShowModal ;
       if PrintGraphFrm.ModalResult = mrOK then begin
          { Add title information to plot }
          plHist.ClearPrinterTitle ;
          plHist.AddPrinterTitleLine( 'File ... ' + FH.FileName ) ;
          plHist.AddPrinterTitleLine( FH.IdentLine ) ;
          for i := 0 to HistResults.Count-1 do
              plHist.AddPrinterTitleLine(HistResults[i]) ;
          { Plot graph to printer }
          plHist.Print ;
          end ;
       end

     else if Page.ActivePage = SummaryTab then begin
       PrintTableFrm.Table := Summary ;
       PrintTableFrm.Title :=  'Waveform Measurements Summary: ' ;
       PrintTableFrm.ShowModal ;
       if PrintTableFrm.ModalResult = mrOK then
          WriteToLogFile( 'Waveform measurements summary table printed' ) ;
       end

     else if Page.ActivePage = TablesTab then begin
       PrintTableFrm.Table := Table ;
       PrintTableFrm.Title :=  'Waveform Measurements: ' ;
       PrintTableFrm.ShowModal ;
       if PrintTableFrm.ModalResult = mrOK then
          WriteToLogFile( 'Waveform measurements results table printed' ) ;
       end

     end ;


procedure TMeasureFrm.CopyImageToClipboard ;
{ -----------------------------------------------------
  Copy active plot to clipboard as Windows metafile
  ----------------------------------------------------- }
begin

    if Page.ActivePage = XYPlotTab then begin
       { Copy X/Y Plot }
       PrintGraphFrm.Plot := plPlot ;
       PrintGraphFrm.ToPrinter := False ;
       PrintGraphFrm.ShowModal ;
       if PrintGraphFrm.ModalResult = mrOK then plPlot.CopyImageToClipboard ;
       end
    else if Page.ActivePage = HistogramTab then begin
       { Copy Histogram }
       PrintGraphFrm.Plot := plHist ;
       PrintGraphFrm.ToPrinter := False ;
       PrintGraphFrm.ShowModal ;
       if PrintGraphFrm.ModalResult = mrOK then plHist.CopyImageToClipboard ;
       end
     else if Page.ActivePage = AnalysisTab then begin
        { Copy fit record on display }
        PrintRecFrm.Destination := deClipboard ;
        PrintRecFrm.DisplayObj := scDisplay ;
        PrintRecFrm.ShowModal ;
        if PrintRecFrm.ModalResult = mrOK then begin
           scDisplay.ClearPrinterTitle ;
           scDisplay.AddPrinterTitleLine( 'File : ' + FH.FileName ) ;
           scDisplay.AddPrinterTitleLine( FH.IdentLine ) ;
           scDisplay.CopyImageToClipboard ;
           end ;
        end ;

     end ;


procedure TMeasureFrm.scDisplayMouseUp(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
{ ---------------------------
  Display zero level mode box
  --------------------------- }
begin
     if (Button = mbRight) and (scDisplay.ActiveHorizontalCursor >=0) then begin
        ZeroFrm.ChSel := scDisplay.ActiveHorizontalCursor ;
        ZeroFrm.NewZeroAt := Round(scDisplay.ScreenCoordToX( ZeroFrm.ChSel, X )) ;
        Channel[ZeroFrm.ChSel].ADCZero := scDisplay.HorizontalCursors[ZeroFrm.ChSel] ;
        ZeroFrm.ShowModal ;
        if ZeroFrm.ModalResult = mrOK then DisplayRecord ;
        end
     else begin
        // Update zero baseline cursor
        if scDisplay.ActiveHorizontalCursor >= 0 then begin
           if Channel[scDisplay.ActiveHorizontalCursor].ADCZeroAt < 0 then begin
              // Fixed baseline level (update zero level to new position)
              Channel[scDisplay.ActiveHorizontalCursor].ADCZero :=
              scDisplay.HorizontalCursors[scDisplay.ActiveHorizontalCursor] ;
              end
           else begin
              // Baseline level computed from record (return to computed level)
              scDisplay.HorizontalCursors[scDisplay.ActiveHorizontalCursor] :=
              Channel[scDisplay.ActiveHorizontalCursor].ADCZero ;
              scDisplay.Invalidate ;
              end ;
           SaveHeader( FH ) ;
           end ;
        end ;

     end ;


procedure TMeasureFrm.scDisplayCursorChange(Sender: TObject);
{ -------------------------------------
  Update cursor labels when mouse moved
  ------------------------------------- }
var
   ch,LastCh,TZeroCursorPos,Cursor1Pos,Cursor0Pos : Integer ;
begin

     if not TScopeDisplay(Sender).CursorChangeInProgress then begin
        TScopeDisplay(Sender).CursorChangeInProgress := True ;
        for Ch := 0 to FH.NumChannels-1 do if Channel[Ch].InUse then LastCh := Ch ;

        { Time zero }
        { Update channel descriptors with any changes to display }
        for ch := 0 to scDisplay.NumChannels-1 do if Channel[ch].InUse then begin
            Channel[Ch].yMin := scDisplay.YMin[Ch] ;
            Channel[Ch].yMax := scDisplay.YMax[Ch] ;
            end ;

        TScopeDisplay(Sender).CursorChangeInProgress := False ;

        end;
     end ;

procedure TMeasureFrm.cbRateofRiseModeChange(Sender: TObject);
{ -----------------------------------------------------
  Change differentiation mode for rate of rise variable
  ----------------------------------------------------- }
begin
     Settings.DifferentiationMode := cbRateofRiseMode.ItemIndex ;
     end;


procedure TMeasureFrm.bHistFitClick(Sender: TObject);
{ ---------------------------------------------------
  Fit a gaussian prob. density functions to Histogram
  ---------------------------------------------------
    25/6/98 Clipboard buffer limit reduced to 31000
    14/1/99 New MathFunc object used }
var
   i,iStart,iEnd,nFit,iBins, Comp,LineNum,NumComp : Integer ;
   ParName : string ;
   Scale,x,dx,PScale : single ;
   ParTemp : Array[0..LastParameter] of single ;
   FitData : PXYData ;
   OK : Boolean ;
begin
     OK := True ;
     New( FitData ) ;
     Try

        { Clear all existing lines on plot }
        plHist.ClearAllLines ;
        HistResults.Clear ;
        erHistResults.Lines.Clear ;

        { Select type of equation to be fitted }
        MathFunc.Setup( TEqnType(cbHistEquation.Items.Objects[cbHistEquation.ItemIndex]),
                        Channel[cbHistChannel.ItemIndex].ADCUnits,
                        plHist.yAxisLabel)  ;
        if MathFunc.Equation = None then OK := False ;

        { Copy data into fitting array }
        if OK then begin

           nFit := 0 ;
           { Lower and upper x data limit set by display cursors }
           iStart := plHist.FindNearestIndex( 0, Cursors.HistFit0 ) ;
           iEnd :=   plHist.FindNearestIndex( 0, Cursors.HistFit1 ) ;
           for iBins := Min(iStart,iEnd) to Max(iStart,iEnd) do begin
               FitData^.x[nFit] := Hist.Bins[iBins].Mid ;
               FitData^.y[nFit] := Hist.Bins[iBins].y ;
               Inc(nFit) ;
               end ;

           // Convert to percentage of total events in histogram
           if ckPercentage.Checked then begin
              PScale := 0.0 ;
              for i := 0 to Hist.NumBins-1 do PScale := PScale + Hist.Bins[i].y ;
              PScale := 100.0 / PScale ;
              for i := 0 to nFit-1 do FitData^.y[i] := PScale*FitData^.y[i] ;
              end ;

           { Abort curve fit, if not enough data points }
           if nFit < MathFunc.NumParameters then begin
              ShowMessage( format('%d points is insufficient for fit',[nFit])) ;
              MathFunc.Setup( None, ' ',' ' ) ;
              OK := False ;
              end ;
           end ;

        { Create an initial set of guesses for parameters }
        if OK then begin
           SetFitParsFrm.MathFunc := MathFunc ;
           SetFitParsFrm.XYData := FitData ;
           SetFitParsFrm.NumPoints := nFit ;
           SetFitParsFrm.ShowModal ;
           if SetFitParsFrm.ModalResult <> mrOK then OK := False ;
           end ;

        { Fit curve using non-linear regression }
        if OK then begin
           MathFunc := SetFitParsFrm.MathFunc ;
           { Prevent FitCurve from changing parameter settings }
           MathFunc.ParametersSet := True ;
           MathFunc.UseBinWidths := False ;
           MathFunc.FitCurve( FitData^, nFit ) ;
           OK := MathFunc.GoodFit ;
           end ;

        { Plot equation on graph }
        if OK and (MathFunc.Equation <> None) then begin

           x := plHist.xAxisMin ;
           dx := (plHist.xAxisMax - plHist.xAxisMin) / NumFitPoints ;
           plHist.ShowLines := True ;
           plHist.CreateLine( FittedLine, clRed, msNone, psSolid ) ;
           for i := 0 to NumFitPoints-1 do begin
               plHist.AddPoint( FittedLine, x, MathFunc.Value(x) ) ;
               x := x + dx ;
               end ;

           { Save parameters and initialise gaussian component lines }
           NumComp := MathFunc.NumParameters div 3 ;
           LineNum := FittedLine ;
           for Comp := 0 to NumComp-1 do begin
               Inc(LineNum) ;
               plHist.CreateLine( LineNum, clRed, msNone, psSolid ) ;
               ParTemp[Comp*3] := MathFunc.Parameters[Comp*3] ;
               ParTemp[Comp*3+1] := MathFunc.Parameters[Comp*3+1] ;
               ParTemp[Comp*3+2] := MathFunc.Parameters[Comp*3+2] ;
               MathFunc.Parameters[Comp*3+2] := 0.0 ;
               end ;

           { Plot each individual gaussian component }
           if NumComp > 1 then begin
              LineNum := FittedLine ;
              for Comp := 0 to NumComp-1 do begin
                  MathFunc.Parameters[Comp*3+2] := ParTemp[Comp*3+2] ;
                  Inc(LineNum) ;
                  x := plHist.xAxisMin ;
                  dx := (plHist.xAxisMax - plHist.xAxisMin) / NumFitPoints ;
                  for i := 0 to Hist.NumBins-1 do begin
                      plHist.AddPoint( LineNum, x, MathFunc.Value(x) ) ;
                      x := x + dx ;
                      end ;
                  MathFunc.Parameters[Comp*3+2] := 0.0 ;
                  end ;
              end ;

           { Restore parameters }
           for Comp := 0 to NumComp-1 do begin
               MathFunc.Parameters[Comp*3] := ParTemp[Comp*3] ;
               MathFunc.Parameters[Comp*3+1] := ParTemp[Comp*3+1] ;
               MathFunc.Parameters[Comp*3+2] := ParTemp[Comp*3+2] ;
               end ;
           end ;


        { Display results }

        if OK then begin

          case MathFunc.Equation of
              Gaussian : HistResults.Add(
                ' y(x) = (a/^!(2^sp^ss^+2))*exp(-(x-^sm)^+2/(2*^ss^+2) )') ;
              Gaussian2 : HistResults.Add(
                ' y(x) = ^sS^-i^-=^-1^-.^-.^-2 (a^-i/^!(2^sp^ss^-i^+2))*exp(-(x-^sm^-i)^+2/(2*^ss^-i^+2) )') ;
              Gaussian3 : HistResults.Add(
                ' y(x) = ^sS^-i^-=^-1^-.^-.^-3 (a^-i/^!(2^sp^ss^-i^+2))*exp(-(x-^sm^-i)^+2/(2*^ss^-i^+2) )') ;
              end ;

           { Best fit parameters and standard error }
           for i := 0 to MathFunc.NumParameters-1 do begin

               { Convert gaussian peak parameter to gaussian area }
               if ((i+1) mod 3) = 0 then begin
                  if MathFunc.NumParameters > 3 then
                     ParName := format('Events%d',[(i+1) mod 3])
                  else ParName := 'a' ;
                  Scale := (sqrt(6.2831)*MathFunc.Parameters[i-1])/Hist.BinWidth ;
                  end
               else begin
                  ParName := MathFunc.ParNames[i] ;
                  Scale := 1.0 ;
                  end ;

               if not MathFunc.FixedParameters[i] then
                  HistResults.Add( format(' %s = %.4g ^~ %.4g (sd) %s',
                                       [ParName,
                                        Scale*MathFunc.Parameters[i],
                                        Scale*MathFunc.ParameterSDs[i],
                                        MathFunc.ParUnits[i]] ) )
               else
                  { Fixed parameter }
                  HistResults.Add( format(' %s = %.4g (fixed) %s',
                                       [ParName,
                                        MathFunc.Parameters[i],
                                        MathFunc.ParUnits[i]] ) ) ;
               end ;

           { Residual standard deviation }
           HistResults.Add( format(' Residual S.D. = %.4g %s',
                                   [MathFunc.ResidualSD,plHist.yAxisLabel] ) ) ;

           { Statistical degrees of freedom }
           HistResults.Add( format(' Degrees of freedom = %d ',
                                   [MathFunc.DegreesOfFreedom]) );

           { No. of iterations }
           HistResults.Add( format(' No. of iterations = %d ',[MathFunc.Iterations]) ) ;

           end ;

        MathFunc.CopyResultsToRichEdit( HistResults, erHistResults ) ;

     finally
            Dispose(FitData) ;
            end ;

     { Make sure plot is updated with changes }
     plHist.Invalidate ;

     end ;



procedure TMeasureFrm.EMFit(
          var Mean : Array of single ;
          var SD : Array of single ;
          var A : Array of single ;
          var nData : Integer ;
          var NumIterations : Integer ) ;
const
     MaxGaussians = 2 ;
     MaxIteration = 100 ;
     Threshold = 1E-4 ;
type
     TPostP = Array[0..MaxGaussians-1,0..5000] of Single ;
var
   Gaus : Array[0..MaxGaussians-1] of Single ;
   OldMean : Array[0..MaxGaussians-1] of Single ;
   OldSD : Array[0..MaxGaussians-1] of Single ;
   OldA : Array[0..MaxGaussians-1] of Single ;
   i,g,Iteration,Rec  : Integer ;
   RecStart,RecEnd,VarCh,VarVar : Integer ;
   Sum,XMin,XMax,Denom : single ;
   Done : Boolean ;
   x : ^TSingleArray ;
   PostP : ^TPostP ;
begin

     New(x) ;
     New(PostP) ;

     { Determine records, channels & variables to be plotted }
     VarCh := cbHistChannel.itemIndex ;
     VarVar := cbHistVariable.ItemIndex ;
     RecStart := Round( edPlotRange.LoValue ) ;
     RecEnd := Round( edPlotRange.HiValue ) ;

     nData := 0 ;
     for Rec := RecStart to RecEnd do begin
         { Read record analysis block from file }
         GetRecordHeaderOnly( fH, rH, Rec ) ;
         if UseRecord( rH, cbTypeToBeAnalysed.text ) then begin
            x^[nData] := rH.Analysis.Value[ VarCh, VarVar  ]
                    * rH.Analysis.UnitsScale[VarCh,VarVar] ;
            Inc(nData);
            end ;
         end ;

     for g := 0 to 1 do begin
         OldMean[g] := 0.0 ;
         OldSD[g] := 0.0 ;
         Olda[g] := 0.0 ;
         end ;

     Done := False ;
     Iteration := 0 ;
     while not Done do begin
         { Posterior probabilities }
         for i := 0 to nData-1 do begin
             Gaus[0] := GaussianPDF(x^[i],Mean[0],SD[0]) ;
             Gaus[1] := GaussianPDF(x^[i],Mean[1],SD[1]) ;
             PostP^[0,i] := (a[0]*Gaus[0]) / ( a[0]*Gaus[0] + a[1]*Gaus[1] ) ;
             PostP^[1,i] := 1.0 - PostP^[0,i] ;
             end ;

         { Relative proportions of each component }
         Sum := 0.0 ;
         for i := 0 to nData-1 do Sum := Sum + PostP^[0,i] ;
         a[0] := Sum / nData ;
         a[1] := 1.0 - a[0] ;

         { Mean of each component }
         for g := 0 to 1 do begin
             Sum := 0.0 ;
             for i := 0 to nData-1 do Sum := Sum + PostP^[g,i]*x^[i] ;
             if a[g] > 0.0 then Mean[g] := Sum/(nData*a[g])
                           else Mean[g] := 0.0 ;
             end ;

         { Standard deviation of each component }
         for g := 0 to 1 do begin
             Sum := 0.0 ;
             for i := 0 to nData-1 do
                 Sum := Sum + PostP^[g,i]*(x^[i]-Mean[g])*(x^[i]-Mean[g]) ;
             if a[g] > 0.0 then SD[g] := sqrt(Sum/(nData*a[g]))
                           else SD[g] := 0.0 ;
             end ;

         Inc(Iteration) ;

         Done := True ;
         for g := 0 to 1 do begin
             If OldMean[g] <> 0.0 then Denom := OldMean[g]
                                  else Denom := 1.0 ;
             if Abs(Mean[g]-OldMean[g])/Denom >= Threshold then Done := False ;
             If OldSD[g] <> 0.0 then Denom := OldSD[g]
                                  else Denom := 1.0 ;
             if Abs(SD[g]-OldSD[g])/Denom >= Threshold then Done := False ;
             If Olda[g] <> 0.0 then Denom := Olda[g]
                                  else Denom := 1.0 ;
             if Abs(a[g]-Olda[g])/Denom >= Threshold then Done := False ;
             OldMean[g] := Mean[g] ;
             OldSD[g] := SD[g] ;
             Olda[g] := a[g] ;
             end ;
         if (Iteration = MaxIteration) then Done := True ;
         end ;

     Dispose(x) ;
     Dispose(PostP) ;
     end;


function TMeasureFrm.GaussianPDF(
         x : single ;
         Mean : single ;
         SD : single
         ) : single ;
var
   z : single ;
begin
     z := (x-Mean)/SD ;
     Result := exp(-z*z*0.5)/(SD*sqrt(2.0*Pi)) ;
     end ;

procedure TMeasureFrm.bFilterClick(Sender: TObject);
{ ------------------------------------------------
  Accept/reject records based upon filter criteria
  ------------------------------------------------ }
var
   Rec,StartAtRec,EndAtRec,yVar,yCh,NumMatches : Integer ;
   Status : string ;
   AllRecords,Matched : Boolean ;
   y : Single ;
begin

     FilterFrm.VarNames := VarNames ;
     FilterFrm.ShowChannels := True ;
     FilterFrm.ShowModal ;
     if FilterFrm.ModalResult = mrOK then begin
        { Get filtering criteria }
        AllRecords := FilterFrm.AllRecords ;
        Ych := FilterFrm.Channel ;
        YVar := FilterFrm.Variable ;
        { Record range }
        StartAtRec := Round(edPlotRange.LoValue) ;
        EndAtRec := Round(edPlotRange.HiValue ) ;
        { Set progress bar }
        prFilterRecords.Min := 1 ;
        prFilterRecords.Max := FH.NumRecords ;
        prFilterRecords.Min := StartAtRec ;
        prFilterRecords.Max := EndAtRec ;
        prFilterRecords.Position := prFilterRecords.Min ;
        NumMatches := 0 ;
        edFilterMatches.text := ' ' ;
        for Rec := StartAtRec to EndAtRec do begin
            { Read record analysis block from file }
            GetRecordHeaderOnly( fH, rH, Rec ) ;
            { Set record ACCEPTED/REJECTED according to criteria }
            y := rH.Analysis.Value[YCh,YVar]*rH.Analysis.UnitsScale[YCh,YVar] ;


            Matched := False ;

            // All records have been selected
            if AllRecords then Matched := True ;

            // All types of records with selected variable matching critera
            if (FilterFrm.MatchType = 'ALL')
               and (FilterFrm.LowerLimit <= y)
               and (y <= FilterFrm.UpperLimit) then Matched := True ;

            // Selected type of record with selected variable matching critera
            if (FilterFrm.MatchType <> 'ALL') then begin
               if (rH.RecType = FilterFrm.MatchType)
                  and (FilterFrm.LowerLimit <= y)
                  and (y <= FilterFrm.UpperLimit) then Matched := True ;
               end ;

            if Matched then begin
               if FilterFrm.SetRecordStatus then rH.Status := FilterFrm.Status ;
               if FilterFrm.SetRecordType then rH.RecType := FilterFrm.RecType ;
               Inc(NumMatches) ;
               edFilterMatches.text := format(' %d/%d matches',
                                       [NumMatches,EndAtRec-StartAtRec+1]) ;
               end ;
            { Write analysis block }
            PutRecordHeaderOnly( fH, rH, Rec ) ;
            { Update progress bar }
            prFilterRecords.Position := Rec ;
            Application.ProcessMessages ;
            end ;

        { Reset filter progress bar }
        prFilterRecords.Position := prFilterRecords.Min ;

        edFilterMatches.text := format(' %d/%d matches',
                                [NumMatches,EndAtRec-StartAtRec+1]) ;

        { Log change to log file }
        if not AllRecords then WriteToLogFile (
                               Format('Records filtered %s = %.3g <= %s (%s) <= %.3g',
                               [Status,
                               FilterFrm.LowerLimit,
                               VarNames[YVar],
                               ChannelNames[YCh],
                               FilterFrm.UpperLimit] ))
        else WriteToLogFile (Format('All Records = %s',[Status])) ;

        { Update plot }
        bNewXYPlot.Click ;

        end ;

     end;

procedure TMeasureFrm.bClearTableClick(Sender: TObject);
{ -------------------------------
  Clear all columns in data table
  ------------------------------- }
var
   Col : Integer ;
begin
     for Col := 0 to Table.ColCount-1 do TableVariables[Col].InUse := False ;
     FillTable ;
     end;


procedure TMeasureFrm.bXYFitClick(Sender: TObject);
{ --------------------------------------
  Fit mathematical functions to X-Y plot
  --------------------------------------
    25/6/98 Clipboard buffer limit reduced to 31000
    14/1/99 New MathFunc object used }
var
   i,iStart,iEnd,nFit,LineNum : Integer ;
   x,dx,x0,XStart,XEnd,Y : single ;
   FitData : PXYData ;
   OK : Boolean ;
begin

     OK := True ;
     New( FitData ) ;
     Try

        // Make X/Y data graph markers only
        plPlot.MarkerStyles[XYDataLine] := msOpenSquare ;
        plPlot.LineStyles[XYDataLine] := psClear  ;

        // Clear any existing fitted line / results
        plPlot.CreateLine( FittedLine, clRed, msNone, psSolid ) ;
        XYResults.Clear ;
        erXYResults.Lines.Clear ;

        { Select type of equation to be fitted }
        MathFunc.Setup( TEqnType(cbXYEquation.Items.Objects[cbXYEquation.ItemIndex]),
                        rH.Analysis.Units[cbXChannel.ItemIndex,cbXVariable.ItemIndex],
                        rH.Analysis.Units[cbYChannel.ItemIndex,cbYVariable.ItemIndex] ) ;
        if MathFunc.Equation = None then OK := False ;

        { Copy data into fitting array }
        if OK then begin

           nFit := 0 ;
           { Lower and upper x data limit set by display cursors }
           iStart := plPlot.FindNearestIndex( 0, Cursors.PlotFit0 ) ;
           iEnd :=   plPlot.FindNearestIndex( 0, Cursors.PlotFit1 ) ;

           for i := Min(iStart,iEnd) to Max(iStart,iEnd) do begin
               plPlot.GetPoint(0,i,FitData^.x[nFit],FitData^.y[nFit]) ;
               FitData^.x[nFit] := FitData^.x[nFit] ;
               Inc(nFit) ;
               end ;

           { Abort curve fit, if not enough data points }
           if nFit < MathFunc.NumParameters then begin
              ShowMessage( format('%d points is insufficient for fit',[nFit]));
              MathFunc.Setup( None, ' ',' ' ) ;
              OK := False ;
              end ;
           end ;

        if OK then begin
          { Let user create/modify initial parameter settings and/or
            fix parameters at constant values }
           SetFitParsFrm.MathFunc := MathFunc ;
           SetFitParsFrm.XYData := FitData ;
           SetFitParsFrm.NumPoints := nFit ;
           SetFitParsFrm.ShowModal ;
           if SetFitParsFrm.ModalResult <> mrOK then OK := False ;
           end ;

        { Fit curve using non-linear regression }
        if OK then begin
           MathFunc := SetFitParsFrm.MathFunc ;
           { Prevent FitCurve from changing parameter settings }
           MathFunc.ParametersSet := True ;
           MathFunc.UseBinWidths := False ;
           MathFunc.FitCurve( FitData^, nFit ) ;
           OK := MathFunc.GoodFit ;
           end ;

        // Plot equation on graph
        if OK and (MathFunc.Equation <> None) then begin
           plPlot.GetPoint(0,iStart,XStart,y) ;
           plPlot.GetPoint(0,iEnd,XEnd,y) ;
           dx := (XEnd - XStart) / NumFitPoints ;
           X := XStart ;
           plPlot.ShowLines := True ;
           for i := 0 to NumFitPoints-1 do begin
               plPlot.AddPoint( FittedLine, x, MathFunc.Value(x) ) ;
               x := x + dx ;
               end ;
           end ;

        { Display results }

        if OK then begin

           XYResults.Add( ' ' + MathFunc.Name ) ;

           { Best fit parameters and standard error }
           for i := 0 to MathFunc.NumParameters-1 do begin

               if not MathFunc.FixedParameters[i] then
                  XYResults.Add( format(' %s = %.4g +/- %.4g (sd) %s',
                                       [MathFunc.ParNames[i],
                                        MathFunc.Parameters[i],
                                        MathFunc.ParameterSDs[i],
                                        MathFunc.ParUnits[i]] ) )
               else
                  { Fixed parameter }
                  XYResults.Add( format(' %s = %.4g (fixed) %s',
                                       [MathFunc.ParNames[i],
                                        MathFunc.Parameters[i],
                                        MathFunc.ParUnits[i]] ) ) ;
               end ;

           { Residual standard deviation }
           XYResults.Add( format(' Residual S.D. = %.4g %s',
           [MathFunc.ResidualSD,
           rH.Analysis.Units[cbYChannel.ItemIndex,cbYVariable.ItemIndex]] ) ) ;

           { Statistical degrees of freedom }
           XYResults.Add( format(' Degrees of freedom = %d ',
                                [MathFunc.DegreesOfFreedom]) );

           { No. of iterations }
           XYResults.Add( format(' No. of iterations = %d ',
                                [MathFunc.Iterations]) ) ;

           MathFunc.CopyResultsToRichEdit( XYResults, erXYResults ) ;

           end ;

     finally
            Dispose(FitData) ;
            end ;

     { Make sure plot is updated with changes }
     plPlot.Invalidate ;

     end ;



procedure TMeasureFrm.cbPeakModeChange(Sender: TObject);
// -----------------------------
// Peak measurement mode changed
// -----------------------------
begin
     // Call NewFile procedure to update peak variable name
     KeepCursors := True ;
     NewFile ;
     end;


procedure TMeasureFrm.edDecayTimePercentageKeyPress(Sender: TObject; var Key: Char);
// ----------------------------------
// T.x% decay time end-point changed
// ----------------------------------
begin
     // Call NewFile procedure to update T.x% variable name
     if Key = #13 then begin
        FH.DecayTimePercentage := edDecayTimePercentage.Value ;
        SaveHeader(FH) ;
        KeepCursors := True ;
        NewFile ;
        end ;
     end ;


procedure TMeasureFrm.bGetCursorsClick(Sender: TObject);
// ---------------------------------------------------
// Place analysis cursors within displayed screen area
// ---------------------------------------------------
var
    ch : Integer ;
    XMargin : Single ;
begin
   XMargin := (scDisplay.xMax - scDisplay.xMin)*0.02 ;
   for ch := 0 to FH.NumChannels-1 do begin
       scDisplay.VerticalCursors[Cursors.C0[ch]] := Round(scDisplay.xMin + XMargin) ;
       scDisplay.VerticalCursors[Cursors.C1[ch]] := Round(scDisplay.xMax - XMargin) ;
       scDisplay.VerticalCursors[Cursors.T0] := Round(scDisplay.xMin) ;
       end ;
   end ;

Initialization
     BuffersAllocated := False ;
end.
