unit uCom;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, AdProtcl, AdPStat, OoMisc, AdPort, StdCtrls, ExtCtrls, ComCtrls,
  Menus, FileCtrl, Buttons, INIFiles, ZipForge, IdBaseComponent,
  IdComponent, IdCustomTCPServer, IdTCPServer, IdCmdTCPServer,
  IdExplicitTLSClientServerBase, IdFTPServer, IdFTPListOutput,
  IdFTPList;

type
  tDirEntry = record
    // Directory Result from Reciever
    Index: integer;
    Transfered: integer;
    Filename: string[15];
    LastRecording: tDateTime;
    Filesize: integer;
    Fileopen: integer;
    FileAge: extended; // number of seconds file has been closed
  end;
  tDirEntries = array of tDirEntry;
  TfMain = class(TForm)
    Status: TStatusBar;
    MainMenu: TMainMenu;
    File1: TMenuItem;
    Exit1: TMenuItem;
    ReprocessRawFile1: TMenuItem;
    ReprocessDirectory1: TMenuItem;
    Options1: TMenuItem;
    Settings1: TMenuItem;
    bQUIT: TBitBtn;
    Timer1: TTimer;
    cbTimerOK: TCheckBox;
    Zip: TZipForge;
    gbProcessLog: TGroupBox;
    LogMemo: TMemo;
    gbFTPLog: TGroupBox;
    FTPLogMemo: TMemo;
    cbFTPEnable: TCheckBox;
    IdFTPServer1: TIdFTPServer;
    btRCapLaunch: TButton;
    procedure Delay( f: extended );
    procedure FormShow(Sender: TObject);
    function  FZip( inf, ouf: string ): boolean;
    procedure bQUITClick(Sender: TObject);
    procedure Exit1Click(Sender: TObject);
    procedure ReprocessRawFile1Click(Sender: TObject);
    procedure Settings1Click(Sender: TObject);
    procedure ProcessFilesInFolder( P: string );
    procedure ReprocessDirectory1Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure IdFTPServer1ChangeDirectory(ASender: TIdFTPServerContext;
      var VDirectory: string);
    procedure IdFTPServer1GetFileSize(ASender: TIdFTPServerContext;
      const AFilename: string; var VFileSize: Int64);
    procedure IdFTPServer1ListDirectory(ASender: TIdFTPServerContext;
      const APath: string; ADirectoryListing: TIdFTPListOutput; const ACmd,
      ASwitches: string);
    procedure IdFTPServer1RetrieveFile(ASender: TIdFTPServerContext;
      const AFileName: string; var VStream: TStream);
    procedure IdFTPServer1UserLogin(ASender: TIdFTPServerContext;
      const AUsername, APassword: string; var AAuthenticated: Boolean);
    procedure cbFTPEnableClick(Sender: TObject);
    procedure btRCapLaunchClick(Sender: TObject);
    procedure FormResize(Sender: TObject);
  private
    { Private declarations }
    InBuf: string;
    Busy: boolean;
    LastTimer: tDateTime;
  public
    { Public declarations }
    procedure Log( S: string );
    procedure FTPLog( S: string );
    procedure RotateLog;
    procedure ConvertDATFile( FN: string );
  end;

var
  fMain: TfMain;

implementation

uses uUtility, uNMEA, uComUtil, uOptions, uTagLink, Version, uMonitors;

{$R *.dfm}

  const
    ACOL = 12;
    MaxLogLines = 250;
    LastHour: integer = -1;
    SecondsPerDay = 24 * 60 * 60;
    WantToQuit: boolean = FALSE;

  procedure TfMain.Log( S: string );
  // enter a note in the system log
  var
    F: System.Text;
  const
    FN: string = '';
  begin
    if LogMemo.Lines.Count > MaxLogLines + 25 then
    begin
      LogMemo.Lines.BeginUpdate;
        while LogMemo.Lines.Count > MaxLogLines do LogMemo.Lines.Delete(0);
      LogMemo.Lines.EndUpdate;
    end;
    LogMemo.Lines.Append( S );
    LogMemo.SelStart := Length( LogMemo.Text ) - 5;
    LogMemo.SelLength := 1;
    // Log to text file
    try
      {$I+}
      if FN='' then FN := GetEntry('Logfile');
      if FN='' then exit;

      if NOT DirectoryExists(ExtractFilePath(FN)) then ForceDirectories(ExtractFilePath(FN));
      AssignFile( F, FN );
      try
        try
          Append( F );
        except
          ReWrite( F );
        end;
        WriteLn( F, DateTimeToStr(Now)+' '+S );
      finally
        CloseFile( F );
      end;
      {$I-}
    except
      LogMemo.Lines.Append(' ** LOG FILE '+FN+' may not be complete!');
    end;
  end;

  procedure Log( S: string );
  begin
    if Assigned( fMain) then fMain.Log( S );
  end;

  procedure TfMain.FTPLog( S: string );
  // enter a note in the system log
  var
    F: System.Text;
  const
    FN: string = '';
  begin
    if FTPLogMemo.Lines.Count > MaxLogLines + 25 then
    begin
      FTPLogMemo.Lines.BeginUpdate;
        while FTPLogMemo.Lines.Count > MaxLogLines do FTPLogMemo.Lines.Delete(0);
      FTPLogMemo.Lines.EndUpdate;
    end;
    FTPLogMemo.Lines.Append( S );
    FTPLogMemo.SelStart := Length( FTPLogMemo.Text ) - 5;
    FTPLogMemo.SelLength := 1;
    // Log to text file
    try
      {$I+}
      if FN='' then FN := GetEntry('FTPLogfile');
      if FN='' then exit;

      if NOT DirectoryExists(ExtractFilePath(FN)) then ForceDirectories(ExtractFilePath(FN));
      AssignFile( F, FN );
      try
        try
          Append( F );
        except
          ReWrite( F );
        end;
        WriteLn( F, DateTimeToStr(Now)+' '+S );
      finally
        CloseFile( F );
      end;
      {$I-}
    except
      FTPLogMemo.Lines.Append(' ** LOG FILE '+FN+' may not be complete!');
    end;
  end;

  procedure FTPLog( S: string );
  begin
    if Assigned( fMain) then fMain.FTPLog( S );
  end;

  procedure TfMain.RotateLog;
  var
    FN,NFN: string;
    Ndt, Fdt: tDateTime;
    Y,M,D: word;
    i: integer;
  begin
    FN := GetEntry('Logfile');
    Ndt := Now;
    Fdt := GetFileDateDT( FN );
    // If Fdt is a different day, then rotate
    if (Trunc(Ndt) <> Trunc(Fdt)) then
    begin
      DecodeDate( Fdt, Y, M, D );
      NFN := ChangeFileExt( FN, '.'+Int2StrN(Y,4)+Int2StrN(M,2)+Int2StrN(D,2)+'.log');
      i := 1;
      while FileExists( NFN ) do
      begin
        Inc(i);
        NFN := ChangeFileExt( NFN, '.log'+IntToStr(i) );
      end;
      RenameFile( FN, NFN );
      Log('Rotated log file '+FN+' to '+NFN);
    end;
    // *********
    FN := GetEntry('FTPLogfile');
    Ndt := Now;
    Fdt := GetFileDateDT( FN );
    // If Fdt is a different day, then rotate
    if (Trunc(Ndt) <> Trunc(Fdt)) then
    begin
      DecodeDate( Fdt, Y, M, D );
      NFN := ChangeFileExt( FN, '.'+Int2StrN(Y,4)+Int2StrN(M,2)+Int2StrN(D,2)+'.log');
      i := 1;
      while FileExists( NFN ) do
      begin
        Inc(i);
        NFN := ChangeFileExt( NFN, '.log'+IntToStr(i) );
      end;
      RenameFile( FN, NFN );
      FTPLog('Rotated FTPlog file '+FN+' to '+NFN);
    end;
  end;

  procedure Stat( S: string );
  begin
    fMain.Status.SimpleText := S;
    Application.ProcessMessages;
  end;

  procedure TfMain.Delay( f: extended );
  var
    st: tDateTime;
    dt: extended;
    i: integer;
  begin
    st := Now;
    repeat
      dt := DeltaTimeSeconds(st);
      Stat('Delay '+ExtToStr(f-dt,1)+' more seconds.');
      for i:=1 to 50 do Application.ProcessMessages;
    until dt >= f;
  end;


  function IntToSessionName( I: integer ): string;
  // returns a session identifier: A-X (sessions 1-24)
  //                               AA-XA (sessions 25 - 48)
  //                               AB-XB (sessions 49 - 72)
  //                               AC-XC (sessions 73 - 96)
  begin
    case i of
      1..24:  result := Char( Ord('a') + i - 1 );
      25..48: result := IntToSessionName( i-24 ) + 'a';
      49..72: result := IntToSessionName( i-48 ) + 'b';
      73..96: result := IntToSessionName( i-72 ) + 'b';
      else    result := '?';
    end;
  end;

  function Sec2HMS( i: integer ): string;
  var
    r: integer;
  begin
    r := i MOD 60;
    result := Int2StrN( r, 2 );
    i := i DIV 60;
    r := i MOD 60;
    i := i DIV 60;
    result := Int2StrN( i, 2 ) + Int2StrN( r, 2 ) + result;
  end;



procedure TfMain.FormShow(Sender: TObject);
var
  INIFile: tINIFile;
begin
  IdFTPServer1.Active := FALSE;

  // Get screen position
  INIFile := tINIFile.Create( ChangeFileExt( Application.EXEName, '.ini' ));
  try
    Left   := INIFile.ReadInteger( 'FORM', 'Left', Left );
    Top    := INIFile.ReadInteger( 'FORM', 'Top', Top );
    Width  := INIFile.ReadInteger( 'FORM', 'Width', Width );
    Height := INIFile.ReadInteger( 'FORM', 'Height', Height );
  finally
    INIFile.Free;
  end;
  InsureOnScreen( Self );
  // Clear the grid values
  fOptions.ClearEntries;
  fOptions.AddEntry( '[BASE INFORMATION]',        '', oesSeparator, '', '' );

  fOptions.AddEntry( 'Station Name',              'ig10', oesSimple, '',
                     'Name of base station. Four characters A..Z, 0..9, _.' );

  fOptions.AddEntry( 'Reciever IP:Port',          '208.187.131.50:1003', oesSimple, '',
                     'Receiver IP address and port number "208.187.131.50:1003"' );

  fOptions.AddEntry( 'Antenna Number',             '00000000', oesSimple, '',
                     'The Antenna Number to include in OBS files.');

  fOptions.AddEntry( 'Antenna Type', 'CHCC220GR       CHCD', oesSimple, '',
                     'The Antenna type to include in OBS files.');

  fOptions.AddEntry( 'Operating Agency', 'iGage', oesSimple, '',
                     'The name of the base operator to include in OBS files.');

  fOptions.AddEntry( 'Observer', 'MES', oesSimple, '',
                     'The name of the observer to include in OBS files.');

  fOptions.AddEntry( 'OBS Comment', '', oesSimple, '',
                     'Optional comment to include in OBS files.');

  fOptions.AddEntry( 'Receiver Serial Number', 'R112345', oesSimple, '',
                     'GPS receiver serial number to include in OBS files.');

  fOptions.AddEntry( '[DIRECTORIES]', '', oesSeparator, '', '' );

  fOptions.AddEntry( 'Temp Folder', 'e:\iBase\temp', oesFolder, '',
                     'Enter the folder where the program may store temp files.' );

  fOptions.AddEntry( 'Logfile', 'e:\iBase\log\iBase.log', oesFilename, 'All files (*.*)|*.*',
                     'Enter the name and path of the file to store log entries to.' );

  fOptions.AddEntry( 'FTPLogfile', 'e:\iBase\log\FTP.log', oesFilename, 'All files (*.*)|*.*',
                     'Enter the name and path of the file to store FTP log entries to.' );

  fOptions.AddEntry( 'Raw Folder', 'e:\iBase\raw\', oesFolder, '',
                     'Enter the folder where raw data files are placed.' );

  fOptions.AddEntry( 'Raw Backup Folder', 'e:\iBase\BACKUP\native\', oesFolder, '',
                     'Enter the folder where backup raw data files are placed. Typically on separate physical drive.' );

  fOptions.AddEntry( 'FTP Folder', 'e:\iBase\ftp\', oesFolder, '',
                     'Base folder exposed to ftp clients.' );

  fOptions.AddEntry( 'RINEX Folder', 'e:\iBase\ftp\rinex\', oesFolder, '',
                     'Enter the folder where RINEX data files are placed.' );

  fOptions.AddEntry( 'NATIVE Folder', 'e:\iBase\ftp\native\', oesFolder, '',
                     'Enter the folder where the compressed Native (.DAT) files are placed.' );

  fOptions.AddEntry( '[TEQC CONFIGURATION]', '', oesSeparator, '', '' );
  fOptions.AddEntry( 'TEQC Process Command',
                     '+G +R +S +E +C -phc '+
                     '-O.r "%rb%" '+
                     '-O.mo "%mn%" '+
                     '-O.an "%an%" '+
                     '-O.at "%at%" '+
                     '-O.ag "%ag%" '+
                     '-O.c "%cmt%" '+
                     '-O.rn "%rsn%" '+
                     '+err "%lf%" '+
                     // '+out %lf% '+
                     '+nav "%nf%","%glonf%","%sbnf%","%galnf%" '+
                     '+obs "%of%" ',
                     oesSimple, '',
                     'The TEQC command to convert DAT file to NAV and OBS. %of% OBSFile, %nf% GPS Nav, '+
                     '%glonf% GLO Nav, %galnf% GAL Nav, %lf% LogFile, %an% ant number, %at% ant type, '+
                     '%ag% agency, %cmt% comment, %rsn% recv sn, %mn% station id, %rb% iBase Build#' );

  fOptions.AddEntry( '[FTP CONFIGURATION]', '', oesSeparator, '', '' );
  fOptions.AddEntry( 'FTP Port', '21', oesSimple, '',
                     'FTP Port number');
  fOptions.AddEntry( 'FTP Users', 'user,user;mark,silver', oesSimple, '',
                     'Semicolon delimited list of valid FTP Users "name1,pw1;name2,pw2"');


  Log('----- iBase starting '+DateTimeToStr(Now)+' -------------------------------------------------');
  FTPLog('----- iBase starting '+DateTimeToStr(Now));


end;

procedure TfMain.bQUITClick(Sender: TObject);
begin
  cbFTPEnable.Checked := FALSE;
  cbTimerOK.Checked := FALSE;
  WantToQuit := TRUE;
  Log('!!! Attempt to QUIT !!! User pressed ''QUIT NOW!'' button!!!');
end;

function FileExistsClosed( FN: string;             // filename to check
                           maxwait: extended;      // maximum time to wait in seconds
                           MinSize: integer ): boolean;  // minimum filesize to allow
// wait up to maxwait for FN to exist, have a filesize other than 0 and have a filedatetime
// that is 1/5 of a second old.
var
  StartTime: tDateTime;
  dT, FileAge: extended;
begin
  try
    StartTime := Now;
    result := TRUE; // assume success!
    repeat
      if WantToQuit then exit;
      if uFileExists( FN ) then
      begin
        if (uFileSize( FN ) > MinSize) then
        begin
          FileAge := DeltaTimeSeconds( GetFileDateDT( FN ) );
          if FileAge > 0.2 then exit;
        end;
      end;
      Delay( 0.2 );
      dT := DeltaTimeSeconds( StartTime );
      Stat('Waiting for '+FN+' to close: '+ExtToStrComma(dT,1)+'/'+ExtToStrComma(maxwait,1)+' seconds.');
    until dT > MaxWait;
    result := FALSE;
  finally
    Stat('');
  end;
end;

procedure CAP2stdFN( _FN: string; var Path, stdFN: string );
//        0        1
//        1234567890123456
// Change IG03201501300526.cap
// to     IG03jjja0.yyo
var
  FN,site, year, month, day, hour, dayofyear, session: string;
  iYear, iMonth, iDay, jDate: word;
  D: tDateTime;
begin
  FN := ExtractFilename( _FN ); // just the filename, loose path
  Path := ''; stdFN := ''; // failsafe
  if Length( FN ) < 16  then exit;

  // Extract components
  site  := LowerCase( Copy( FN, 1, 4 ));
  year  := Copy( FN, 5, 4 );
  month := Copy( FN, 9, 2 );
  day   := Copy( FN, 11, 2 );
  hour  := Copy( FN, 13, 4 );
  // Compute Julian Day of Year
  try
    iYear  := StrToInt( Year );
    iMonth := StrToInt( Month );
    iDay   := StrToInt( Day );
    D := EncodeDate( iYear, iMonth, iDay );
    jDate := Round( D - EncodeDate( iYear, 1, 1 )) + 1;
  except
    Log('Failed to convert filename to Julian Date.');
  end;
  dayofyear := Int2StrN( jDate, 3 );
  // Generate Hour
  session := IntToSessionName( 1 + StrToInt( Copy( hour, 1, 2 ) ));
  // build filename
  stdFN :=  site + Int2StrN( jDate, 3 ) + session + '.' + copy ( year, 3, 2 ); // caller can append filetype
  // build path
  Path := year + '\' + Int2StrN( jDate, 3 ) + '\' + site + '\';
end;

function TfMain.FZip( inf, ouf: string ): boolean;
var
  exep: string;
begin
  result := FALSE;
  // Only process if the input file exists
  if uFileExists(inf) then
  begin
    // Zip the files
    if Pos( '.zip', ouf ) < 1 then ouf := ouf + '.zip';
    // Make sure the output file does not already exist
    if FileExists( ouf ) then
    begin
      uDeleteFile(ouf);
      Delay( 0.2 );
    end;
    Log(' FZip ZIP '+inf+' to '+ouf );
    ZIP.TempDir  := AddBackSlash( GetEntry('Temp Folder') );
    ZIP.Filename := ouf;
    ZIP.BaseDir  := ExtractFilePath( ouf );
    ZIP.OpenArchive;
    try
      ZIP.AddFiles( inf );
      result := TRUE;
    finally
      ZIP.CloseArchive;
    end;
  end else
  begin
    Log(' FZip missing file '+inf);
  end;
end;


function ReplaceChars( Path: string ):string;
// Force FTP folder and filenames into our jail
begin
  Path := StrReplace( Path, '/', '\' );
  Path := StrReplace( Path, '\\', '\' );
  Path := StrReplace( Path, '..', '.' );
  Result := Path;
end;

function GetSizeOfFile(AFile : String) : Integer;
var
 FStream : TFileStream;
begin
  try
    FStream := TFileStream.Create(AFile, fmOpenRead);
    try
      Result := FStream.Size;
    finally
      FreeAndNil(FStream);
    end;
  except
    Result := 0;
  end;
end;

function FTPBase: string;
begin
  result := AddBackSlash( GetEntry('FTP Folder') );
end;

procedure TfMain.IdFTPServer1ChangeDirectory(ASender: TIdFTPServerContext;
  var VDirectory: string);
begin
  ASender.CurrentDir := VDirectory;
  // FTPLog( ' change folder to '+ VDirectory );
end;

procedure TfMain.IdFTPServer1GetFileSize(ASender: TIdFTPServerContext;
  const AFilename: string; var VFileSize: Int64);
var
  FN,LFile : String;
begin
  LFile := ReplaceChars( FTPBase + AFilename );
  try
  If uFileExists(LFile) then
    VFileSize :=  GetSizeOfFile(LFile)
  else
    VFileSize := 0;
  except
    VFileSize := 0;
  end;
  // FTPLog( 'FTP get filesize '+LFile+' '+IntToStr(VFileSize));
end;

procedure TfMain.IdFTPServer1ListDirectory(ASender: TIdFTPServerContext;
  const APath: string; ADirectoryListing: TIdFTPListOutput; const ACmd,
  ASwitches: string);
var
  LFTPItem: TIdFTPListItem;
  SRI : Integer;
  SR: tSearchRec;
  P: string;
begin
   ADirectoryListing.DirFormat := doUnix;
   P := FTPBase + APath + '\*.*';
   P := ReplaceChars( P );
   // FTPLog( 'FTP list directory '+P );
   SRI := FindFirst( P, faAnyFile - faHidden - faSysFile, SR);
   While SRI = 0 do
   begin
     LFTPItem := ADirectoryListing.Add;
     LFTPItem.FileName := SR.Name;
     LFTPItem.Size := SR.Size;
     LFTPItem.ModifiedDate := FileDateToDateTime(SR.Time);
     if SR.Attr = faDirectory then
       LFTPItem.ItemType   := ditDirectory
     else
       LFTPItem.ItemType   := ditFile;
     SRI := FindNext(SR);
   end;
   FindClose(SR);
end;

procedure TfMain.IdFTPServer1RetrieveFile(ASender: TIdFTPServerContext;
  const AFileName: string; var VStream: TStream);
var
  FN: string;
begin
  FN := ReplaceChars( FTPBase + AFilename );
  FTPLog( 'retrieve file '+ExtractFilename(FN) );
  VStream := TFileStream.Create(FN,fmOpenRead);
end;

procedure TfMain.IdFTPServer1UserLogin(ASender: TIdFTPServerContext;
  const AUsername, APassword: string; var AAuthenticated: Boolean);
var
  S,T,U,P: string;
begin
  S := GetEntry('FTP Users');
  while S<>'' do
  begin
    T := TokenParse( S, ';' );
    U := TokenParse( T, ',' );
    P := T;
    if (U=AUserName) AND (P=APassword) then
    begin
      // We know them!
      AAuthenticated := True;
      FTPLog('Login attempt by user "'+AUserName+'" was succesful.');
      exit;
    end;
  end;
  // No one that we know.
  AAuthenticated := FALSE;
  FTPLog('Login attempt by user "'+AUserName+'" failed.');
end;

procedure TfMain.btRCapLaunchClick(Sender: TObject);
var
  CMD, RCaptureFN: string;
begin
  RCaptureFN := GetEntry('Raw Folder') + 'rcapture.exe';
  CMD := '-d'+GetEntry('Reciever IP:Port')+' -n60 -l'+GetEntry('Station Name');
  Log(' @Executing '+RCaptureFN+' '+CMD );
  Log( LinkTo( RCaptureFN, ExtractFilePath( RCaptureFN ), CMD ) );
end;

procedure TfMain.cbFTPEnableClick(Sender: TObject);
begin
  IdFTPServer1.Active := cbFTPEnable.Checked;
  FTPLog('FTP Server Active '+BoolToYN(IdFTPServer1.Active));
end;

procedure TfMain.ConvertDATFile( FN: string );
// FN is the RCAP filename
// stdFN is the standardized filename
var
  StartTime: tDateTime;
  stdFN, cfn, rdir, navF, obsF, tlogF, TEQCFN, CMD, TempDir, GZFN: string;
  relPath, ndir,datF,glonavF,sbnavF,galnavF,tnavF,tglonavF,tsbnavF,tgalnavF,tobsF,logF: string;
  exep, nbf: string;
begin
  StartTime := Now;

  Log('[');
  Log('CONVERT DAT/CAP-file: '+FN+' to RINEX' );

  // TEQC and GZIP live in the same folder as our executable
  exep := AddBackSlash( ExtractFilePath(Application.ExeName));

  // Make sure we have a temp folder to work in
  Stat('Make TempDir...');
  TempDir := AddBackSlash( GetEntry('Temp Folder') );
  // Make Sure TempDir exists
  if NOT uDirectoryExists( TempDir ) then
  begin
    Log(' @ Make temp folder '+TempDir );
    uForceDirectories( TempDir  );
  end;

  nbf := AddBackSlash( GetEntry('Raw Backup Folder') );
  // Make Sure native format backup folder exists
  if NOT uDirectoryExists( nbf ) then
  begin
    Log(' @ Make native backup folder '+nbf );
    uForceDirectories( nbf );
  end;
  // Make a backup copy of this raw file now. In case something goes wrong.
  Stat('Make backup copy of Native file at '+nbf);
  FZip( FN, nbf + ExtractFilename(FN) );

  if uFileSize( FN ) < 100 then
  begin
    Log('File '+ExtractFilename(FN)+' is too small to be of value. It has been archived but will not be processed.');
    uDeleteFile( FN );
    exit;
  end;

  // Delete all the files in TempDir
  Stat('Clear TempDir...');
  DeleteAll( TempDir, '*.*' );

  // Make sure the output folder exists
  CAP2stdFN( FN, relPath, stdFN );
  Log(' @ '+ExtractFilename(FN)+' -> ' + relPath + '  ' + stdFN );

  Stat('Make RINEX output folder');
  rdir := AddBackSlash( GetEntry('RINEX Folder') ) + relPath;
  rdir := rdir;
  // Make sure the output folder exists
  if NOT uDirectoryExists( rdir ) then
  begin
    Log(' @ Make RINEX output folder '+rdir);
    uForceDirectories( rdir );
  end;

  Stat('Make NATIVE output folder');
  ndir := AddBackSlash( GetEntry('NATIVE Folder') ) + relPath;
  // Make sure the output folder exists
  if NOT uDirectoryExists( ndir ) then
  begin
    Log(' @ Make NATIVE output folder '+ndir);
    uForceDirectories( ndir );
  end;

  datF := ndir + ExtractFilename( FN );
  navF := rdir + stdFN + 'n';
  glonavF := rdir + stdFN + 'g';
  sbnavF  := rdir + stdFN + 's';
  galnavF := rdir + stdFN + 'l';
  obsF := rdir + stdFN + 'o';

  tnavF := TempDir + stdFN + 'n';
  tglonavF := TempDir + stdFN + 'g';
  tsbnavF  := TempDir + stdFN + 's';
  tgalnavF := TempDir + stdFN + 'l';
  tobsF := TempDir + stdFN + 'o';

  tlogF := ChangeFileExt( tobsF, '.log' );
  logF := ChangeFileExt( obsF, '.log' );

  // Use TEQC to generate observation and navigation files
  Stat(']] Convert DAT file to obs and nav files: '+FN);
  TEQCFN    := exep + 'teqc.exe';

  // Load up CMD string
  CMD       := GetEntry('TEQC Process Command');
  CMD       := StrReplaceNC( CMD, '%of%', tobsF );
  CMD       := StrReplaceNC( CMD, '%nf%', tnavF );
  CMD       := StrReplaceNC( CMD, '%glonf%', tglonavF );
  CMD       := StrReplaceNC( CMD, '%sbnf%', tsbnavF );
  CMD       := StrReplaceNC( CMD, '%galnf%', tgalnavF );
  CMD       := StrReplaceNC( CMD, '%lf%', tlogF );

  // Receiver, Agency Information
  CMD       := StrReplaceNC( CMD, '%an%',  GetEntry('Antenna Number') );
  CMD       := StrReplaceNC( CMD, '%at%',  GetEntry('Antenna Type') );
  CMD       := StrReplaceNC( CMD, '%ag%',  GetEntry('Operating Agency') );
  CMD       := StrReplaceNC( CMD, '%ob%',  GetEntry('Observer') );
  CMD       := StrReplaceNC( CMD, '%cmt%', GetEntry('OBS Comment') );
  CMD       := StrReplaceNC( CMD, '%rsn%', GetEntry('Receiver Serial Number') );
  CMD       := StrReplaceNC( CMD, '%mn%',  GetEntry('Station Name' ) );
  CMD       := StrReplaceNC( CMD, '%rb%',  'iBase'+_BuildNumber );

  CMD := CMD + ' "' + FN + '"';

  Log(' @Executing '+TEQCFN+' '+CMD );
  LinkTo( TEQCFN, ExtractFilePath( TEQCFN ), CMD );
  Stat('Wait for conversion to finish and close files...');
  if NOT (FileExistsClosed( tnavF, 15, 0 ) AND
          FileExistsClosed( tobsF, 15, 0 ) ) then
  begin
    Log('!! Error! TEQC output files were not created within allocated time.');
    exit;
  end;

  // Zip the files
  Delay( 0.25 );
  FZip( tobsF, obsF );
  FZip( tnavF, navF );
  FZip( tglonavF, glonavF );
  FZip( tsbnavF, sbnavF );
  FZip( tgalnavF, galnavF );
  FZip( tLogF, LogF );
  // Zip and archive the native file
  if FZip( FN, datF ) then
  begin
    Log(' Deleting native source file '+FN );
    uDeleteFile( FN );
  end;

  Log('CONVERT complete '+FN+' in '+ExtToStrComma(DeltaTimeSeconds(StartTime),2)+' seconds.');
  Log(']');
  Log('');

end;

procedure TfMain.Exit1Click(Sender: TObject);
// Main Menu option "File: Exit"
begin
  if mrYES = MessageDlg( 'Are you sure you want to abort any running processes and exit the iBase program?',
                        mtConfirmation, [mbYES,mbNO], 0 ) then
  begin
    WantToQuit := TRUE; // Abort current processes
    Close;
  end;
end;

procedure TfMain.ReprocessRawFile1Click(Sender: TObject);
var
  OpnUDlg: tOpenDialog;
begin
  if BUSY then
  begin
    Log('Already busy! Try again later...');
    exit;
  end;
  BUSY := TRUE;
  try
    WantToQuit := FALSE;
    // Reprocess a single file
    OpnUDlg := tOpenDialog.Create( Self );
    try
      OpnUDlg.InitialDir := GetEntry('Raw Folder');
      OpnUDlg.Filename   := '';
      OpnUDlg.Filter     := 'Raw Files|*.dat;*.cap|All Files|*.*';
      OpnUDlg.Title      := 'DAT/CAP File to Convert to RINEX.Z';
      if OpnUDlg.Execute then ConvertDATFile( OpnUDlg.FileName )
    finally
      OpnUDlg.Free;
    end;
  finally
    BUSY := FALSE;
  end;
end;

function FileIsAvailable( FN: string ): boolean;
// returns TRUE if the file is not open by another process.
var
  F: system.Text;
  dTime,fTime: extended;
  SaveFileMode: integer;
  Ch: char;
begin
  result := FALSE; // Assume not ready for reading.
  if NOT uFileExists( FN ) then exit;
  if (uFileSize(FN)<1) then exit;
  // Try to open the file
  fTime := DeltaTimeSeconds( GetFileDateDT( FN ) );
  if fTime > 4.0 then
  begin
    // Try to read the first character of the file. If we can, then
    // we are ready to roll.
    try
      {$I+}
      SaveFileMode := FileMode;
      FileMode := fmOpenReadWrite + fmShareExclusive;
      try
        AssignFile( F, FN );
        ReSet( F );
        try
          Read( F, Ch );
        finally
          CloseFile( F );
        end;
      finally
        FileMode := SaveFileMode;
      end;
      result := TRUE;
      exit;
    except
    end;
  end; // file closed for 0.5 seconds
end;

procedure TfMain.ProcessFilesInFolder( P: string );
// Process any file in the folder P that begins with the letter 'U'
var
  SR: tSearchRec;
  OK: boolean;
  FN,Ext: string;
begin
  P := AddBackSlash( P );
  OK := 0 = FindFirst( P+'*.*', faANYFILE, SR );
  try
    while OK AND (NOT WantToQuit) do
    begin
      if FirstChar( SR.Name ) <> '.' then
      begin
        if (SR.Attr AND faDirectory) > 0 then
        begin
          ProcessFilesInFolder( P+SR.Name );
        end else
        begin
          Ext := UpperCase( ExtractFileExt( SR.Name ) );
          if (Ext = '.DAT') OR (Ext = '.CAP') then
          begin
            FN := P + SR.Name;
            // Is this file closed?
            if FileIsAvailable( FN ) then
            begin
              ConvertDATFile( FN );
            end;
          end;
        end;
      end;
      OK := 0 = FindNext( SR );
    end;
  finally
    FindClose( SR );
  end;
end;

procedure TfMain.ReprocessDirectory1Click(Sender: TObject);
var
  P: string;
begin
  if BUSY then
  begin
    Log('Already busy! Try again later...');
    exit;
  end;
  BUSY := TRUE;
  try
    WantToQuit := FALSE;
    P := GetEntry('Raw Folder');
    if SelectDirectory( 'Path to process for DAT/CAP files', '', P ) then
    begin
      Log('>>>>>> Begin processing path '+P);
      ProcessFilesInFolder( P );
      Log('>>>>>> Processing complete on path '+P);
    end;
  finally
    BUSY := FALSE;
  end;
end;

procedure TfMain.Settings1Click(Sender: TObject);
// Main Menu: "Options: Settings..."
begin
  fOptions.ShowModal;
end;

procedure TfMain.FormDestroy(Sender: TObject);
var
  INIFile: tINIFile;
begin
  // Get screen position
  INIFile := tINIFile.Create( ChangeFileExt( Application.EXEName, '.ini' ));
  try
    INIFile.WriteInteger( 'FORM', 'Left', Left );
    INIFile.WriteInteger( 'FORM', 'Top', Top );
    INIFile.WriteInteger( 'FORM', 'Width', Width );
    INIFile.WriteInteger( 'FORM', 'Height', Height );
  finally
    INIFile.Free;
  end;
  Log('----- iBase ending '+DateTimeToStr(Now)+' ---------------------------------------------------');
end;

procedure TfMain.FormResize(Sender: TObject);
begin
  gbProcessLog.Width := Round( 2 * ClientWidth / 3.0 );
  gbFTPLog.Left := gbProcessLog.Left + gbProcessLog.Width + 2;
  gbFTPLog.Width := ClientWidth - gbFTPLog.Left;
end;

procedure TfMain.FormCreate(Sender: TObject);
begin
  BUSY      := FALSE;
  LastTimer := Now;
end;

procedure TfMain.Timer1Timer(Sender: TObject);
var
  dT: extended;
const
  CheckEvery = 15.0;
begin
  Timer1.Enabled := FALSE;
  try
    dT := DeltaTimeSeconds( LastTimer );
    if NOT cbTimerOK.Checked then exit;   // bail if not enabled
    if dT > CheckEvery then
    begin
      LastTimer := Now;
      // Main Timer, checks receiver status
      if BUSY then
      begin
        Log('Already busy! Try again later...');
        exit;
      end;
      BUSY := TRUE;
      try
        Stat('Check for log file rotation');
        RotateLog;
        Stat('Check for new file...');
        ProcessFilesInFolder( GetEntry('Raw Folder') );
        LastTimer := Now;
      finally
        BUSY := FALSE;
      end;
    end else
    begin
      // Display information for user
      Stat('waiting '+Ext2Str( dT, 3 )+' of '+Ext2Str( CheckEvery, 3 )+' seconds.');
    end;
  finally
    Timer1.Interval := Timer1.Interval;
    Timer1.Enabled  := TRUE;
  end;

end;


end.
