unit plotter;

{$mode objfpc}

interface

uses
  Classes, SysUtils, FileUtil, TAGraph, TASeries, TATransformations,
  TAIntervalSources, TATools, LResources, Forms, Controls, Graphics, Dialogs,
  Grids, LCLType, TypInfo
  , ComCtrls, StdCtrls, ExtCtrls
  , EditBtn
  , Buttons
  , Spin, upascaltz, BCButton
  , TAChartUtils, TADrawUtils, TACustomSeries, TATypes, TAStyles, TACustomSource
  , TASources
  , Types
  , TAGeometry //For PointDistX function
  ;

type

  { TPlotterForm }

  TPlotterForm = class(TForm)
    AstronomicalTwilightCheckBox: TCheckBox;
    PlotEditModifierKeyCombo: TComboBox;
    PlotEditClickKeyCombo: TComboBox;
    DarkestReadingText: TEdit;
    DarkestReadingLabel: TLabel;
    DarkestReadingTime: TEdit;
    EditModeRadios: TRadioGroup;
    ClickLabel: TLabel;
    ModifierLabel: TLabel;
    ChartToolset1DataPointClickTool2: TDataPointClickTool;
    PendingDeletionMemo: TMemo;
    PreviewButton: TCheckBox;
    RecordNumberLabel: TLabel;
    RecordNumberText: TEdit;
    SelectionModeLabel: TLabel;
    SelectionModeIndicator: TShape;
    ExportButton: TButton;
    CivilTwilightCheckBox: TCheckBox;
    ClearAllButton: TButton;
    ClearAllButton1: TButton;
    ClearSelButton: TButton;
    ClipSunMoonCheckBox: TCheckBox;
    ColorSwatch: TShape;
    ContinuousLineCheckBox: TCheckBox;
    PlotterModelLabel: TLabel;
    DarknessCheckBox: TCheckBox;
    DayUpDown: TUpDown;
    DisplayGroup: TGroupBox;
    dLabel: TLabel;
    EditPositionButton: TBitBtn;
    FileSelectionModeGroupBox: TGroupBox;
    FileSelectionModeRadioGroup: TRadioGroup;
    FilesGroupBox: TGroupBox;
    FilterComboBox: TComboBox;
    FilterLabel: TLabel;
    GridCheckBox: TCheckBox;
    hLabel: TLabel;
    HourUpDown: TUpDown;
    MoonText: TEdit;
    MoonTextLabel: TLabel;
    PlotterModelText: TEdit;
    PlotterModelType: TEdit;
    PlotterSN: TEdit;
    readingstextB: TEdit;
    readingstextC: TEdit;
    readingstextG: TEdit;
    readingstextR: TEdit;
    ReadingText: TEdit;
    ReadingTime: TEdit;
    ReadingTimeLabel: TLabel;
    Memo1: TMemo;
    MinuteUpDown: TUpDown;
    MiscGroupBox: TGroupBox;
    mLabel: TLabel;
    MoonCheckBox: TCheckBox;
    MultiFileStringGrid: TStringGrid;
    NauticalTwilightCheckBox: TCheckBox;
    PageControl1: TPageControl;
    PlotDirectoryButton: TBitBtn;
    PlotDirectoryEdit: TEdit;
    PlotFilesStringGrid: TStringGrid;
    PlotFileTimezoneLabel: TLabeledEdit;
    PlotNumberGroup: TRadioGroup;
    ListChartSource1: TListChartSource;
    PositionEntry: TLabeledEdit;
    PositionGroupBox: TGroupBox;
    ReadingTextLabel: TLabel;
    ChartToolset1DataPointClickTool1: TDataPointClickTool;
    RecordText: TEdit;
    RefreshDirectoryButton: TButton;
    RegionLabel: TLabel;
    ReplotButton: TButton;
    ResetToPlotDirectoryButton: TBitBtn;
    ReZoomButton: TBitBtn;
    ExportDialog: TSaveDialog;
    SavePNGButton: TBitBtn;
    SaveSVGButton: TBitBtn;
    SettingsGroupBox: TGroupBox;
    SettingsPanel: TPanel;
    sLabel: TLabel;
    SunCheckBox: TCheckBox;
    SunDiffChartAxisTransformations: TChartAxisTransformations;
    SunriseDifferenceLabel: TLabel;
    SunriseDifferenceSeries: TLineSeries;

    ListChartSource1Backup: TListChartSource;

    SunDiffChartAxisTransformationsAutoScaleAxisTransform1: TAutoScaleAxisTransform;
    SunriseDifferenceText: TEdit;
    SunText: TEdit;
    SunTextLabel: TLabel;
    SunTwilightCheckBox: TCheckBox;
    FilesTabSheet: TTabSheet;
    SettingsTabSheet: TTabSheet;
    EditTabSheet: TTabSheet;
    HelpTabSheet: TTabSheet;
    TemperatureCheckBox: TCheckBox;
    TemperatureReadingText: TEdit;
    TemperatureReadingTextLabel: TLabel;
    ThreeDayCheckBox: TCheckBox;
    TimeOffsetGroup: TGroupBox;
    TimeOffsetSpinEdit: TSpinEdit;
    TimeRefGroup: TRadioGroup;
    TimezoneLabel: TLabel;
    TwilightGroup: TGroupBox;
    ControlButtonPanel: TPanel;
    ControlPanel: TPanel;
    ControlPanelButton: TButton;
    ScrollBox1: TScrollBox;
    SnowSeries: TLineSeries;


    SunElevationChartSource: TListChartSource;
    BlueSeries: TLineSeries;

    TemperatureChartAxisTransformationsAutoScaleAxisTransform1: TAutoScaleAxisTransform;

    MPSASChartAxisTransformationsAutoScaleAxisTransform1: TAutoScaleAxisTransform;
    MPSASChartAxisTransformationsLinearAxisTransform1: TLinearAxisTransform;

    SunMoonChartAxisTransformationsAutoScaleAxisTransform1: TAutoScaleAxisTransform;
    TemperatureChartAxisTransformations: TChartAxisTransformations;
    MPSASChartAxisTransformations: TChartAxisTransformations;
    Chart1: TChart;
    AstronmicalTwilightSeries: TLineSeries;
    SunTwilightSeries: TLineSeries;
    NauticalTwilightSeries: TLineSeries;
    TZLocationBox: TComboBox;
    TZRegionBox: TComboBox;
    VoltageChartAxisTransformations: TChartAxisTransformations;
    CivilTwilightSeries: TLineSeries;
    SunSeries: TLineSeries;
    ClearSeries: TLineSeries;
    GreenSeries: TLineSeries;
    MoonSeries: TLineSeries;
    MPSASSeries: TLineSeries;
    MPSASSeries2: TLineSeries;
    MPSASSeries3: TLineSeries;

    VoltageChartAxisTransformationsAutoScaleAxisTransform1: TAutoScaleAxisTransform;
    VoltageCheckBox: TCheckBox;
    VoltageText: TEdit;
    VoltageTextLabel: TLabel;
    WeekUpDown: TUpDown;
    wLabel: TLabel;
    WorkingPanel: TPanel;
    ChartPanel: TPanel;
    RedSeries: TLineSeries;
    ChartToolset1ZoomDragTool1: TZoomDragTool;
    ChartToolset1: TChartToolset;
    ChartToolset1DataPointCrosshairTool1: TDataPointCrosshairTool;
    ChartToolset1PanDragTool1: TPanDragTool;
    ChartToolset1ZoomMouseWheelTool1: TZoomMouseWheelTool;
    DateTimeIntervalChartSource1: TDateTimeIntervalChartSource;
    SunMoonChartAxisTransformations: TChartAxisTransformations;
    ZeroPlotCheckBox: TCheckBox;
    procedure AstronomicalTwilightCheckBoxEditingDone(Sender: TObject);
    procedure ChartToolset1DataPointClickTool1PointClick(ATool: TChartTool;
      APoint: TPoint);
    procedure ChartToolset1DataPointClickTool2AfterMouseMove(ATool: TChartTool;
      APoint: TPoint);
    procedure FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure PlotEditModifierKeyComboChange(Sender: TObject);
    procedure PlotEditClickKeyComboChange(Sender: TObject);
    procedure DayUpDownClick(Sender: TObject; Button: TUDBtnType);
    procedure ControlPanelButtonClick(Sender: TObject);
    procedure ChartToolset1PanDragTool1AfterMouseUp(ATool: TChartTool;
      APoint: TPoint);
    procedure ExportButtonClick(Sender: TObject);
    procedure HourUpDownClick(Sender: TObject; Button: TUDBtnType);
    procedure MinuteUpDownClick(Sender: TObject; Button: TUDBtnType);
    procedure PageControl1Change(Sender: TObject);
    procedure PreviewButtonChange(Sender: TObject);
    procedure RefreshDirectoryButtonClick(Sender: TObject);
    procedure TimeOffsetSpinEditChange(Sender: TObject);
    procedure TwilightSeriesGetPointerStyle(ASender: TChartSeries;
      AValueIndex: Integer; var AStyle: TSeriesPointerStyle);
    procedure ClipSunMoonCheckBoxEditingDone(Sender: TObject);
    procedure CivilTwilightCheckBoxEditingDone(Sender: TObject);
    procedure ContinuousLineCheckBoxEditingDone(Sender: TObject);
    procedure DarknessCheckBoxEditingDone(Sender: TObject);
    procedure NauticalTwilightCheckBoxEditingDone(Sender: TObject);
    procedure SavePNGButtonClick(Sender: TObject);
    procedure SaveSVGButtonClick(Sender: TObject);
    procedure FileSelectionModeRadioGroupClick(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure GridCheckBoxEditingDone(Sender: TObject);
    procedure MoonCheckBoxEditingDone(Sender: TObject);
    procedure MultiFileStringGridButtonClick(Sender: TObject; aCol,
      aRow: Integer);
    procedure FormResize(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure MPSASSeriesGetPointerStyle(ASender: TChartSeries;
      AValueIndex: Integer; var AStyle: TSeriesPointerStyle);
    procedure PlotDirectoryEditEditingDone(Sender: TObject);
    procedure ReZoomButtonClick(Sender: TObject);
    procedure ClearSelButtonClick(Sender: TObject);
    procedure FilterComboBoxChange(Sender: TObject);
    procedure PlotDirectoryButtonClick(Sender: TObject);
    procedure ReplotButtonClick(Sender: TObject);
    procedure ResetToPlotDirectoryButtonClick(Sender: TObject);
    procedure ChartToolset1DataPointCrosshairTool1Draw(
      ASender: TDataPointDrawTool);
    procedure EditPositionButtonClick(Sender: TObject);
    //procedure ChartButtonClick(Sender: TObject);
    procedure ClearAllButtonClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    //procedure SettingsButtonClick(Sender: TObject);
    procedure PlotFilesStringGridClick(Sender: TObject);
    procedure PlotFilesStringGridCompareCells(Sender: TObject; ACol, ARow, BCol,
      BRow: Integer; var Result: integer);
    procedure PlotFilesStringGridHeaderClick(Sender: TObject; IsColumn: Boolean;
      Index: Integer);
    procedure SunCheckBoxEditingDone(Sender: TObject);
    procedure SunTwilightCheckBoxEditingDone(Sender: TObject);
    procedure TemperatureCheckBoxEditingDone(Sender: TObject);
    procedure TimeRefGroupClick(Sender: TObject);
    //procedure VerticalSplitterMoved(Sender: TObject);
    //procedure ThreeDayCheckBoxClick(Sender: TObject); {!!! maybe keep this !!!}
    procedure TZLocationBoxChange(Sender: TObject);
    procedure TZRegionBoxChange(Sender: TObject);
    procedure VoltageCheckBoxEditingDone(Sender: TObject);
    procedure WeekUpDownClick(Sender: TObject; Button: TUDBtnType);
    procedure ZeroPlotCheckBoxEditingDone(Sender: TObject);
  private
    { private declarations }
    procedure ReadINI();
    procedure GetCustomizations();
    procedure PlotFile(PlotFileName: String);
    procedure AddFilesToList(FilePathName : String);
    procedure FileSelect();
    Procedure FillTimezones();
    procedure SaveChart(FileType:String);
    procedure TemperatureSeriesGetPointerStyle(ASender: TChartSeries;
      AValueIndex: Integer; var AStyle: TSeriesPointerStyle);
    procedure VoltSeriesGetPointerStyle(ASender: TChartSeries; AValueIndex: Integer; var AStyle: TSeriesPointerStyle);
    procedure PlotterEditKeyShift();
    procedure UpdateEditChart();
  public
    { public declarations }

  end;

  { Declarations outside the form }
  function FileSizeFormat(bytes:Double):string;
  function AddBackSlash(Instring:String):String;

  function DialogCentered(const aCaption: String;
                    const aMsg: AnsiString;
                    DlgType: TMsgDlgType;
                    Buttons: TMsgDlgButtons;
                    HelpCtx: longint;
                    Form: TForm
                    ): TModalResult;

  const
  DefaultSettingsWidth = 475;
  DefaultSettingsheight = 210;

  TwilightMax = 90;
  TwilightMin = -90;

var
  PlotterForm: TPlotterForm;

  MyLatitude: extended = 0.0; //Latitude
  MyLongitude: extended = 0.0; //Longitude
  MyElevation: extended = 0.0; //Elevation

  PlotterINIsection: String;
  AZones: TStringList; //To hold timezone names for display.
  ptzAll :TPascalTZ; // All regions for conversions.
  ptzRegion :TPascalTZ; //One region for user selection of timezones.
  subfix: ansistring; //Used for time zone conversions

  CurrentDirectory: String;
  PlotterModel:Integer;
  PlotType:String=''; //options: Color, empty for standard.

  LastRedDarkness: Real = 0.0;
  LastGreenDarkness: Real = 0.0;
  LastBlueDarkness: Real = 0.0;
  LastClearDarkness: Real = 0.0;

  FullChart:Boolean = False;

  NulPointerStyle:TSeriesPointerStyle;

  SplitterPosLeft: Integer;
  PlotterTZRegion, PlotterTZLocation:String; //Only used for plotter
  DefaultTZLocation: String;
  MeterTZRegion, MeterTZLocation:String; //Selected meter location for comparison to plotter location
  SelectedSerialNumberString:String; //Meter serial number pulled from .dat file
  TZChanging:Boolean=False;//Indicates that programmatic changes are taking place to the Time Zone

  VoltSeries: TLineSeries;
  VoltSeriesBackup: TLineSeries;
  TemperatureSeries: TLineSeries;
  TemperatureSeriesBackup: TLineSeries;
  MultiChart : array[0..9] of TLineSeries;

  GridVisible:Boolean;
  SunTwilightVisible, CivilTwilightVisible, NauticalTwilightVisible, AstronomicalTwilightVisible :Boolean;
  DarknessVisible:Boolean;
  VoltageVisible:Boolean;
  TemperatureVisible:Boolean;
  SunVisible:Boolean;
  MoonVisible:Boolean;
  SunriseDiffVisible:Boolean;
  TimeReference: String; {UTC or Local setting.}
  PositionUsable: Boolean;
  ClipSunMoon: Boolean; {Clip plotting of Sun/Moon below 18°.}
  ContinuousLine: Boolean; {Continuous plot line without timed separations.}
  ZeroPlot: Boolean; {Plot the lines when readings are zero.}

  CursorTimeFormat: TFormatSettings; //Cursor time format settings
  FileSelectionMode: String;

  MultiIndex:Integer; //Index to open place in multiple file grid row

  SelectedFilename: String; //The filename used on the plottor form title bar

  RecordType: Integer = 0; //0=Initial or 1=subsequent

  MinLocalRecordTime : TDateTime = 0;
  MaxLocalRecordTime : TDateTime = 0;
  MaxLocalRecordDate : TDateTime;
  MinPlotTime, MaxPlotTime: TDateTime;

  TimeOffset: Int64; //Time offset for plotting

  HeaderStrings: TStringList; // Holds the header part of a records file.
  PendingDeletionMode: Boolean = False; //Flad to indicate pending deletion editing mode.
  PointedToRecord:Integer = 0;
  StartPendingDeletionIndex: Integer = 0;
  EndPendingDeletionIndex: Integer = 0;
  LastStartPendingDeletionIndex: Integer = 0;
  LastEndPendingDeletionIndex: Integer = 0;
  symbolStartDelete: TSeriesPointerStyle = psCircle;
  symbolPendingDelete: TSeriesPointerStyle = psDiagCross;
  symbolKeep: TSeriesPointerStyle = psDiamond;
  PreviewMode:Boolean = False;
  MPSASSeriesBackup: TLineSeries;


implementation
uses
  appsettings
  , dateutils //Required to convert logged UTC string to TDateTime
  , strutils //Required for checking lines in conversion file.
  , moon //required for Moon calculations
  , Math //for float identifier
  , dlheader //For Timezone conversions.
  , LazFileUtils //Necessary for filename extraction
  , worldmap
  , header_utils //for statusmessage
  , Unit1
  , TADrawerSVG // for saving SVG of chart
  , FPCanvas //needed for pen styles
  , uPascalTZ_Types //For some time zone constants
  ;

var
    UpdatingGUI:Boolean = False;

{ TPlotterForm }

procedure TPlotterForm.MPSASSeriesGetPointerStyle(ASender: TChartSeries; AValueIndex: Integer; var AStyle: TSeriesPointerStyle);
var
  symbol: Double;
begin
  { White color pointers indicate an initial reading.
    Other color pointers indicate a subsequent reading.
    SVG saving is much smaller with no subsequent pointer (psNone), and the plot looks cleaner.}

 // Was working pretty good before 20260327, and preview copying of mpsasseries
  symbol := MPSASSeries.XValues[AValueIndex, 1];
  AStyle := TSeriesPointerStyle(round(symbol));{ TODO : seems to cause a problem in win64 with strange style value. }

end;

procedure TPlotterForm.VoltSeriesGetPointerStyle(ASender: TChartSeries; AValueIndex: Integer; var AStyle: TSeriesPointerStyle);
begin
    { White color pointers indicate an initial reading.
      Other color pointers indicate a subsequent reading.
      SVG saving is much smaller with no subsequent pointer (psNone), and the plot looks cleaner.}
    if ASender.GetColor(AValueIndex)=clWhite then
      AStyle:=psRectangle
    else
      AStyle:=psNone;
end;

procedure TPlotterForm.TemperatureSeriesGetPointerStyle(ASender: TChartSeries;
  AValueIndex: Integer; var AStyle: TSeriesPointerStyle);
begin
  { White color pointers indicate an initial reading.
    Other color pointers indicate a subsequent reading.
    SVG saving is much smaller with no subsequent pointer (psNone), and the plot looks cleaner.}
  if ASender.GetColor(AValueIndex)=clWhite then
    AStyle:=psRectangle
  else
    AStyle:=psNone;
end;

procedure TPlotterForm.TwilightSeriesGetPointerStyle(
  ASender: TChartSeries; AValueIndex: Integer; var AStyle: TSeriesPointerStyle);
begin
  if ASender.GetYValue(AValueIndex)=TwilightMax then
     AStyle:=psDownTriangle
  else
      AStyle:=psTriangle;
end;

procedure TPlotterForm.FormCreate(Sender: TObject);
var
  W,H: Integer; { Temporary width and height variables. }
  PlotEditKeysResult: TShiftState = [];
  S: String;

begin
  UpdatingGUI:=True; //prevent change events from updating while programmatic changes are being done.

  { Do not show any space for multiple files until they are added. }
  MultiFileStringGrid.RowCount:=0;

  { Initialize required variables. }

  { Fill regions }
  TZRegionBox.Items.AddStrings(TZ_FILES_STANDARD);

  AZones:=TStringList.Create;

  ptzAll := TPascalTZ.Create();
  ptzAll.ParseDatabaseFromDirectory(appsettings.TZDirectory); //Get timezones
  ptzAll.DetectInvalidLocalTimes:=False;   //Prevent errors of "time does not exist in"

  ptzRegion := TPascalTZ.Create();
  ptzRegion.DetectInvalidLocalTimes:=False;   //Prevent errors of "time does not exist in"

  ReadINI();

  PlotterForm.Position:=poScreenCenter;

  { Restore size of panel. }
  { Read in stored width and height. }
  W :=StrToIntDef(vConfigurations.ReadString(PlotterINIsection,'PanelWidth'),0);
  H :=StrToIntDef(vConfigurations.ReadString(PlotterINIsection,'PanelHeight'),0);

  if ((W=0) or (H=0)) then
    { Maximize if no stored settings. }
    PlotterForm.WindowState:=wsMaximized
  else begin
   { check for oversize, then restore valid or saved sizes.}
      if (W>Screen.Width) then PlotterForm.Width :=Screen.Width  else PlotterForm.Width :=W;
      if (H>Screen.Height) then PlotterForm.Height:=Screen.Height else PlotterForm.Height:=H;
   //if (W>1000) then PlotterForm.Width :=1000 else PlotterForm.Width :=W;
   //if (H>1000) then PlotterForm.Height:=1000 else PlotterForm.Height:=H;
  end;

  { Set up chart timestamps format. }
  DateTimeIntervalChartSource1.DateTimeFormat:='hh:mm'+#13#10+'DD-mmm'+#13#10+'yyyy';

  { Get Plot Edit Key Modifiers }
  S:= vConfigurations.ReadString(PlotterINIsection,'PlotEditModifiers','');
  PlotEditModifierKeyCombo.ItemIndex:=0; //Assume None shift modifier
  if Pos('ssMeta', S) > 0 then begin Include(PlotEditKeysResult, ssMeta); PlotEditModifierKeyCombo.ItemIndex:=1; end;
  if Pos('ssShift', S) > 0 then begin Include(PlotEditKeysResult, ssShift); PlotEditModifierKeyCombo.ItemIndex:=2; end;
  if Pos('ssCtrl', S) > 0 then begin Include(PlotEditKeysResult, ssCtrl); PlotEditModifierKeyCombo.ItemIndex:=3; end;
  if Pos('ssAlt', S) > 0 then begin Include(PlotEditKeysResult, ssAlt); PlotEditModifierKeyCombo.ItemIndex:=4; end;
  if Pos('ssLeft', S) > 0 then begin Include(PlotEditKeysResult, ssLeft); PlotEditClickKeyCombo.ItemIndex:=0; end;
  if Pos('ssMiddle', S) > 0 then begin Include(PlotEditKeysResult, ssMiddle); PlotEditClickKeyCombo.ItemIndex:=1; end;
  if Pos('ssRight', S) > 0 then begin Include(PlotEditKeysResult, ssRight); PlotEditClickKeyCombo.ItemIndex:=2; end;

  ChartToolset1DataPointClickTool1.Shift:=PlotEditKeysResult;


  { Get File Selection Mode setting. }
  FileSelectionMode:=vConfigurations.ReadString(PlotterINIsection,'FileSelectionMode','Single');
  case FileSelectionMode of
    'Accumulate': begin
       FileSelectionModeRadioGroup.ItemIndex:=1;
       MultiFileStringGrid.Visible:=False;
    end;
    'Multiple': begin
       FileSelectionModeRadioGroup.ItemIndex:=2;
       MultiFileStringGrid.Visible:=True;
    end;
    else begin //Default to Single mode
       FileSelectionModeRadioGroup.ItemIndex:=0;
       MultiFileStringGrid.Visible:=False;
    end;
  end;

  { Get Time reference setting. }
  TimeReference:=vConfigurations.ReadString(PlotterINIsection,'TimeReference','');
  { Change chart legend accordingly. }
  case TimeReference of
    'Local': begin
               TimeRefGroup.ItemIndex:=1;
               Chart1.AxisList[0].Title.Caption:='Sample time (Local)';
             end
    else begin
            TimeRefGroup.ItemIndex:=0;
            Chart1.AxisList[0].Title.Caption:='Sample time (UTC)';
         end;
  end;

  { Get chart grid setting}
  GridVisible:=vConfigurations.ReadBool(PlotterINIsection,'Grid',True);
  GridCheckBox.Checked:=GridVisible;
  Chart1.BottomAxis.Grid.Visible:=GridVisible;
  Chart1.LeftAxis.Grid.Visible:=GridVisible;

  {Get twilight lines visibility}
  SunTwilightVisible:=vConfigurations.ReadBool(PlotterINIsection,'SunTwilight',True);
  CivilTwilightVisible:=vConfigurations.ReadBool(PlotterINIsection,'CivilTwilight',True);
  NauticalTwilightVisible:=vConfigurations.ReadBool(PlotterINIsection,'NauticalTwilight',True);
  AstronomicalTwilightVisible:=vConfigurations.ReadBool(PlotterINIsection,'AstronomicalTwilight',True);

  {Set check boxes}
  SunTwilightCheckBox.Checked:=SunTwilightVisible;
  CivilTwilightCheckBox.Checked:=CivilTwilightVisible;
  NauticalTwilightCheckBox.Checked:=CivilTwilightVisible;
  AstronomicalTwilightCheckBox.Checked:=AstronomicalTwilightVisible;

  { Set plotter options}
  if SunTwilightVisible then
    SunTwilightSeries.LinePen.Style:=psSolid
  else
    SunTwilightSeries.LinePen.Style:=psClear;

  if CivilTwilightVisible then
    CivilTwilightSeries.LinePen.Style:=psSolid
  else
    CivilTwilightSeries.LinePen.Style:=psClear;

  if NauticalTwilightVisible then
    NauticalTwilightSeries.LinePen.Style:=psSolid
  else
    NauticalTwilightSeries.LinePen.Style:=psClear;

  if AstronomicalTwilightVisible then
    AstronmicalTwilightSeries.LinePen.Style:=psSolid
  else
    AstronmicalTwilightSeries.LinePen.Style:=psClear;
  {Finished getting twilight lines visibility}

  {Get setting to clip Sun/Moon plots below 18°}
  ClipSunMoon:=vConfigurations.ReadBool(PlotterINIsection,'ClipSunMoon',False);
  ClipSunMoonCheckBox.Checked:=ClipSunMoon;

  {Get setting to clip Sun/Moon plots below 18°}
  ContinuousLine:=vConfigurations.ReadBool(PlotterINIsection,'ContinuousLine',False);
  ContinuousLineCheckBox.Checked:=ContinuousLine;

  {Get setting for plotting to zero readings.}
  ZeroPlot:=vConfigurations.ReadBool(PlotterINIsection,'ZeroPlot',False);
  ZeroPlotCheckBox.Checked:=ZeroPlot;

  { Get Darkness visibility setting}
  DarknessVisible:=vConfigurations.ReadBool(PlotterINIsection,'DarknessVisible',True);
  DarknessCheckBox.Checked:=DarknessVisible;
  ReadingText.Visible:=DarknessVisible;
  ReadingTextLabel.Visible:=DarknessVisible;
  if DarknessVisible then begin
    MPSASSeries.LinePen.Style:=psSolid;
    MPSASSeries2.LinePen.Style:=psSolid;
    MPSASSeries3.LinePen.Style:=psSolid;
  end
  else begin
    MPSASSeries.LinePen.Style:=psClear;
    MPSASSeries2.LinePen.Style:=psClear;
    MPSASSeries3.LinePen.Style:=psClear;
  end;

  { Allow for extra data like pointerstyle in each record}
  MPSASSeries.ListSource.XCount:=2;
  ListChartSource1.XCount:=2;

  { Get Sun visibility setting}
  SunVisible:=vConfigurations.ReadBool(PlotterINIsection,'Sun',True);
  SunCheckBox.Checked:=SunVisible;
  if SunVisible then
    SunSeries.LinePen.Style:=psSolid
  else
    SunSeries.LinePen.Style:=psClear;
  SunText.Visible:=SunVisible;
  SunTextLabel.Visible:=SunVisible;

  { Get Moon visibility setting}
  MoonVisible:=vConfigurations.ReadBool(PlotterINIsection,'Moon',True);
  MoonCheckBox.Checked:=MoonVisible;
  if MoonVisible then
    MoonSeries.LinePen.Style:=psSolid
  else
    MoonSeries.LinePen.Style:=psClear;
  MoonText.Visible:=MoonVisible;
  MoonTextLabel.Visible:=MoonVisible;

  Chart1.AxisList[2].Visible:= (SunVisible or MoonVisible);

  { Add Axis }

  {Add fixed series here. }

  {Backup series}
  MPSASSeriesBackup:= TLineSeries.Create(nil);
  MPSASSeriesBackup.ListSource.XCount:=2;
  ListChartSource1Backup:= TListChartSource.Create(Self);
  ListChartSource1Backup.XCount:=2;

  { Voltage for DL records. }
  VoltSeries := TLineSeries.Create(self);
  VoltSeries.SeriesColor:=clBlue;
  VoltSeries.LinePen.Width:=2;
  VoltSeries.Title:='Voltage';
  VoltSeries.AxisIndexY:=3; //Voltage axis
  VoltSeries.Pointer.HorizSize:=2;
  VoltSeries.Pointer.VertSize:=2;
  VoltSeries.Pointer.Pen.Color:=clBlue;
  VoltSeries.Pointer.Visible:=True;
  VoltSeries.OnGetPointerStyle:=@VoltSeriesGetPointerStyle;
  Chart1.AddSeries(VoltSeries);

  { Get chart voltage visibilitysetting}
  VoltageVisible:=vConfigurations.ReadBool(PlotterINIsection,'Voltage',True);
  VoltageCheckBox.Checked:=VoltageVisible;
  if VoltageVisible then begin
    VoltSeries.LinePen.Style:=psSolid;
    Chart1.AxisList[3].Visible:=True;
  end
  else begin
    VoltSeries.LinePen.Style:=psClear;
    Chart1.AxisList[3].Visible:=False;
  end;
  VoltageText.Visible:=VoltageVisible;
  VoltageTextLabel.Visible:=VoltageVisible;

  VoltSeriesBackup := TLineSeries.Create(self);


  { Temperature series. }
  TemperatureSeries := TLineSeries.Create(self);
  TemperatureSeries.SeriesColor:=$00008000;//was clLime until 20190707;
  TemperatureSeries.LinePen.Width:=2;
  TemperatureSeries.Title:='Temperature';
  TemperatureSeries.AxisIndexY:=4; //Temperature axis
  TemperatureSeries.Pointer.HorizSize:=2;
  TemperatureSeries.Pointer.VertSize:=2;
  TemperatureSeries.Pointer.Pen.Color:=$00008000;
  TemperatureSeries.Pointer.Visible:=True;
  TemperatureSeries.OnGetPointerStyle:=@TemperatureSeriesGetPointerStyle;
  Chart1.AddSeries(TemperatureSeries);
  Chart1.AxisList[4].Marks.LabelFont.Color:=TemperatureSeries.SeriesColor;
  Chart1.AxisList[4].Title.LabelFont.Color:=TemperatureSeries.SeriesColor;

  TemperatureSeriesBackup := TLineSeries.Create(self);

  { Get chart Temperature visibilitysetting}
  TemperatureVisible:=vConfigurations.ReadBool(PlotterINIsection,'Temperature',True);
  TemperatureCheckBox.Checked:=TemperatureVisible;
  if TemperatureVisible then begin
    TemperatureSeries.LinePen.Style:=psSolid;
    Chart1.AxisList[4].Visible:=True;
  end
  else begin
    TemperatureSeries.LinePen.Style:=psClear;
    Chart1.AxisList[4].Visible:=False;
  end;

  TemperatureReadingText.Visible:=TemperatureVisible;
  TemperatureReadingTextLabel.Visible:=TemperatureVisible;

  {Edit panel}
  PendingDeletionMemo.Lines.Add('Start selection: Click once near first record of selection.');

  {Show Files Panel}
  PageControl1.ActivePageIndex:=0;

  UpdatingGUI:=False; //Allow change events to update


  { Update chart }
  Chart1.Invalidate;  {Refresh plot}

end;

//procedure TPlotterForm.SettingsButtonClick(Sender: TObject);
//begin
//  if (FullChart) then begin
//    OptionsPanel.Visible:=True;
//    VerticalSplitter.Left:=SplitterPosLeft;
//    ChartButton.Down:=False;
//    FullChart:=False;
//  end;
//
//  SettingsButton.down:=True;
//
//end;

//procedure TPlotterForm.ChartButtonClick(Sender: TObject);
//begin
//  if (not FullChart) then begin
//    OptionsPanel.Visible:=False;
//    {Save splitter position}
//    SplitterPosLeft:=VerticalSplitter.Left;
//    VerticalSplitter.Left:=0;
//    SettingsButton.down:=False;
//    FullChart:=True;
//  end;
//
//  ChartButton.Down:=True;
//
//end;

procedure TPlotterForm.EditPositionButtonClick(Sender: TObject);
begin
  worldmap.FormWorldmap.WorldmapShow('Plotter', PositionEntry.Text );
end;

{Clear plot
  - All series must be cleared to clear bottom axis range.}
procedure TPlotterForm.ClearAllButtonClick(Sender: TObject);
var
  i: Integer = 0;
begin
 MPSASSeries.Clear;
 VoltSeries.Clear;
 TemperatureSeries.Clear;
 MPSASSeries2.Clear;
 MPSASSeries3.Clear;
 RedSeries.Clear;
 GreenSeries.Clear;
 BlueSeries.Clear;
 ClearSeries.Clear;
 SunSeries.Clear;
 MoonSeries.Clear;
 SunTwilightSeries.Clear;
 CivilTwilightSeries.Clear;
 AstronmicalTwilightSeries.Clear;
 NauticalTwilightSeries.Clear;

 { Clear multiple plots }
 for i:= 0 to Length(MultiChart)-1 do begin
   Chart1.DeleteSeries(MultiChart[i]);
 end;
 MultiFileStringGrid.Clear;

 MinLocalRecordTime := 0;
 MaxLocalRecordTime := 0;

end;

procedure TPlotterForm.ReZoomButtonClick(Sender: TObject);
begin
  Chart1.ZoomFull();
end;

procedure TPlotterForm.FormResize(Sender: TObject);
begin
  { Save width and height, }
  vConfigurations.WriteString(PlotterINIsection,'PanelWidth',IntToStr(PlotterForm.Width));
  vConfigurations.WriteString(PlotterINIsection,'PanelHeight',IntToStr(PlotterForm.Height));
end;

procedure TPlotterForm.FileSelectionModeRadioGroupClick(Sender: TObject);
begin
  if not UpdatingGUI then begin

   { Clear plot since new file mode has been selected. }
   ClearAllButtonClick(nil);

   case FileSelectionModeRadioGroup.ItemIndex of
     0: begin
        FileSelectionMode:='Single';
        MultiFileStringGrid.Visible:=False;
        { TODO : Note should read: Select one file ata time to plot. }
     end;
     1: begin
        FileSelectionMode:='Accumulate';
        MultiFileStringGrid.Visible:=False;
        { TODO : Note should read: Select sequential files in chronological order. }
     end;
     2: begin
        FileSelectionMode:='Multiple';
        MultiFileStringGrid.Visible:=True;
        { No note can be seen since the grid is shown in the not place. }
     end;
   end;

   { Save Time reference setting. }
   vConfigurations.WriteString(PlotterINIsection,'FileSelectionMode',FileSelectionMode);

 end;//End of updating check
end;

procedure TPlotterForm.FormDestroy(Sender: TObject);
begin
  if Assigned(AZones) then FreeAndNil(AZones);
  if Assigned(ptzAll) then FreeAndNil(ptzAll);
  if Assigned(ptzRegion) then FreeAndNil(ptzRegion);
  if Assigned(VoltSeries) then FreeAndNil(VoltSeries);
  if Assigned(TemperatureSeries) then FreeAndNil(TemperatureSeries);
end;

procedure TPlotterForm.GridCheckBoxEditingDone(Sender: TObject);
begin
  GridVisible:=GridCheckBox.Checked;
  {Save setting for future}
  vConfigurations.WriteBool(PlotterINIsection,'Grid',GridVisible);

  Chart1.BottomAxis.Grid.Visible:=GridVisible;
  Chart1.LeftAxis.Grid.Visible:=GridVisible;
  Chart1.Invalidate;  {Refresh plot}
end;

procedure TPlotterForm.MoonCheckBoxEditingDone(Sender: TObject);
begin
  if not UpdatingGUI then begin
    MoonVisible:=MoonCheckBox.Checked;
    {Save setting for future}
    vConfigurations.WriteBool(PlotterINIsection,'Moon',MoonVisible);

    if MoonVisible then begin
      MoonSeries.LinePen.Style:=psSolid;
    end else begin
      MoonSeries.LinePen.Style:=psClear;
    end;

   MoonText.Visible:=MoonVisible;
   MoonTextLabel.Visible:=MoonVisible;

    Chart1.AxisList[2].Visible:= (SunVisible or MoonVisible);

  end;
end;

procedure TPlotterForm.MultiFileStringGridButtonClick(Sender: TObject; aCol,
  aRow: Integer);
begin
  //WriteLnDebug(format('button click col=%d, row=%d',[aCol, aRow]));
  //ColorDialog1.Execute;
  //MultiFileStringGrid.Cells[0,0].;
end;

procedure TPlotterForm.FormShow(Sender: TObject);
begin
  PlotFileDirectory:=RemoveMultiSlash(PlotDirectoryEdit.Text);
  AddFilesToList(PlotFileDirectory);

end;


{User has selected or changed the directory of files to choose plotfile from}
procedure TPlotterForm.PlotDirectoryEditEditingDone(Sender: TObject);
begin
  PlotFileDirectory:=RemoveMultiSlash(PlotDirectoryEdit.Text);
  AddFilesToList(PlotFileDirectory);
end;

procedure TPlotterForm.ClearSelButtonClick(Sender: TObject);
begin
  case PlotNumberGroup.ItemIndex of
    0: MPSASSeries.Clear;
    1: MPSASSeries2.Clear;
    2: MPSASSeries3.Clear;
  end;
  SunSeries.Clear;
  MoonSeries.Clear;
  SunTwilightSeries.Clear;
  CivilTwilightSeries.Clear;
  AstronmicalTwilightSeries.Clear;
  NauticalTwilightSeries.Clear;
  VoltSeries.Clear;
  SunriseDifferenceSeries.Clear;
end;

procedure TPlotterForm.FilterComboBoxChange(Sender: TObject);
begin
  AddFilesToList(PlotFileDirectory);
end;

procedure TPlotterForm.PlotDirectoryButtonClick(Sender: TObject);
begin
  if (SelectDirectory('Select the .dat directory',PlotDirectoryEdit.Text, PlotFileDirectory)) then begin
    {Assign setting}
    PlotDirectoryEdit.Text:=PlotFileDirectory;
    PlotDirectoryEdit.Hint:= 'Location of plot files: ' + PlotFileDirectory;

    {Save setting to file}
    vConfigurations.WriteString(PlotterINIsection,'TreeViewPath', RemoveMultiSlash(PlotFileDirectory));
    {List files}
    AddFilesToList(PlotFileDirectory);
  end;
end;

procedure TPlotterForm.GetCustomizations();

procedure GetColor(O:TLineSeries; Name, DefaultColor:String);
var
    Color:TColor;
begin
  Color:=StringToColor(vConfigurations.ReadString(PlotterINIsection,Name,DefaultColor));
  O.SeriesColor :=Color;
  O.Pointer.Pen.Color:=Color;
end;

procedure GetWidth(O:TLineSeries; Name:String; DefaultWidth:Integer);
var
    Width:Integer;
begin
  Width:=StrToIntDef(vConfigurations.ReadString(PlotterINIsection,Name),DefaultWidth);
  O.LinePen.Width:=Width;
end;

begin
      {Get plot line customizations
        Color names: https://wiki.lazarus.freepascal.org/Colors#Convert_TColor_to.2Ffrom_string
        Color names are not case sensitive.
      }

      GetColor(MPSASSeries, 'MPSASColor','clRed');
      GetColor(MPSASSeries2, 'MPSAS2Color','clLime');
      GetColor(MPSASSeries3, 'MPSAS3Color','clBlue');
      GetColor(MoonSeries, 'MoonColor','clBlack');
      GetColor(SunSeries,'SunColor','$000097FF');
      GetColor(CivilTwilightSeries,'CivilColor','clTeal');
      GetColor(NauticalTwilightSeries,'NauticalColor','clNavy');
      GetColor(AstronmicalTwilightSeries,'AstronmicalColor','clBlack');
      GetColor(SunTwilightSeries,'SunTwilightColor','clAqua');

      GetWidth(MPSASSeries,'MPSASWidth',2);
      GetWidth(MPSASSeries2,'MPSAS2Width',2);
      GetWidth(MPSASSeries3,'MPSAS3Width',2);
      GetWidth(MoonSeries,'MoonWidth',1);
      GetWidth(SunSeries,'SunWidth',1);
      GetWidth(CivilTwilightSeries,'CivilWidth',1);
      GetWidth(NauticalTwilightSeries,'NauticalWidth',1);
      GetWidth(AstronmicalTwilightSeries,'AstronmicalWidth',1);
      GetWidth(SunTwilightSeries,'SunTwilightWidth',1);

      if VoltageVisible then begin
        GetColor(VoltSeries,'VoltageColor','clBlue');
        GetWidth(VoltSeries,'VoltageWidth',2);
      end;

      if TemperatureVisible then begin
       GetColor(TemperatureSeries,'TemperatureColor','$00008000');
       GetWidth(TemperatureSeries,'TemperatureWidth',2);
      end;

end;

procedure TPlotterForm.ReplotButtonClick(Sender: TObject);
begin

  if (FileSelectionMode='Single') then
    ClearSelButtonClick(nil);
  FileSelect();
end;


{Reset plot file directory to logs file directory}
procedure TPlotterForm.ResetToPlotDirectoryButtonClick(Sender: TObject);
begin
  {Reset}
  PlotFileDirectory:= RemoveMultiSlash(appsettings.LogsDirectoryDefault());
  PlotDirectoryEdit.Text:=PlotFileDirectory;
  PlotDirectoryEdit.Hint:= 'Location of plot files: ' + PlotFileDirectory;

  {Save selection}
  vConfigurations.WriteString(PlotterINIsection,'TreeViewPath',RemoveMultiSlash(PlotDirectoryEdit.Text));

  {List files}
  AddFilesToList(PlotFileDirectory);
end;

{ Display the cursor position values }
procedure TPlotterForm.ChartToolset1DataPointCrosshairTool1Draw(ASender: TDataPointDrawTool);
var
    RedValue, GreenValue, BlueValue:Byte;
    Red, Green, Blue, Clear, MaximumRGB, MinimumRGB, MaximumColor, MinimumColor,  Range: Float;
    DarknessValue: Float;

begin

  if FileSelectionMode<>'Multiple' then begin

  { Ensure that cursor index is within the plotter value. }
  if InRange(ASender.PointIndex,0,MPSASSeries.Count-1) then begin

  if (PlotType='Color') then begin //color model
      if InRange(ASender.PointIndex,0,RedSeries.Count-1) then begin
       Red:=RedSeries.GetYValue(ASender.PointIndex);
       readingstextR.Caption:= FloatToStr(Red);
       ReadingTime.Caption:=DateTimeToStr(RedSeries.GetXValue(ASender.PointIndex),CursorTimeFormat)
      end else
        readingstextR.Caption:= '';

      if InRange(ASender.PointIndex,0,GreenSeries.Count-1) then begin
       Green:=GreenSeries.GetYValue(ASender.PointIndex);
       readingstextG.Caption:= FloatToStr(Green);
      end else
        readingstextG.Caption:= '';

      if InRange(ASender.PointIndex,0,BlueSeries.Count-1) then begin
       Blue:=BlueSeries.GetYValue(ASender.PointIndex);
       readingstextB.Caption:= FloatToStr(Blue);
      end else
        readingstextB.Caption:= '';

      if InRange(ASender.PointIndex,0,ClearSeries.Count-1) then begin
       Clear:=ClearSeries.GetYValue(ASender.PointIndex);
       readingstextC.Caption:= FloatToStr(Clear);
      end else
        readingstextC.Caption:= '';

      {Make swatch ...}
      { - Invert darkness value to get brightness values }
      Red:= -1 * Red;
      Green:= -1 * Green;
      Blue:= -1 * Blue;
      Clear:= -1 * Clear;
      { - Find MaximumRGB }
      MaximumRGB:=abs(Max(Max(Red,Green),Blue));
      MaximumColor:=abs(Max(MaximumRGB,Clear));
      { - Find MinimumRGB }
      MinimumRGB:=abs(Min(Min(Red,Green),Blue));
      MinimumColor:=abs(Max(MinimumRGB,Clear));
      { - Apply offset}
      Red:=Red+MinimumRGB;
      Green:=Green+MinimumRGB;
      Blue:=Blue+MinimumRGB;
      { - Normalize values }
      Range:=abs(MaximumRGB - MinimumRGB);
      RedValue:= round(Red*255/Range);
      GreenValue:=round(Green*255/Range);
      BlueValue:=round(Blue*255/Range);
      { - Colorize the swatch }
      ColorSwatch.Brush.Color:=RGBToColor(RedValue, GreenValue, BlueValue);
      {Darkness value range}
      //actual darkness values not shown here because they are unreliable since dark calibration is too low of a value.
      ReadingText.Caption:= Format('%.2f to %.2fm',[MaximumColor,MinimumColor]);
    end else begin
      {Non-color models}
      DarknessValue:=MPSASSeries.GetYValue(ASender.PointIndex);
      if IsNan(DarknessValue) then
        ReadingText.Caption:=''
      else
        ReadingText.Caption:= Format('%.2fm',[DarknessValue]);

      if InRange(ASender.PointIndex,0,MPSASSeries.Count-1) then
         ReadingTime.Caption:=DateTimeToStr(MPSASSeries.GetXValue(ASender.PointIndex),CursorTimeFormat);

      if InRange(ASender.PointIndex,0,VoltSeries.Count-1) then  {Check if within range.}
        VoltageText.Caption:= Format('%.2fV',[VoltSeries.GetYValue(ASender.PointIndex)])
      else
        VoltageText.Caption:='';
    end;

    if InRange(ASender.PointIndex,0,TemperatureSeries.Count-1) then
      TemperatureReadingText.Caption:= Format('%.2f°C',[TemperatureSeries.GetYValue(ASender.PointIndex)])
    else
      TemperatureReadingText.Caption:= '';

    {Sun plot cursor value}
    if (SunVisible and (SunSeries.Count>0) and InRange(ASender.PointIndex,0,SunSeries.Count-1)) then begin
      SunText.Caption:=Format('%.1f°',[SunSeries.GetYValue(ASender.PointIndex)]);
    end
    else
      SunText.Caption:='';

    {Sunrise difference value}
    if (SunriseDiffVisible and (SunriseDifferenceSeries.Count>0) and InRange(ASender.PointIndex,0,SunriseDifferenceSeries.Count-1)) then
      SunriseDifferenceText.Caption:=Format('%.1fs',[SunriseDifferenceSeries.GetYValue(ASender.PointIndex)])
    else
      SunriseDifferenceText.Caption:='';

    {Moon plot cursor value}
    if (MoonVisible and (MoonSeries.Count>0) and InRange(ASender.PointIndex,0,MoonSeries.Count-1)) then
      MoonText.Caption:=Format('%.1f°',[MoonSeries.GetYValue(ASender.PointIndex)])
    else
      MoonText.Caption:='';

   end
   else begin //the cursor points outside the plot data
       ReadingText.Caption:= '';
       ReadingTime.Caption:='';
   end;

  end;
end;


{ Populate form from INI file }
procedure TPlotterForm.ReadINI();
var
  pieces: TStringList;

begin

   pieces := TStringList.Create;
   pieces.Delimiter := ',';
   pieces.StrictDelimiter := False; //Parse spaces also

   PlotterINIsection:='Plotter:';

   { Read previously selected file path }
   PlotDirectoryEdit.Text:=RemoveMultiSlash(vConfigurations.ReadString(
                              PlotterINIsection,
                              'TreeViewPath',
                              appsettings.LogsDirectory+DirectorySeparator
                              ));
   PlotDirectoryEdit.Hint:= 'Location of plot files: ' + PlotDirectoryEdit.Text;

   { Pull Timezone information from INI file if it exists.}
   PlotterTZRegion:=vConfigurations.ReadString(PlotterINIsection,'Local region');
   TZRegionBox.Text:= PlotterTZRegion;
   FillTimezones();

   { Read the previously recorded entries. }
   DefaultTZLocation:= vConfigurations.ReadString(PlotterINIsection,'Local time zone');
   TZLocationBox.Text:=DefaultTZLocation;

   PositionEntry.Text:=vConfigurations.ReadString(PlotterINIsection,'Position');
   //Parse location
   pieces.DelimitedText := PositionEntry.Text;
   if pieces.Count>11 then begin
    {Check if European commas were used instead of decimal points}
    if pieces.Count>=5 then begin
     MyLatitude:=StrToFloatDef(pieces.Strings[0]+'.'+pieces.Strings[1],0,FPointSeparator);
     MyLongitude:=StrToFloatDef(pieces.Strings[2]+'.'+pieces.Strings[3],0,FPointSeparator);
    end else begin
     MyLatitude:=StrToFloatDef(pieces.Strings[0],0,FPointSeparator);
     MyLongitude:=StrToFloatDef(pieces.Strings[1],0,FPointSeparator);
    end;
   end else begin
     MyLatitude:=0;
     MyLongitude:=0;
   end;
   //Parse elevation
   if pieces.Count>2 then begin
     MyElevation:=StrToFloatDef(pieces.Strings[2],0,FPointSeparator);
   end else begin
     MyElevation:=0;
   end;

   if Assigned(pieces) then FreeAndNil(pieces);

end;
procedure TPlotterForm.PlotFile(PlotFileName: String);
var
    File1: TextFile;
    Str: String; //Currently read string.
    LastStr: String = ''; //Last read string.
    pieces: TStringList;
    i: Integer = 0; //General purpose counter
    j: Integer = 0; //General purpose counter
    MSASField: Integer = -1; //Field that contains the MSAS variable, -1 = not defined  yet.
    CountsField: Integer = -1; //Field that contains the count/time variable, -1 = not defined  yet.
    ScaleField: Integer = -1; //Field that contains the Scale variable, -1 = not defined  yet.
    ColorField: Integer = -1; //Field that contains the Color variable, -1 = not defined  yet.
    TemperatureField: Integer = -1; //Field that contains the Temperature variable, -1 = not defined  yet.
    VoltageField: Integer = -1; //Field that contains the Voltage variable, -1 = not defined  yet.
    RecordTypeField: Integer = -1; //Field that contains the Record Type (Initial/subsequent) variable, -1 = not defined  yet.
    StdLinField: Integer = -1; //Field that contains the Record Type (Standard Linear value (from Snow unit)) variable, -1 = not defined  yet.
    SnowMSASField: Integer = -1; //Field that contains the Record Type (Snow reading) variable, -1 = not defined  yet.
    SnowLinField: Integer = -1; //Field that contains the Record Type (Snow Linear value (from Snow unit)) variable, -1 = not defined  yet.
    SunriseDiffField:Integer = -1; //Field that contains the Sunrise difference variable, -1 = not defined  yet.

    MoonElevation: extended = 0.0;
    MoonAzimuth: extended = 0.0;

    SunElevation: extended = 0.0;
    SunAzimuth: extended = 0.0;

    LocalRecordTime,UTCRecordTime,PlotRecordTime : TDateTime;
    LastPlotRecordTime : TDateTime = 0;
    LastRecordMinuteSpan : TDateTime = 0;
    PlotLocalRecordDate, PlotUTCRecordDate : TDateTime;
    TwilightMorningSunTimeStamp,TwilightEveningSunTimeStamp :TDateTime;
    TwilightMorningCivilTimeStamp,TwilightEveningCivilTimeStamp :TDateTime;
    TwilightMorningAstronomicalTimeStamp,TwilightEveningAstronomicalTimeStamp :TDateTime;
    TwilightMorningNauticalTimeStamp,TwilightEveningNauticalTimeStamp :TDateTime;
    Darkness: Float = 0.0;
    DarkestReading: Float = 0.0;
    LastDarkness: Float = 0.0;
    Temperature: Float = 0.0;
    Voltage: Float = 0.0;
    SnowMSAS: float = 0.0;
    SunriseDifference: float = 0.0;
    FirmwareString: String;
    DateString:String;
    FS: TFormatSettings;
    SerialINIsection:String;
    Plottable:Boolean = False;//flag to indentify if plot can be made.
    datLocalTZ: String; //Timezone information stored in the file.

    //RecordLabel:String; //Pointer label
    MPSASPointerColor:TColor; //Pointer color
    VoltagePointerColor:TColor; //Pointer color
    TemperaturePointerColor:TColor; //Pointer color
    symbol: TSeriesPointerStyle;

    RequireSunMoonElevationLabel: Boolean;

    CurrentLine: Integer; //Current .dat line being read.
    IgnoreNonSequential: Boolean; //Flag to ignore .dat Non-Sequential records

Function PositionParse(): Bool;
begin
   { Get Lat, Lon for Moon calculations. }
   pieces.Delimiter := ',';

   { Separate the fields of the lat,lon.}
   pieces.DelimitedText := PositionEntry.Text;

   {Assume that position is unusable unless something below fails}
   PositionUsable:=True;

   if (pieces.Count > 1) then begin
    {Check if European commas were used instead of decimal points}
    if pieces.Count>=5 then begin
     MyLatitude:=StrToFloatDef(pieces.Strings[0]+'.'+pieces.Strings[1],0,FPointSeparator);
    end else
     MyLatitude:=StrToFloat(pieces.Strings[0],FPointSeparator);
     if ((MyLatitude<-90.0) or (MyLatitude>90.0)) then begin
         PositionUsable:=False;
         MessageDlg('Latitude Error',
           'Error: Latitude ' + FloatToStr(MyLatitude)+' out of range.',
           mtError,
           [mbOK],0);
     end;
     {Check if European commas were used instead of decimal points}
     if pieces.Count>=5 then begin
      MyLongitude:=StrToFloatDef(pieces.Strings[2]+'.'+pieces.Strings[3],0,FPointSeparator);
     end else
       MyLongitude:=StrToFloat(pieces.Strings[1],FPointSeparator);
     if ((MyLongitude<-180.0) or (MyLongitude>180.0)) then begin
         PositionUsable:=False;
         MessageDlg('Longitude Error',
           'Error: Longitude ' + FloatToStr(MyLongitude)+' out of range.',
           mtError,
           [mbOK],0);
     end;
   end else begin
      {Improper or no position is entered, position is not usable. }
      PositionUsable:=False;
      {Check if SunMoon display is enabled}
      if (SunVisible or MoonVisible) then begin
       screen.Cursor:= crDefault;
       if(MessageDlg('Sun/Moon plot enabled',
          'Sun/Moon display is enabled, but world position is not set in data file. Press:'+sLineBreak+
          ' Yes, to continue loading and clear Sun/Moon display settings.'+sLineBreak+
          ' No , to stop loading file.',
          mtWarning,
          [mbYes,mbNo],0)) = mrYes then begin
             SunVisible:=False;
             MoonVisible:=False;
             SunCheckBox.Checked:=False;
             MoonCheckBox.Checked:=False;
             vConfigurations.WriteBool(PlotterINIsection,'Sun',SunVisible);
             vConfigurations.WriteBool(PlotterINIsection,'Moon',MoonVisible);
             SunSeries.LinePen.Style:=psClear;
             SunSeries.LinePen.Width:=0;
             SunText.Visible:=False;
             SunTextLabel.Visible:=False;
             MoonText.Visible:=False;
             MoonTextLabel.Visible:=False;
             Chart1.AxisList[2].Visible:= False;
          end else exit(false);
      end;
   end;
   exit(True);
end;

begin
   screen.Cursor:= crHourGlass;
   StatusMessage(format('Plotting file: %s',[PlotFileName]));
   pieces := TStringList.Create;
   AssignFile(File1, PlotFileName);
   HeaderStrings:=TStringList.Create;

   {for .txt files probably from sqm reader by knightware.
     - cannot get this to work on some 2 digit year timestamp files.
     - need more sample files to test against.}
   FS := DefaultFormatSettings;
   FS.DateSeparator := '/';
   FS.ShortDateFormat := 'yyyy-mm-dd';
   FS.ShortTimeFormat := 'hh:nn:ss';

   CursorTimeFormat := DefaultFormatSettings;
   CursorTimeFormat.DateSeparator := '/';
   CursorTimeFormat.ShortDateFormat := 'yyyy-mm-dd';
   CursorTimeFormat.ShortTimeFormat := 'hh:nn:ss';

   { Read the region database table. }
   {  Required for converting local to GMT, local zone must be found from region selection.}
   {  Since region is not stored in .dat header, entire database of zones must be loaded}
   //if (FileExists(appsettings.TZDirectory+PlotterTZRegion) and (length(PlotterTZRegion)>0))then begin
   //  ptz.Destroy;
   //  ptz := TPascalTZ.Create();
   //  ptz.ParseDatabaseFromDirectory(appsettings.TZDirectory);
   //  Azones.Clear;
   //  ptz.GetTimeZoneNames(AZones,true); //only geo name = true
   //end;

   case ExtractFileExt(PlotFileName) of


   '.txt':begin

       if (AnsiContainsStr(FilterComboBox.Text,'SQMReader')) then begin
         try
           {$I+}

           //Default to no model selected
           PlotterModel:=0;

           //Default to no plot type selected
           PlotType:='';

           Reset(File1);
           repeat
             // Read one line at a time from the file.
             Readln(File1, Str);

             {Separate the fields of the record.}
             pieces.Delimiter := ',';
             pieces.DelimitedText := Str;

             {Set default MPSAS field}
             MSASField:=2;

             { Check for comment lines }
             if (AnsiStartsStr('Produced by SQM Reader',Str) or AnsiStartsStr('Year',Str)) then begin
               //Ignore these lines
             end

             else if (AnsiStartsStr('Date/Time,MPSAS',Str)) then begin
                 FS.ShortDateFormat := 'dd/mm/yyyy';
                 FS.ShortTimeFormat := 'hh:nn:ss';
                 MSASField:=1;
             end

             else begin

                 {Separate the fields of the record.}
                 pieces.DelimitedText := Str;

                 DateString:=pieces.Strings[0]+ ' ' +pieces.Strings[1];
                 LocalRecordTime:= StrToDateTime(DateString, FS);

                 PlotRecordTime:=LocalRecordTime;

                 //Update Chart
                 Darkness:= StrToFloatDef(AnsiLeftStr(pieces.Strings[MSASField],Length(pieces.Strings[MSASField])),0,FPointSeparator);

                   if PlotNumberGroup.ItemIndex=0 then
                      ListChartSource1.Add(PlotRecordTime, Darkness,Str,clFuchsia);
                   if PlotNumberGroup.ItemIndex=1 then
                      MPSASSeries2.AddXY(PlotRecordTime, Darkness);
                   if PlotNumberGroup.ItemIndex=2 then
                      MPSASSeries3.AddXY(PlotRecordTime, Darkness);

             end;//end of checking for data

           until(EOF(File1)); // EOF(End Of File) The the program will keep reading new lines until there is none.
           CloseFile(File1);

         except
           on E: EInOutError do
             MessageDlg('Error', 'File handling error occurred. Details: '+E.ClassName+'/'+E.Message, mtError, [mbOK],0);
           on E: Exception do
             ShowMessage( 'Error: '+ E.ClassName + #13#10 + '>' + str + '<' + #13#10 + E.Message );

         end;

       end //End of SQM Reader files

   else if (AnsiContainsStr(FilterComboBox.Text,'LightPollutionMap')) then begin
    try
      {$I+}

      //Default to no model selected
      PlotterModel:=0;

      //Default to no plot type selected
      PlotType:='';

      FS.ShortDateFormat := 'dd/mm/yyyy';
      FS.LongTimeFormat := 'hh:nn:ss';

      TemperaturePointerColor:=$00008000; //Inidicates 'subsequent' reading

      {Set default MPSAS field}
      MSASField:=1;
      TemperatureField:=2;

      Reset(File1);
      Readln(File1, Str);//**debug delete first line
      repeat
        // Read one line at a time from the file.
        Readln(File1, Str);

        {Separate the fields of the record.}
        pieces.Delimiter := ' ';
        pieces.DelimitedText := Str;

        {Separate the fields of the record.}
        pieces.DelimitedText := Str;

        {Get date from filename IN:yyyymmdd, OUT:dd/mm/yyyy}
        DateString:=AnsiRightStr(Utf8ToAnsi(ExtractFileNameOnly(PlotFileName)),2) + //date
                    '/'+AnsiMidStr(Utf8ToAnsi(ExtractFileNameOnly(PlotFileName)),5,2) + //Month
                    '/'+AnsiLeftStr(Utf8ToAnsi(ExtractFileNameOnly(PlotFileName)),4) + //Year
                    ' '+pieces.Strings[0];
        LocalRecordTime:= StrToDateTime(DateString, FS);

        PlotRecordTime:=LocalRecordTime;

        //Update Chart
        Darkness:= StrToFloatDef(AnsiLeftStr(pieces.Strings[MSASField],Length(pieces.Strings[MSASField])-1),0,FPointSeparator);
        Temperature:=StrToFloatDef(AnsiLeftStr(pieces.Strings[TemperatureField],Length(pieces.Strings[TemperatureField])-1),0,FPointSeparator);

        if PlotNumberGroup.ItemIndex=0 then
           ListChartSource1.Add(PlotRecordTime, Darkness, Str, clFuchsia);
        if PlotNumberGroup.ItemIndex=1 then
           MPSASSeries2.AddXY(PlotRecordTime, Darkness);
        if PlotNumberGroup.ItemIndex=2 then
           MPSASSeries3.AddXY(PlotRecordTime, Darkness);

        TemperatureSeries.AddXY(PlotRecordTime, Temperature, '', TemperaturePointerColor);

      until(EOF(File1)); // EOF(End Of File) The the program will keep reading new lines until there is none.
      CloseFile(File1);

    except
      on E: EInOutError do
        MessageDlg('Error', 'File handling error occurred. Details: '+E.ClassName+'/'+E.Message, mtError, [mbOK],0);
      on E: Exception do
        ShowMessage( 'Error: '+ E.ClassName + #13#10 + '>' + str + '<' + #13#10 + E.Message );

    end;

  end;//End of LightPollutionMap file


   end; //End of checking for .txt file type


   '.log': begin   //Old log files from Perl script
            {$I+}
            try

              //Get Lat, Lon for Moon calculations
              pieces.Delimiter := ',';
              //Separate the fields of the lat,lon.
              pieces.DelimitedText := PositionEntry.Text;

              MyLatitude:=StrToFloat(pieces.Strings[0],FPointSeparator);
              if ((MyLatitude<-90.0) or (MyLatitude>90.0)) then begin
                  MessageDlg('Latitude Error',
                    'Error: Latitude ' + FloatToStr(MyLatitude)+' out of range.',
                    mtError,
                    [mbOK],0);
              end;
              MyLongitude:=StrToFloat(pieces.Strings[1],FPointSeparator);
              if ((MyLongitude<-180.0) or (MyLongitude>180.0)) then begin
                  MessageDlg('Longitude Error',
                    'Error: Longitude ' + FloatToStr(MyLongitude)+' out of range.',
                    mtError,
                    [mbOK],0);
              end;


              Reset(File1);
              pieces.Delimiter := ' ';
              repeat
                // Read one line at a time from the file.
                Readln(File1, Str);

                //Separate the fields of the record.
                pieces.DelimitedText := Str;

                //Make sure there are enough fields
                if (pieces.Count<>3) then begin
                    MessageDlg('Error', 'Incorrect number of fields in record. Expected 3, got: '+ IntToStr(pieces.Count), mtError, [mbOK],0);
                    break;
                  end
                else begin
                  //parse the fields, and convert as necessary.
                  //Convert UTC string 'YYYY-MM-DDTHH:mm:ss.fff' into TDateTime
                  LocalRecordTime:=ScanDateTime('yyyymmddhh:nn:ss',LazFileUtils.ExtractFileNameOnly(PlotFileName) + pieces.Strings[0]);

                  UTCRecordTime := ptzAll.LocalTimeToGMT(LocalRecordTime,PlotterTZLocation);

                  //Convert local time to UTC for Moon calculation

                  //Update Chart
                  Darkness:= StrToFloatDef(AnsiLeftStr(pieces.Strings[1],Length(pieces.Strings[1])-1),0,FPointSeparator); //remove trailing m from mpsas
                  ListChartSource1.Add(LocalRecordTime, Darkness, Str, clFuchsia);
                  //Calculate Moon position
                  //Change sign for Moon calculations
                  Moon_Position_Horizontal(
                                          StrToDateTime(DateTimeToStr(UTCRecordTime)),
                                          -1.0*MyLongitude,
                                          MyLatitude,
                                          MoonElevation,
                                          MoonAzimuth);
                  MoonSeries.AddXY(LocalRecordTime,MoonElevation);
                  //Calculate Sun position
                  //Change sign for Sun calculations
                  Sun_Position_Horizontal(StrToDateTime(DateTimeToStr(UTCRecordTime)),
                                          -1.0*MyLongitude,
                                          MyLatitude,
                                          SunElevation,
                                          SunAzimuth);
                  SunSeries.AddXY(LocalRecordTime,SunElevation);

                end;//End of checking number of fields in record.

              until(EOF(File1)); // EOF(End Of File) The the program will keep reading new lines until there is none.
              CloseFile(File1);



            except
              on E: EInOutError do begin
               MessageDlg('Error', 'File handling error occurred. Details: '+E.ClassName+'/'+E.Message, mtError, [mbOK],0);
              end;

            end;
   end;

   //New format .dat files from Log continuous and DL retrieve.
   '.dat': begin
             {$I+}
             try

                {Show default position}
               { Pull Timezone information from INI file if it exists.}
               PlotterTZRegion:=Trim(vConfigurations.ReadString(PlotterINIsection,'Local region'));

               { Read the previously recorded entries. }
               DefaultTZLocation:= Trim(vConfigurations.ReadString(PlotterINIsection,'Local time zone'));

               TZChanging:=True; //prevent change triggers
                 { Update timezone identifiers on screen. }
                 TZRegionBox.Text:=PlotterTZRegion;
                 TZLocationBox.Text:=DefaultTZLocation;
               TZChanging:=False;

               PlotterTZLocation:='';
               PlotFileTimezoneLabel.Text:=PlotterTZLocation;

               { Assume that Sun/Moon elevation labels are not required. }
               RequireSunMoonElevationLabel:=False;


               Application.ProcessMessages;

               if (not PositionParse) then begin
                     if Assigned(pieces) then FreeAndNil(pieces);
                     exit;
               end else
                 screen.Cursor:= crHourGlass;


               //Default to no model selected
               PlotterModel:=0;

               //Default to no plot type selected
               PlotType:='';

               Reset(File1);
               CurrentLine:=0;
               IgnoreNonSequential:=False;
               repeat
                 // Read one line at a time from the file.
                 Readln(File1, Str);
                 Inc(CurrentLine);

                 //Check for comment lines
                 //Skip improper dates (like 1985)
                 if (AnsiStartsStr('#',Str) or AnsiStartsStr('1985',Str)) then begin

                  {Save all header lines for possible export in the Edit panel.}
                  HeaderStrings.Add(Str);


                  { *** Determine if file UTC and position match default values, warn. }
                   { Get the .dat timezone information if it exists}
                   if (AnsiStartsStr('# Local timezone:',str)) then begin
                     pieces.Delimiter := ':';
                     pieces.StrictDelimiter := True; //Do not parse spaces also
                     pieces.DelimitedText := Str;
                     if (pieces.Count>1) then
                       if Trim(pieces.Strings[1]) <>'' then begin
                         datLocalTZ:=Trim(pieces.Strings[1]);
                       end;
                     {Determine if zone is valid.}
                     if ((Length(datLocalTZ)>0) and (not ptzAll.TimeZoneExists(datLocalTZ,True))) then begin
                       if (ptzAll.TimeZoneExists(StringReplace(datLocalTZ,' ','_',[rfReplaceAll]),True)) then begin
                       ShowMessage(Format('Time zone [%s] %s%sin the file:%s %s%sshould not have spaces in name.',
                         [datLocalTZ,sLineBreak,sLineBreak,PlotFileName,sLineBreak,sLineBreak]));
                       end else
                       ShowMessage(Format('Time zone [%s] %s%sin the file:%s %s%sdoes not exist in the timezone database.',
                         [datLocalTZ,sLineBreak,sLineBreak,PlotFileName,sLineBreak,sLineBreak]));
                       datLocalTZ:='';
                     end;
                   end;

                   { Get Serial number (for location information) }
                    if (AnsiStartsStr('# SQM serial number:',str)) then begin
                       pieces.Delimiter := ':';
                       pieces.StrictDelimiter := True; //Do not parse spaces also
                       pieces.DelimitedText := Str;
                       if (pieces.Count>1) then
                         if Trim(pieces.Strings[1]) <>'' then begin
                           SelectedSerialNumberString:=Trim(pieces.Strings[1]);
                         end;

                       { Use .dat file serial number to get stored time zone information. }
                       SerialINIsection:='Serial:'+SelectedSerialNumberString;
                       MeterTZLocation:= Trim(vConfigurations.ReadString(SerialINIsection,'Local time zone'));
                       {Determine if zone is valid.}
                       if ((Length(MeterTZLocation)>0) and (not ptzAll.TimeZoneExists(MeterTZLocation,True))) then begin
                         ShowMessage(Format('Time zone [%s] %s in the config file for this meter [Serial:%s]%s does not exist in timezone database.',
                           [MeterTZLocation,sLineBreak,SelectedSerialNumberString,sLineBreak]));
                         MeterTZLocation:='';
                       end;

                    end;

                  { Parse position definition line. }
                   if (AnsiStartsStr('# Position',str)) then begin
                      pieces.Delimiter := ':';
                      pieces.StrictDelimiter := True; //Do not parse spaces also
                      pieces.DelimitedText := Str;
                      if (pieces.Count>1) then begin
                        if Trim(pieces.Strings[1]) <>'' then begin
                          PositionEntry.Text:=Trim(pieces.Strings[1]);
                          if (not PositionParse) then begin
                                if Assigned(pieces) then FreeAndNil(pieces);
                                exit;
                          end else
                            screen.Cursor:= crHourGlass;
                        end;
                      end;
                   end;

                   { Parse the model type. }
                   if (AnsiStartsStr('# SQM firmware version:',str)) then begin
                      pieces.Delimiter := ':';
                      pieces.StrictDelimiter := True; //Do not parse spaces also
                      pieces.DelimitedText := Str;
                      if (pieces.Count>1) then begin
                        FirmwareString:=pieces.Strings[1];
                        pieces.Delimiter := '-';
                        pieces.StrictDelimiter := True; //Do not parse spaces also
                        pieces.DelimitedText := FirmwareString;
                         if (pieces.Count=3) then begin
                           PlotterModel:= StrToIntDef(pieces.Strings[1],0);
                           PlotterModelText.Caption:=IntToStr(PlotterModel);
                         end;
                      end;
                   end;

                   { Parse field definition line. }
                   if (AnsiContainsStr(str,'UTC') and AnsiContainsStr(str,'Local') and AnsiContainsStr(str,'MSAS')) then begin
                     pieces.Delimiter := ',';
                     pieces.StrictDelimiter := True; //Do not parse spaces also
                     pieces.DelimitedText := Str;

                     { Get the field locations. }
                     for i:=0 to pieces.Count-1 do begin
                       case Trim(pieces.Strings[i]) of
                         'MSAS': MSASField:=i;
                         'Counts': CountsField:=i;
                         'Scale': ScaleField:=i;
                         'Color': ColorField:=i;
                         'Temperature': TemperatureField:=i;
                         'Voltage': VoltageField:=i;
                         'Record type': RecordTypeField:=i;
                         'Std lin.': StdLinField:=i;
                         'Snow MSAS': SnowMSASField:=i;
                         'Snow lin.': SnowLinField:=i;
                         'SunriseDiff':SunriseDiffFIeld:=i;
                       end;
                     end;
                     if ((PlotterModel=4) and (ScaleField>0) and (ColorField>0)) then begin
                         PlotType:='Color';
                         PlotterModelType.Caption:=PlotType;
                         PlotterSN.Caption:=SelectedSerialNumberString;
                         readingstextR.Visible:=True;
                         readingstextG.Visible:=True;
                         readingstextB.Visible:=True;
                         readingstextC.Visible:=True;
                         ColorSwatch.Visible:=True;
                         VoltageText.Visible:=False;
                         VoltageTextLabel.Visible:=False;
                         Chart1.AxisList[1].Marks.Format:='%0:3.0f';
                       end else begin
                         case PlotterModel of
                           3: PlotType:='LE/LU';
                           5: PlotType:='LR';
                           6: PlotType:='DL';
                           11:PlotType:='V';
                           else PlotType:='';
                         end;
                          PlotterModelType.Caption:=PlotType;
                          PlotterSN.Caption:=SelectedSerialNumberString;
                          readingstextR.Visible:=False;
                          readingstextG.Visible:=False;
                          readingstextB.Visible:=False;
                          readingstextC.Visible:=False;
                          ColorSwatch.Visible:=False;
                          VoltageText.Visible:=True;
                          VoltageTextLabel.Visible:=True;
                          Chart1.AxisList[1].Marks.Format:='%0:3.2f';
                       end;

                       ReadingText.Visible:=DarknessVisible;
                       ReadingTextLabel.Visible:=DarknessVisible;

                       SunText.Visible:=SunVisible;
                       SunTextLabel.Visible:=SunVisible;

                       MoonText.Visible:=MoonVisible;
                       MoonTextLabel.Visible:=MoonVisible;

                       VoltageText.Visible:=VoltageVisible;
                       VoltageTextLabel.Visible:=VoltageVisible;

                       TemperatureReadingText.Visible:=TemperatureVisible;
                       TemperatureReadingTextLabel.Visible:=TemperatureVisible;

                   end;

                   {Finished reading header/comment lines.}
                   if (AnsiStartsStr('# END OF HEADER',str)) then begin

                     {Assumptions}
                     Plottable:=True; // Plottable assumed.
                     PlotterTZLocation:='';// No time zone iniitally selected.

                     {Load up desired timezone. the selection priority is:
                        1. .dat value (datLocalTZ)
                        2. meter serial associated value (MeterTZLocation)
                        3. stored default (DefaultTZLocation)
                      }

                     if (Length(datLocalTZ)>0) then
                       PlotterTZLocation:=datLocalTZ else
                       if (Length(MeterTZLocation)>0) then
                         PlotterTZLocation:=MeterTZLocation else
                         if (Length(DefaultTZLocation)>0) then
                           PlotterTZLocation:=DefaultTZLocation else begin
                             ShowMessage('Enter timezone.');
                             Plottable:=False;
                           end;

                     {Determine if zone is valid.}
                     if (not ptzAll.TimeZoneExists(PlotterTZLocation,True)) then begin
                       ShowMessage(Format('Time zone [%s] does not exist.',[PlotterTZLocation]));
                       Plottable:=False;
                     end;

                     PlotFileTimezoneLabel.Text:=PlotterTZLocation;
                     Application.ProcessMessages;

                   end;


                 end else begin


                   if Plottable then begin

                   //Separate the fields of the record.
                   pieces.Delimiter := ';';
                   pieces.DelimitedText := Str;

                   //Make sure there are enough fields
                   if (MSASField=-1) then begin

                       DialogCentered(
                         'Error',
                         'No MSAS field defined in file',
                         mtError,
                         [mbOK],
                         0,
                         PlotterForm
                        );

                       break;
                     end
                   else begin

                     UTCRecordTime  :=ScanDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz',pieces.Strings[0]);
                     LocalRecordTime:=ScanDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz',pieces.Strings[1]);

                     if YearOf(UTCRecordTime)>2000 then begin

                     //Time reference for plotting.
                     case TimeReference of
                       'UTC': PlotRecordTime:=UTCRecordTime;
                       else PlotRecordTime:=LocalRecordTime;
                     end;

                     {Add manual time offset to this data set}
                     if TimeOffset<>0 then begin
                       UTCRecordTime  :=IncSecond(  UTCRecordTime, TimeOffset);
                       LocalRecordTime:=IncSecond(LocalRecordTime, TimeOffset);
                       PlotRecordTime :=IncSecond( PlotRecordTime, TimeOffset);
                     end;

                     {Get range of time to plot Sun, Moon, Twilights}
                     If (LocalRecordTime>MaxLocalRecordTime) then
                       MaxLocalRecordTime:=LocalRecordTime;
                     If ((LocalRecordTime<MinLocalRecordTime) or (MinLocalRecordTime=0)) then
                       MinLocalRecordTime:=LocalRecordTime;

                     //Update Chart
                     {Temperature}
                     if TemperatureField>-1 then begin
                       Temperature := StrToFloatDef(pieces.Strings[TemperatureField],0,FPointSeparator);
                     end;

                     {Voltage}
                     if VoltageField>-1 then
                       Voltage:= StrToFloatDef(pieces.Strings[VoltageField],0,FPointSeparator);

                     if SunriseDiffField>-1 then begin
                       SunriseDifference:=StrToFloatDef(pieces.Strings[SunriseDiffField],0,FPointSeparator);
                       SunriseDiffVisible:=True;
                       SunriseDifferenceLabel.visible:=True;
                       SunriseDifferenceText.visible:=True;
                       Chart1.AxisList[5].Visible:=True;
                       end
                     else begin
                       SunriseDiffVisible:=False;
                       SunriseDifferenceLabel.visible:=False;
                       SunriseDifferenceText.visible:=False;
                       Chart1.AxisList[5].Visible:=False;
                     end;

                     {RecordTypeField}
                     if ((RecordTypeField>-1) and (pieces.Count>RecordTypeField)) then begin
                       RecordType:= StrToIntDef(pieces.Strings[RecordTypeField],0);
                     end else
                       RecordType:= 1; //Assume subsequent record.

                     {Darkness}

                     { - Color model, only display counts since very dark mpsas might be messed up}
                     //if (PlotType='Color') then
                     //  MSASField:=CountsField;

                     if pieces.Count>MSASField then begin
                       Darkness:= StrToFloatDef(AnsiLeftStr(pieces.Strings[MSASField],Length(pieces.Strings[MSASField])),0,FPointSeparator);
                       if (Darkness>DarkestReading) then begin
                         DarkestReading:=Darkness;
                         DarkestReadingText.Text:=Format('%.2fm',[DarkestReading]);;
                         DarkestReadingTime.Text:=DateTimeToStr(PlotRecordTime,CursorTimeFormat);
                       end;


                       {Gather darknesses for color model}
                       if (PlotType='Color') then begin
                         if pieces.Count>ColorField then begin
                          case StrToIntDef(pieces.Strings[ColorField],0) of
                            0: LastRedDarkness  :=StrToFloatDef(AnsiLeftStr(pieces.Strings[CountsField],Length(pieces.Strings[CountsField])),0,FPointSeparator);
                            1: LastBlueDarkness :=StrToFloatDef(AnsiLeftStr(pieces.Strings[CountsField],Length(pieces.Strings[CountsField])),0,FPointSeparator);
                            2: LastClearDarkness:=StrToFloatDef(AnsiLeftStr(pieces.Strings[CountsField],Length(pieces.Strings[CountsField])),0,FPointSeparator);
                            3: LastgreenDarkness:=StrToFloatDef(AnsiLeftStr(pieces.Strings[CountsField],Length(pieces.Strings[CountsField])),0,FPointSeparator);
                          end;
                         end;
                       end;
                     end else begin
                       Darkness:=0;
                     end;

                     if SnowMSASField>-1 then
                       SnowMSAS:=StrToFloatDef(pieces.Strings[SnowMSASField],0,FPointSeparator);

                       case FileSelectionMode of
                         'Single','Accumulate': begin

                           { Colorize pointer label depending on initial/subsequent reading.
                             Pointer style is also changed depending on color in the OngetPointerStyle handler.}
                           if RecordType=0 then begin {initial record}
                             if ContinuousLine then begin
                               MPSASPointerColor:=clRed; {Inidicates 'continuous' reading.}
                               VoltagePointerColor:=clBlue; {Inidicates 'continuous' reading.}
                               TemperaturePointerColor:=$00008000; {Inidicates 'continuous' reading.}
                             end
                             else begin
                               {Leave a blank unconnected space between lines.}
                               MPSASPointerColor:=clWhite; //Inidicates 'initial' reading
                               VoltagePointerColor:=clWhite; //Inidicates 'initial' reading
                               TemperaturePointerColor:=clWhite; //Inidicates 'initial' reading
                               LastPlotRecordTime:=0;
                               {Leave a blank unconnected space between subsequent to initial record.}
                               if DarknessVisible then MPSASSeries.AddNull();
                               if VoltageVisible then VoltSeries.AddNull();
                               if TemperatureVisible then TemperatureSeries.AddNull();
                               if MoonVisible then MoonSeries.AddNull();
                               if SunVisible then SunSeries.AddNull();
                             end;
                           end else begin {subsequent record}


                               {Check and warn if records are backwards in time}
                               if ((CompareDateTime(LastPlotRecordTime,PlotRecordTime)>0) and not IgnoreNonSequential)then begin
                                case DialogCentered('Records reading error','Records are non-sequential:'+ sLineBreak +
                                  'Line ' + IntToStr(CurrentLine-1) + ': ' + LastStr + sLineBreak +
                                  'Line ' + IntToStr(CurrentLine) + ': ' + Str + sLineBreak +
                                  IfThen(ContinuousLine,
                                    'Continuous line mode is ON, reverse times would be plotted.',
                                    'Continuous line mode is OFF, reverse times will not be plotted.') + sLineBreak +
                                  ' ' + sLineBreak +
                                  'Press:' + sLineBreak +
                                  '  OK to stop loading.'+ sLineBreak +
                                  '  Ignore to continue loading.',
                                  mtConfirmation,[mbOK,mbIgnore],0,PlotterForm) of
                                    mrOK: begin
                                       StatusMessage('OK pressed');
                                       Break;
                                    end;
                                    mrIgnore: begin
                                       StatusMessage('Ignore pressed');
                                       IgnoreNonSequential:=True;
                                    end;
                                  end;
                               end;


                             MPSASPointerColor:=clRed; //Inidicates 'subsequent' reading
                             VoltagePointerColor:=clBlue; //Inidicates 'subsequent' reading
                             TemperaturePointerColor:=$00008000; //Inidicates 'subsequent' reading

                             {Check if line should be drawn if current_span is  <= last_span+1minute}
                             {Check if darkness is zero.}
                             if (
                                 ((MinuteSpan(LastPlotRecordTime,PlotRecordTime)>LastRecordMinuteSpan+1)
                                 and not ContinuousLine)
                                 or (not ZeroPlot and ((Darkness=0) or (LastDarkness=0)))
                                ) then begin
                               {Leave a blank unconnected space between lines.}
                               if DarknessVisible then MPSASSeries.AddNull();
                               {Have to add nulls for other series so taht preview mode does not crash }
                               if VoltageVisible then VoltSeries.AddNull();
                               if TemperatureVisible then TemperatureSeries.AddNull();
                               if MoonVisible then MoonSeries.AddNull();
                               if SunVisible then SunSeries.AddNull();
                             end;

                             //if ( //Had to remove this section because preview mode crashes when all series are not the same count.
                             //    ((MinuteSpan(LastPlotRecordTime,PlotRecordTime)>LastRecordMinuteSpan+1)
                             //    and not ContinuousLine)) then begin
                             //  {Leave a blank unconnected space between lines.}
                             //  if VoltageVisible then VoltSeries.AddNull();
                             //  if TemperatureVisible then TemperatureSeries.AddNull();
                             //  if MoonVisible then MoonSeries.AddNull();
                             //  if SunVisible then SunSeries.AddNull();
                             //end;

                             LastRecordMinuteSpan:=MinuteSpan(LastPlotRecordTime,PlotRecordTime);
                             LastPlotRecordTime:=PlotRecordTime;
                           end;

                           {Determine pointer symbol for each record}
                           if RecordType=0 then //Initial reading
                             symbol:=psRectangle
                           else if Darkness=0 then //zero reading (saturated brightness)
                             symbol:=psPoint
                           else
                             symbol:=psDiamond;//DEBUG psNone; //Subsequent readings have a clean connecting line.

                           if ContinuousLine then
                              symbol:=psNone; //Override pointer symbol when in continuous line mode.

                           if (PlotType='Color') then begin  {Color model}
                               RedSeries.AddXY(PlotRecordTime, LastRedDarkness);
                               GreenSeries.AddXY(PlotRecordTime, LastgreenDarkness);
                               BlueSeries.AddXY(PlotRecordTime, LastBlueDarkness);
                               ClearSeries.AddXY(PlotRecordTime, LastClearDarkness);
                             end
                           else begin {non-color model}
                             {Add record to list}
                             j:=ListChartSource1.Add(PlotRecordTime, Darkness,Str, MPSASPointerColor);
                             {Record darkness for future comparisons}
                             LastDarkness:=Darkness;
                             MPSASSeries.XValues[j, 1] := ord(symbol);
                           end;

                           if VoltageVisible then
                              VoltSeries.AddXY(PlotRecordTime, Voltage, '', VoltagePointerColor);

                           if TemperatureVisible then
                              TemperatureSeries.AddXY(PlotRecordTime, Temperature, '', TemperaturePointerColor);

                           if SnowMSASField>-1 then
                             SnowSeries.AddXY(PlotRecordTime, SnowMSAS, '', clBlack);

                           if SunriseDiffVisible then
                             SunriseDifferenceSeries.AddXY(PlotRecordTime, SunriseDifference, '', clBlack);

                         end;

                         'Multiple': MultiChart[MultiIndex].AddXY(PlotRecordTime, Darkness);
                       end;

                     {Check if position is usable for Moon/Sun plotting}
                      if PositionUsable then begin

                        {Check if Moon plot has been selected}
                        if MoonVisible then begin;
                          { Calculate Moon position:
                              - Change longitude sign for astronomy calculations. }
                           Moon_Position_Horizontal(
                                                   StrToDateTime(DateTimeToStr(UTCRecordTime)),
                                                   -1.0*MyLongitude,
                                                   MyLatitude,
                                                   MoonElevation,
                                                   MoonAzimuth);
                           if ((not ClipSunMoon) or (MoonElevation>=-18)) then begin
                            MoonSeries.AddXY(PlotRecordTime,MoonElevation);
                            RequireSunMoonElevationLabel:=True;
                           end else
                             MoonSeries.AddNull;

                         end;

                        {Check if Sun plot has been selected}
                        if SunVisible then begin;
                           { Calculate Sun position:
                               -  Change longitude sign for astronomy calculations. }
                           Sun_Position_Horizontal(StrToDateTime(DateTimeToStr(UTCRecordTime)),
                                                   -1.0*MyLongitude,
                                                   MyLatitude,
                                                   SunElevation,
                                                   SunAzimuth);
                          if ((not ClipSunMoon) or (SunElevation>=-18)) then begin
                           SunSeries.AddXY(PlotRecordTime,SunElevation);
                           RequireSunMoonElevationLabel:=True;
                          end else
                           SunSeries.AddNull;
                        end;

                      end; // End of Checking if position is usable for Moon/Sun plotting
                     end;//End of checking for valid year in timestamp
                   end;//End of checking number of fields in record.

                  end;//End of plottable section
                 end;//end of checking for data

                 LastStr:=Str; //Preserve last string read for error reporting

               until(EOF(File1)); // EOF(End Of File) The the program will keep reading new lines until there is none.
               CloseFile(File1);

             except
               on E: EInOutError do
                 MessageDlg('Error', 'File handling error occurred. Details: '+E.ClassName+'/'+E.Message, mtError, [mbOK],0);
               on E: Exception do begin
                 ShowMessage( 'Error: '+ E.ClassName + #13#10 + '>' + str + '<' + #13#10 + E.Message );
               end;

             end;

             { Only plot rise/set times if UTC and position are defined }
             if (Plottable and PositionUsable) then begin
               RequireSunMoonElevationLabel:=True;

               {--- Plot twilight lines ---
                 Twilight times for all recorded range is plotted. }
               PlotLocalRecordDate:=DateOf(MinLocalRecordTime); //Starting point
               MaxLocalRecordDate := DateOf(MaxLocalRecordTime); //Ending point

               {Create time range for visible twilight lines}
               case TimeReference of
                    'UTC': begin
                      MinPlotTime:=ptzAll.LocalTimeToGMT(MinLocalRecordTime,PlotterTZLocation);
                      MaxPlotTime:=ptzAll.LocalTimeToGMT(MaxLocalRecordTime,PlotterTZLocation);
                    end
                    else begin
                      MinPlotTime:=MinLocalRecordTime;
                      MaxPlotTime:=MaxLocalRecordTime;
                    end;
               end;

               { Rescale X axis because use of MPSAS flags stored in X[1] caused bad x axis ranging.}
               Chart1.Extent.XMin:=MinPlotTime;
               Chart1.Extent.UseXMin:=True;

               while (PlotLocalRecordDate<=MaxLocalRecordDate+1) do begin
                   {Get UTC value for this Local date}
                   try
                     PlotUTCRecordDate := ptzAll.LocalTimeToGMT(PlotLocalRecordDate,PlotterTZLocation);
                   except
                     on E: Exception do begin
                       ShowMessage( 'Error: '+ E.ClassName + #13#10 + '>' + str + '<' + #13#10 + E.Message );
                     end;
                   end;

                   try

                     if SunTwilightVisible then begin
                      case TimeReference of
                        'UTC': begin //UTC time stamps
                            TwilightMorningSunTimeStamp:=Sun_Rise(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude);
                            TwilightEveningSunTimeStamp:=Sun_Set(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude);
                        end;
                        else begin //local time stamps
                            TwilightMorningSunTimeStamp:=ptzAll.GMTToLocalTime(Sun_Rise(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude),PlotterTZLocation, subfix);
                            TwilightEveningSunTimeStamp:=ptzAll.GMTToLocalTime(Sun_Set(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude),PlotterTZLocation, subfix);
                        end;
                      end;
                      { Check if computed twilight line falls within min/max record time }
                      if ((TwilightMorningSunTimeStamp>=MinPlotTime) and (TwilightMorningSunTimeStamp<=MaxPlotTime)) then begin
                       {Plot morning Sunrise line}
                       SunTwilightSeries.AddNull();
                       SunTwilightSeries.AddXY(TwilightMorningSunTimeStamp,TwilightMax,'',clDefault);
                       SunTwilightSeries.AddXY(TwilightMorningSunTimeStamp,TwilightMin,'',clDefault);
                      end;
                      if ((TwilightEveningSunTimeStamp>=MinPlotTime) and (TwilightEveningSunTimeStamp<=MaxPlotTime)) then begin
                       {Plot evening Sunset line}
                       SunTwilightSeries.AddNull();
                       SunTwilightSeries.AddXY(TwilightEveningSunTimeStamp,TwilightMin,'',clDefault);
                       SunTwilightSeries.AddXY(TwilightEveningSunTimeStamp,TwilightMax,'',clDefault);
                      end;
                     end;

                     {Plot morning Civil point}
                     if CivilTwilightVisible then begin
                      case TimeReference of
                        'UTC': begin //UTC time stamps
                            TwilightMorningCivilTimeStamp:=Morning_Twilight_Civil(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude);
                            TwilightEveningCivilTimeStamp:=Evening_Twilight_Civil(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude);
                        end;
                        else begin //local time stamps
                            TwilightMorningCivilTimeStamp:=ptzAll.GMTToLocalTime(Morning_Twilight_Civil(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude),PlotterTZLocation, subfix);
                            TwilightEveningCivilTimeStamp:=ptzAll.GMTToLocalTime(Evening_Twilight_Civil(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude),PlotterTZLocation, subfix);
                        end;
                      end;
                      { Check if computed twilight line falls within min/max record time }
                      if ((TwilightMorningCivilTimeStamp>=MinPlotTime) and (TwilightMorningCivilTimeStamp<=MaxPlotTime)) then begin
                       {Plot morning Civil line}
                       CivilTwilightSeries.AddNull();
                       CivilTwilightSeries.AddXY(TwilightMorningCivilTimeStamp,TwilightMax,'',clDefault);
                       CivilTwilightSeries.AddXY(TwilightMorningCivilTimeStamp,TwilightMin,'',clDefault);
                      end;
                      if ((TwilightEveningCivilTimeStamp>=MinPlotTime) and (TwilightEveningCivilTimeStamp<=MaxPlotTime)) then begin
                      {Plot evening Civil point}
                       CivilTwilightSeries.AddNull();
                       CivilTwilightSeries.AddXY(TwilightEveningCivilTimeStamp,TwilightMin,'',clDefault);
                       CivilTwilightSeries.AddXY(TwilightEveningCivilTimeStamp,TwilightMax,'',clDefault);
                      end;
                     end;

                     {Plot morning Nautical point}
                     if NauticalTwilightVisible then begin
                      case TimeReference of
                        'UTC': begin //UTC time stamps
                            TwilightMorningNauticalTimeStamp:=Morning_Twilight_Nautical(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude);
                            TwilightEveningNauticalTimeStamp:=Evening_Twilight_Nautical(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude);
                        end;
                        else begin //local time stamps
                            TwilightMorningNauticalTimeStamp:=ptzAll.GMTToLocalTime(Morning_Twilight_Nautical(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude),PlotterTZLocation, subfix);
                            TwilightEveningNauticalTimeStamp:=ptzAll.GMTToLocalTime(Evening_Twilight_Nautical(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude),PlotterTZLocation, subfix);
                        end;
                      end;
                      { Check if computed twilight line falls within min/max record time }
                      if ((TwilightMorningNauticalTimeStamp>=MinPlotTime) and (TwilightMorningNauticalTimeStamp<=MaxPlotTime)) then begin
                         {Plot morning Nautical line}
                         NauticalTwilightSeries.AddNull();
                         NauticalTwilightSeries.AddXY(TwilightMorningNauticalTimeStamp,TwilightMax,'',clDefault);
                         NauticalTwilightSeries.AddXY(TwilightMorningNauticalTimeStamp,TwilightMin,'',clDefault);
                       end;
                      if ((TwilightEveningNauticalTimeStamp>=MinPlotTime) and (TwilightEveningNauticalTimeStamp<=MaxPlotTime)) then begin
                         {Plot evening Nautical point}
                         NauticalTwilightSeries.AddNull();
                         NauticalTwilightSeries.AddXY(TwilightEveningNauticalTimeStamp,TwilightMin,'',clDefault);
                         NauticalTwilightSeries.AddXY(TwilightEveningNauticalTimeStamp,TwilightMax,'',clDefault);
                       end;
                     end;

                     {Plot morning Astronmical point}
                     if AstronomicalTwilightVisible then begin
                      case TimeReference of
                        'UTC': begin //UTC time stamps
                            TwilightMorningAstronomicalTimeStamp:=Morning_Twilight_Astronomical(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude);
                            TwilightEveningAstronomicalTimeStamp:=Evening_Twilight_Astronomical(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude);
                        end;
                        else begin //local time stamps
                            TwilightMorningAstronomicalTimeStamp:=ptzAll.GMTToLocalTime(Morning_Twilight_Astronomical(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude),PlotterTZLocation, subfix);
                            TwilightEveningAstronomicalTimeStamp:=ptzAll.GMTToLocalTime(Evening_Twilight_Astronomical(PlotUTCRecordDate,MyLatitude,-1.0 * MyLongitude),PlotterTZLocation, subfix);
                        end;
                      end;
                      { Check if computed twilight line falls within min/max record time }
                      if ((TwilightMorningAstronomicalTimeStamp>=MinPlotTime) and (TwilightMorningAstronomicalTimeStamp<=MaxPlotTime)) then begin
                         {Plot morning Astronomical line}
                         AstronmicalTwilightSeries.AddNull();
                         AstronmicalTwilightSeries.AddXY(TwilightMorningAstronomicalTimeStamp,TwilightMax,'',clDefault);
                         AstronmicalTwilightSeries.AddXY(TwilightMorningAstronomicalTimeStamp,TwilightMin,'',clDefault);
                       end;
                      if ((TwilightEveningAstronomicalTimeStamp>=MinPlotTime) and (TwilightEveningAstronomicalTimeStamp<=MaxPlotTime)) then begin
                         {Plot evening Astronmical point}
                         AstronmicalTwilightSeries.AddNull();
                         AstronmicalTwilightSeries.AddXY(TwilightEveningAstronomicalTimeStamp,TwilightMin,'',clDefault);
                         AstronmicalTwilightSeries.AddXY(TwilightEveningAstronomicalTimeStamp,TwilightMax,'',clDefault);
                      end;
                    end;

                   except
                     StatusMessage('Plotter: Twilight calculation exception');
                   end;

                   {Next day}
                   PlotLocalRecordDate:=IncDay(PlotLocalRecordDate);
               end;
               {--- End: Plot twilight lines ---}

             end; //end of plottable twilight

             { Show Sun/Moon elevation axis if necessary}
             Chart1.AxisList[2].Visible:= (SunVisible or MoonVisible);


   end;//End of .dat plotting

   else begin end;

   end;
   screen.Cursor:= crDefault;
   if Assigned(pieces) then FreeAndNil(pieces);

end;

{ User requests file to be plotted }
procedure TPlotterForm.PlotFilesStringGridClick(Sender: TObject);
begin
   ReplotButtonClick(nil);
end;

{ Custom sorting routine for string/number/date}
procedure TPlotterForm.PlotFilesStringGridCompareCells(Sender: TObject; ACol, ARow,
  BCol, BRow: Integer; var Result: integer);
begin

  { Initially (without an actual click), the SortColumn is -1. And,
    any program initiated sort done then will still call this routine.}

  case PlotFilesStringGrid.SortColumn of
  -1,0: Result:=CompareText(PlotFilesStringGrid.Cells[ACol,ARow],PlotFilesStringGrid.Cells[BCol,BRow]);
  1: begin //numeric sort for column 1 (numerical)
      // Result will be either <0, =0, or >0 for normal order.
        result := StrToIntDef(PlotFilesStringGrid.Cells[ACol,ARow],0)-StrToIntDef(PlotFilesStringGrid.Cells[BCol,BRow],0);
     end;
  2: begin //Date compare
      try  //In case date is unreadable or empty
        result := DateTimeToUnix(StrToDateTime(PlotFilesStringGrid.Cells[ACol,ARow],FDateSettings))
               - DateTimeToUnix(StrToDateTime(PlotFilesStringGrid.Cells[BCol,BRow],FDateSettings));
      except
        StatusMessage('Plotter: PlotFilesStringGridCompareCells exception');
      end;
     end;
  end;

  // For inverse order, just negate the result (eg. based on grid's SortOrder).
  if PlotFilesStringGrid.SortOrder = soDescending then
     result := -result;

end;

{ User requests sorting }
procedure TPlotterForm.PlotFilesStringGridHeaderClick(Sender: TObject;
  IsColumn: Boolean; Index: Integer);
begin
   if IsColumn then
    PlotFilesStringGrid.SortColRow(True, Index);

end;

procedure TPlotterForm.SaveChart(FileType:String);
var
  fs: TFileStream;
  id: IChartDrawer;
  fn: String; // Filename
  AllowFlag:Boolean=false; //Allow writing file
  MessageString:String;
begin

  { Save to same location as log file was retrieved from.
    Filename resembles chart title.}
  fn:=RemoveMultiSlash(
          PlotFileDirectory+
          DirectorySeparator+
          ExtractFileNameOnly(SelectedFilename)+
          '.'+
          FileType
          );

  //Check that file does not already exist
  if FileExists(fn) then begin
    MessageString:=fn+ ' exists!';
    StatusMessage(MessageString);
    case QuestionDlg('Plot image file exists',MessageString,mtCustom,[mrOK,'Overwrite',mrCancel,'Cancel'],'') of
         mrOK: begin
               AllowFlag:=True; //User allowed overwriting file.
               StatusMessage('Plot image file ('+fn+') exists, user allowed overwriting.');
         end;
         mrCancel: begin
               StatusMessage('plot image file ('+fn+') exists already, user cancelled overwrite.');
           end;
    end;
  end
  else AllowFlag:=True; //File did not exist, allow writing.

  if AllowFlag then begin
    { Write file. }

    case FileType of
         'svg': begin
           fs := TFileStream.Create(fn, fmCreate);
            try
              id := TSVGDrawer.Create(fs, true);
              with Chart1 do
                Draw(id, Rect(0, 0, Width, Height));
            finally
              fs.Free;
            end;
         end;

         'png':   Chart1.SaveToFile(TPortableNetworkGraphic, fn);

    end;

    { Indicate where plot file got saved }
    ShowMessage('File saved to:"' + sLineBreak + fn );
  end;
end;

procedure TPlotterForm.CivilTwilightCheckBoxEditingDone(Sender: TObject);
begin
  CivilTwilightVisible:=CivilTwilightCheckBox.Checked;
  {Save setting for future}
  vConfigurations.WriteBool(PlotterINIsection,'CivilTwilight',CivilTwilightVisible);

  if CivilTwilightVisible then
     CivilTwilightSeries.LinePen.Style:=psSolid
  else
     CivilTwilightSeries.LinePen.Style:=psClear;
end;

procedure TPlotterForm.ContinuousLineCheckBoxEditingDone(Sender: TObject);
begin
  ContinuousLine:=ContinuousLineCheckBox.Checked;
  {Save setting for future}
  vConfigurations.WriteBool(PlotterINIsection,'ContinuousLine',ContinuousLine);
end;

procedure TPlotterForm.DarknessCheckBoxEditingDone(Sender: TObject);
begin
  if not UpdatingGUI then begin
    DarknessVisible:=DarknessCheckBox.Checked;
    {Save setting for future}
    vConfigurations.WriteBool(PlotterINIsection,'DarknessVisible',DarknessVisible);

    ReadingText.Visible:=DarknessVisible;
    ReadingTextLabel.Visible:=DarknessVisible;

    if DarknessVisible then begin
     MPSASSeries.LinePen.Style:=psSolid;
     MPSASSeries2.LinePen.Style:=psSolid;
     MPSASSeries3.LinePen.Style:=psSolid;
    end
    else begin
     MPSASSeries.LinePen.Style:=psClear;
     MPSASSeries2.LinePen.Style:=psClear;
     MPSASSeries3.LinePen.Style:=psClear;
    end;
  end;
end;

procedure TPlotterForm.AstronomicalTwilightCheckBoxEditingDone(Sender: TObject);
begin
  AstronomicalTwilightVisible:=AstronomicalTwilightCheckBox.Checked;
  {Save setting for future}
  vConfigurations.WriteBool(PlotterINIsection,'AstronomicalTwilight',AstronomicalTwilightVisible);

  if AstronomicalTwilightVisible then
     AstronmicalTwilightSeries.LinePen.Style:=psSolid
  else
     AstronmicalTwilightSeries.LinePen.Style:=psClear;
end;

procedure TPlotterForm.UpdateEditChart();
var
  ControlKeyString: String;
begin
 Chart1.Invalidate;  {Refresh plot}

 SelectionModeIndicator.Brush.Color:=ifthen(PendingDeletionMode, clRed, clDefault);

 PendingDeletionMemo.Lines.Clear;

 ControlKeyString:=ifthen(AnsiContainsStr(OperatingSystemString,'Darwin'),'Cmd','Ctrl');
 PendingDeletionMemo.Lines.Add(ifthen(PendingDeletionMode,
   'Select last record of selection.'+sLineBreak+'Esc - to quit.',
   'Select first record start selection.'+sLineBreak+ControlKeyString+'-Z - to undo last delete.'));

end;

procedure TPlotterForm.ChartToolset1DataPointClickTool1PointClick(
  ATool: TChartTool; APoint: TPoint);
var
  Index: Integer = 0;
  //MyRecord: PChartDataItem;
  DeleteKeep: Boolean = True;
  TempSwap: LongInt;
begin

  {Press [sskey]-click once to start deletion marking range}
  {Press [sskey]-click another time to end deletion marking range}
  //WriteLnDebug(inttostr(PointedToRecord)); //***debug
  if InRange(PointedToRecord,0,MPSASSeries.Count-1) then begin

  {Grab first record index}
  if (not PendingDeletionMode) then begin
     StartPendingDeletionIndex:=PointedToRecord;
     MPSASSeries.XValues[StartPendingDeletionIndex, 1] := ord(symbolStartDelete); //Mark first point
     PendingDeletionMode:=True;
  end else  {Highlight all points that need to be deleted}
  begin
     DeleteKeep:=(EditModeRadios.ItemIndex=0); //Mode of operation; Delete or Keep records.
     EndPendingDeletionIndex:=PointedToRecord; //Grab last record index
     if StartPendingDeletionIndex>EndPendingDeletionIndex then begin
      TempSwap:=EndPendingDeletionIndex;
      EndPendingDeletionIndex:=StartPendingDeletionIndex;
      StartPendingDeletionIndex:=TempSwap;
     end;

     {Save deletion points for Undo}
     LastStartPendingDeletionIndex:=StartPendingDeletionIndex;
     LastEndPendingDeletionIndex:=EndPendingDeletionIndex;

     for Index:=StartPendingDeletionIndex to EndPendingDeletionIndex do begin
       //MyRecord := ListChartSource1.Item[Index]; // Access the record
       {Records only contain: X,Y,Text,Color
        The color is used to indicate features like clWhite=Initial, clRed=Subsequent}
       MPSASSeries.XValues[Index, 1] := ifthen(DeleteKeep,ord(symbolPendingDelete),ord(symbolKeep));
     end;
     PendingDeletionMode:=False;
  end;

  UpdateEditChart;

  end;

end;

//Added tool for updating record index on screen

procedure TPlotterForm.ChartToolset1DataPointClickTool2AfterMouseMove(
  ATool: TChartTool; APoint: TPoint);
var
  Params: TNearestPointParams;
  Results: TNearestPointResults;
  Index: Integer;
  Series: TLineSeries;
  MyRecord: PChartDataItem;

begin
  Series := Chart1.Series[0] as TLineSeries;
  Params.FPoint := APoint;
  Params.FRadius := 20;//10;         // Maximum distance in pixels to consider "nearest"
  //Params.FDistFunc := @PointDistX;; // Use default distance function //was grabbing wrong points
  Params.FDistFunc := @PointDistSq; //Distance function
  Params.FTargets := [nptPoint];

  RecordNumberText.Caption:='starting';
  if Series.GetNearestPoint(Params, Results) then begin
    Index := Results.FIndex; //Get the Index number from the result
    RecordNumberText.Caption:=IntToStr(Index); // Show the Index number
    MyRecord := ListChartSource1.Item[Index]; // Access the record
    PointedToRecord:=Index;
    RecordText.Text:=MyRecord^.Text; // Accesses the .Text item

    // Use Index to access XValue[Index] and YValues[Index, 0]
  end else begin
      RecordNumberText.Caption:='';
  end;

end;

procedure TPlotterForm.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
var
    Index: Integer = 0;
    DeleteKeep: Boolean = True;
begin
   {Ctrl-Z undo last edit}

   if (Key = Ord('Z')) and (
   (AnsiContainsStr(OperatingSystemString,'MacOSX' ) and (ssMeta in Shift)) or  //Mac uses ssmeta (CMD key)
   (AnsiContainsStr(OperatingSystemString,'Windows') and (ssCtrl in Shift)) or
   (AnsiContainsStr(OperatingSystemString,'Linux'  ) and (ssCtrl in Shift))) then begin
     if (not PendingDeletionMode and InRange(LastStartPendingDeletionIndex,0,MPSASSeries.Count-1) and InRange(LastEndPendingDeletionIndex,0,MPSASSeries.Count-1)) then begin
      DeleteKeep:=(EditModeRadios.ItemIndex=0); //Mode of operation; Delete or Keep records.
      StartPendingDeletionIndex:=LastStartPendingDeletionIndex;
      EndPendingDeletionIndex:=LastEndPendingDeletionIndex;

      for Index:=StartPendingDeletionIndex to EndPendingDeletionIndex do begin
        //MyRecord := ListChartSource1.Item[Index]; // Access the record
        MPSASSeries.XValues[Index, 1] := ifthen(DeleteKeep,ord(symbolKeep),ord(symbolPendingDelete));
      end;
      UpdateEditChart;
     end;
   end;

   {Escape key, unmarks first edit point}
   if (Key = VK_ESCAPE) then begin
     if (PendingDeletionMode and InRange(StartPendingDeletionIndex,0,MPSASSeries.Count-1)) then begin
        MPSASSeries.XValues[StartPendingDeletionIndex, 1] := ord(symbolKeep); //Unmark first point
        PendingDeletionMode:=False;
        UpdateEditChart;
     end;
   end;
 end;


procedure TPlotterForm.PlotterEditKeyShift();
var
    PlotEditClickType: TShiftStateEnum;
    Shift: TShiftState;
    Result: String = '';
begin

   case PlotEditClickKeyCombo.Text of
        'Left':PlotEditClickType:=ssLeft;
        'Middle':PlotEditClickType:=ssMiddle;
        'Right':PlotEditClickType:=ssRight;
        else StatusMessage('No Plot-edit ClickType selected.');
   end;

   case PlotEditModifierKeyCombo.Text of
        'None': ChartToolset1DataPointClickTool1.Shift:=[PlotEditClickType];
        'CMD/WIN': ChartToolset1DataPointClickTool1.Shift:=[ssMeta,PlotEditClickType];
        'SHIFT': ChartToolset1DataPointClickTool1.Shift:=[ssShift,PlotEditClickType];
        'CTRL': ChartToolset1DataPointClickTool1.Shift:=[ssCtrl,PlotEditClickType];
        'ALT': ChartToolset1DataPointClickTool1.Shift:=[ssAlt,PlotEditClickType];
        else StatusMessage('No Plot-edit Modifier Key selected');
   end;

   Shift:=ChartToolset1DataPointClickTool1.Shift;
   if ssShift in Shift then Result := Result + 'ssShift ';
   if ssCtrl in Shift then Result := Result + 'ssCtrl ';
   if ssAlt in Shift then Result := Result + 'ssAlt ';
   if ssMeta in Shift then Result := Result + 'ssMeta ';
   if ssLeft in Shift then Result := Result + 'ssLeft ';
   if ssMiddle in Shift then Result := Result + 'ssMiddle ';
   if ssRight in Shift then Result := Result + 'ssRight ';

   StatusMessage('Plot edit key modifiers changed to: ' + Trim(Result));
   vConfigurations.WriteString(PlotterINIsection,'PlotEditModifiers',Trim(Result));

end;

procedure TPlotterForm.PlotEditModifierKeyComboChange(Sender: TObject);
begin
 if not UpdatingGUI then
     PlotterEditKeyShift();
end;

procedure TPlotterForm.PlotEditClickKeyComboChange(Sender: TObject);
begin
 if not UpdatingGUI then
     PlotterEditKeyShift();
end;

procedure TPlotterForm.DayUpDownClick(Sender: TObject; Button: TUDBtnType);
begin
if Button=btNext then
 inc(TimeOffset,86400)
 else
 dec(TimeOffset,86400);

TimeOffsetSpinEdit.Text:=IntToStr(TimeOffset);

ReplotButtonClick(nil);
end;

{ Show/Hide control panel }
procedure TPlotterForm.ControlPanelButtonClick(Sender: TObject);
begin
 ControlPanel.Visible:=not ControlPanel.Visible;
 ControlPanelButton.Hint:=ifthen(ControlPanel.Visible,'Hide control panel','Show control panel');
 ControlPanelButton.Caption:=ifthen(ControlPanel.Visible,'<','>');
end;

procedure TPlotterForm.ChartToolset1PanDragTool1AfterMouseUp(ATool: TChartTool;
  APoint: TPoint);
begin
  { TODO : try to reset sun/moon y axis }
  //SunMoonChartAxisTransformationsAutoScaleAxisTransform1.;
end;

(*
Notes for editing and trimming datapoints:

When loading plot, check for non-incrementing timestamps, offer truncate the garbage or continue, options:
 - Stop loading
 - Ignore time inconbsitencies and continue loading

If adding new files to the directory, rescan the directory.

Data points should have an extra hidden attribute that stores the entire record text that can be exported later.
 - there are more (AddNull) data points that have been added to make unconnected plot lines at "initial" and breaks in recording.

Editor:
Start up editor, for visibility, change all data points from Series-Pointer-Style = psNone to psCircle
When deleting any point, change the next point to "Initial" (and maybe add a dummy point)
  so that it does not join to previous "Subsequent" point.
Use mouse shiftstyle (like ctrl or alt) to start and end block selection
    - change point style to psCross to indicate pending action like delete.
    - get the range of indexes for deletion
Delete points in a sequence backwards to avoid losing the index.

Export:
 - The header would have to have been saved.
 - Run through all points in the plot and write out the extra attribute that the original record text was stored in.

Undo is a nightmare, leave that for a rainy month.
*)
{Export the Header and non-deleted records.}
procedure TPlotterForm.ExportButtonClick(Sender: TObject);
var
 Index: Integer = 0;
 MyRecord: PChartDataItem;
 ExportFilename: AnsiString;
 MyFile: Text;
 symbol: TSeriesPointerStyle;
begin
     {Confirm filename to use.}
     { - Option: ofOverwritePrompt=true checks if filename already exists}

     {Suggest a new name }
     ExportDialog.FileName:=
       ExtractFilePath(SelectedFilename)+
       ExtractFileNameOnly(SelectedFilename)+
       '_exported'+
       ExtractFileExt(SelectedFilename);

     {Export to file}
     if ExportDialog.Execute then begin
      ExportFilename:=ExportDialog.FileName;

        {Write previously saved header}
        HeaderStrings.LineBreak := #13#10; // Force CRLF
        HeaderStrings.SaveToFile(ExportFileName);

        AssignFile(MyFile, ExportFileName);
        try
         Append(MyFile);  // Opens file in append mode
         {Write previously loaded and edited non-deleted records to file}
         for Index:=0 to MPSASSeries.Count-1 do begin
            MyRecord := ListChartSource1.Item[Index]; // Access the record
            //Check if record needs to be skipped or not
            {Records only contain: X,Y,Text,Color
             The color is used to indicate features like clWhite=Initial, clRed=Subsequent}
             symbol:=psDiagCross;
             if (MPSASSeries.XValues[Index, 1] <> ord(symbol)) then begin
                if (MyRecord^.Text <> '') then
                  Write(MyFile, MyRecord^.Text + #13#10); // Append the .Text item, and force windows compatible CRLF
             end;
         end;
        finally
         CloseFile(MyFile);
         AddFilesToList(PlotFileDirectory); //Update the folder listing
        end;

     end;

end;

procedure TPlotterForm.HourUpDownClick(Sender: TObject; Button: TUDBtnType);
begin
  if Button=btNext then
   inc(TimeOffset,3600)
   else
   dec(TimeOffset,3600);

  TimeOffsetSpinEdit.Text:=IntToStr(TimeOffset);

  ReplotButtonClick(nil);

end;

procedure TPlotterForm.MinuteUpDownClick(Sender: TObject; Button: TUDBtnType);
begin
 if Button=btNext then
  inc(TimeOffset,60)
  else
  dec(TimeOffset,60);

 TimeOffsetSpinEdit.Text:=IntToStr(TimeOffset);

 ReplotButtonClick(nil);
end;

procedure TPlotterForm.PageControl1Change(Sender: TObject);
begin
  if PageControl1.ActivePage = EditTabSheet then begin
   MPSASSeries.Pointer.HorizSize:=5; //10 was too big
   MPSASSeries.Pointer.VertSize:=MPSASSeries.Pointer.HorizSize; //square aspect ratio
   ChartToolset1DataPointCrosshairTool1.CrosshairPen.Width:=5;
   ChartToolset1DataPointCrosshairTool1.CrosshairPen.Style:=psSolid;
   Chart1.Invalidate;  {Refresh plot}
  end else begin
     MPSASSeries.Pointer.HorizSize:=2; //return to normal size
     MPSASSeries.Pointer.VertSize:=MPSASSeries.Pointer.HorizSize; //square aspect ratio
     ChartToolset1DataPointCrosshairTool1.CrosshairPen.Width:=1;
     ChartToolset1DataPointCrosshairTool1.CrosshairPen.Style:=psDashDot;
     Chart1.Invalidate;  {Refresh plot}
  end;
end;

procedure CopySeries(SourceSeries, TargetSeries: TLineSeries);
begin
  // Ensure the target series uses a ListChartSource
  if not (TargetSeries.Source is TListChartSource) then
    TargetSeries.Source := TListChartSource.Create(TargetSeries);

  // Copy the data points
  TListChartSource(TargetSeries.Source).CopyFrom(SourceSeries.ListSource);

  // Optionally copy visual properties
  TargetSeries.LinePen := SourceSeries.LinePen;
  TargetSeries.Pointer := SourceSeries.Pointer;
end;


procedure TPlotterForm.PreviewButtonChange(Sender: TObject);
var
    Index: LongInt = 0; //General purpose counter
    ResIndex: LongInt = 0; //General purpose counter
    symbol: TSeriesPointerStyle;
begin

  PreviewMode:=PreviewButton.Checked;
  EditModeRadios.Enabled:=not PreviewMode;
  ExportButton.Enabled:=not PreviewMode;

  {Preview: redraw plot without deletions
   Edit: redraw plot with deletions marked as X}
  if (PreviewMode) then begin
      WriteLnDebug(' ListChartSource1.Count = '+ IntToStr(ListChartSource1.Count));
      WriteLnDebug('       VoltSeries.Count = '+ IntToStr(VoltSeries.Count));
      WriteLnDebug('TemperatureSeries.Count = '+ IntToStr(TemperatureSeries.Count));
     {Backup series}
     ListChartSource1Backup.XCount:=2;
     ListChartSource1Backup.CopyFrom(MPSASSeries.Source);
     CopySeries(VoltSeries, VoltSeriesBackup);
     CopySeries(TemperatureSeries, TemperatureSeriesBackup);

     {Prepare LineSeries for new records}
     MPSASSeries.Clear;
     ListChartSource1.Clear;
     VoltSeries.Clear;
     TemperatureSeries.Clear;

     {Fill chart with edited records only}
     WriteLnDebug(' ListChartSource1Backup.Count = '+ IntToStr(ListChartSource1Backup.Count));
     WriteLnDebug('       VoltSeriesBackup.Count = '+ IntToStr(VoltSeriesBackup.Count));
     WriteLnDebug('TemperatureSeriesBackup.Count = '+ IntToStr(TemperatureSeriesBackup.Count));
     for Index:=0 to ListChartSource1Backup.Count-1 do begin
        {Check if record needs to be skipped or not. "Delete" Symbol indicates pending deletion}
         symbol:=psDiagCross;
         { Loop through all points in the source and add them to the destination}
         if (ListChartSource1Backup.Item[Index]^.XList[0] <> ord(symbol)) then begin
               with ListChartSource1Backup.Item[Index]^ do begin
                 ResIndex:=ListChartSource1.Add(X, Y, Text, Color);
                 ListChartSource1.Item[ResIndex]^.XList[0]:=ListChartSource1Backup.Item[Index]^.XList[0];
                 VoltSeries.AddXY(X, VoltSeriesBackup.GetYValue(Index));
                 TemperatureSeries.AddXY(X, TemperatureSeriesBackup.GetYValue(Index));
               end;
            end;
         end;

  end else begin //Must be in Edit mode
     {Restore series}
     MPSASSeries.ListSource.CopyFrom(ListChartSource1Backup);
     CopySeries(VoltSeriesBackup, VoltSeries);
     CopySeries(TemperatureSeriesBackup, TemperatureSeries);
  end;

  Chart1.Invalidate;  {Refresh plot}

end;

procedure TPlotterForm.RefreshDirectoryButtonClick(Sender: TObject);
begin
      AddFilesToList(PlotFileDirectory);
end;

procedure TPlotterForm.TimeOffsetSpinEditChange(Sender: TObject);
begin
  TimeOffset:=StrToInt64Def(TimeOffsetSpinEdit.Text,0);
  //removed because replotting takes a long time for simple one second increments, and also the value cannot be entered manually since it triggers the change event.
  //ReplotButtonClick(nil);
end;

procedure TPlotterForm.ClipSunMoonCheckBoxEditingDone(Sender: TObject);
begin
  ClipSunMoon:=ClipSunMoonCheckBox.Checked;
  {Save setting for future}
  vConfigurations.WriteBool(PlotterINIsection,'ClipSunMoon',ClipSunMoon);
end;

procedure TPlotterForm.NauticalTwilightCheckBoxEditingDone(Sender: TObject);
begin
  NauticalTwilightVisible:=NauticalTwilightCheckBox.Checked;
  {Save setting for future}
  vConfigurations.WriteBool(PlotterINIsection,'NauticalTwilight',NauticalTwilightVisible);

  if NauticalTwilightVisible then
     NauticalTwilightSeries.LinePen.Style:=psSolid
  else
     NauticalTwilightSeries.LinePen.Style:=psClear;
end;

procedure TPlotterForm.SavePNGButtonClick(Sender: TObject);
begin
  SaveChart('png');
end;

procedure TPlotterForm.SaveSVGButtonClick(Sender: TObject);
begin
  SaveChart('svg');
end;

procedure TPlotterForm.SunCheckBoxEditingDone(Sender: TObject);
begin
  if not UpdatingGUI then begin
    SunVisible:=SunCheckBox.Checked;
    {Save setting for future}
    vConfigurations.WriteBool(PlotterINIsection,'Sun',SunVisible);

    if SunVisible then begin
      SunSeries.LinePen.Style:=psSolid;
      SunSeries.LinePen.Width:=1;
    end else begin
      SunSeries.LinePen.Style:=psClear;
      SunSeries.LinePen.Width:=0;
    end;

    SunText.Visible:=SunVisible;
    SunTextLabel.Visible:=SunVisible;

    Chart1.AxisList[2].Visible:= (SunVisible or MoonVisible);

  end;
end;

procedure TPlotterForm.SunTwilightCheckBoxEditingDone(Sender: TObject);
begin
  SunTwilightVisible:=SunTwilightCheckBox.Checked;
  {Save setting for future}
  vConfigurations.WriteBool(PlotterINIsection,'SunTwilight',SunTwilightVisible);

  if SunTwilightVisible then
     SunTwilightSeries.LinePen.Style:=psSolid
  else
     SunTwilightSeries.LinePen.Style:=psClear;
end;

procedure TPlotterForm.TemperatureCheckBoxEditingDone(Sender: TObject);
begin
  if not UpdatingGUI then begin
    TemperatureVisible:=TemperatureCheckBox.Checked;
    {Save setting for future}
    vConfigurations.WriteBool(PlotterINIsection,'Temperature',TemperatureVisible);

    TemperatureReadingText.Visible:=TemperatureVisible;
    TemperatureReadingTextLabel.Visible:=TemperatureVisible;
    Chart1.AxisList[4].Visible:=TemperatureVisible;

    if TemperatureVisible then
     TemperatureSeries.LinePen.Style:=psSolid
    else
     TemperatureSeries.LinePen.Style:=psClear;
  end;

end;

procedure TPlotterForm.TimeRefGroupClick(Sender: TObject);
begin
   if not UpdatingGUI then begin
    if TimeRefGroup.ItemIndex=0 then
      TimeReference:='UTC'
    else
      TimeReference:='Local';

    { Save Time reference setting. }
    vConfigurations.WriteString(PlotterINIsection,'TimeReference',TimeReference);

    { Change chart legend accordingly. }
    case TimeReference of
      'UTC': Chart1.AxisList[0].Title.Caption:='Sample time (UTC)';
      'Local': Chart1.AxisList[0].Title.Caption:='Sample time (Local)';
    end;

    {Update plot when Time reference has been changed}
    ReplotButtonClick(nil);
  end;//End of updating check
end;

//procedure TPlotterForm.VerticalSplitterMoved(Sender: TObject);
//begin
//  SplitterPosLeft:=VerticalSplitter.Left;
//
//  { Save splitter position for restarting }
//  vConfigurations.WriteString(PlotterINIsection,'SplitterPosLeft',IntToStr(SplitterPosLeft));
//end;
//

{!!! Maybe keep this !!!}
//procedure TPlotterForm.ThreeDayCheckBoxClick(Sender: TObject);
//begin
// ShellListView1Click(nil);
//end;

procedure TPlotterForm.TZLocationBoxChange(Sender: TObject);
begin
  if not TZChanging then begin
     TZChanging:=True;
     { Save the TZ selection. }
     PlotterTZLocation:=TZLocationBox.Text;
     Application.ProcessMessages;
     vConfigurations.WriteString(PlotterINIsection,'Local time zone',PlotterTZLocation);
     TZChanging:=False;
  end;
end;

procedure TPlotterForm.TZRegionBoxChange(Sender: TObject);
begin
 if not TZChanging then begin
   TZChanging:=True;

   { Get and save region }
   PlotterTZRegion:=TZRegionBox.Text;
   Application.ProcessMessages; //Wait for GUI to put screen text into variable.
   vConfigurations.WriteString(PlotterINIsection,'Local region',PlotterTZRegion); //Save TZ Region

   { Fill up timezone location names }
   FillTimezones();

   { Clear out selected time zone location because time zone region was just changed. }
   PlotterTZLocation:='';
   TZLocationBox.Text:=PlotterTZLocation;
   vConfigurations.WriteString(PlotterINIsection,'Local time zone',PlotterTZLocation);

   TZChanging:=False;
 end;
end;

procedure TPlotterForm.VoltageCheckBoxEditingDone(Sender: TObject);
begin

  if not UpdatingGUI then begin
    VoltageVisible:=VoltageCheckBox.Checked;
    {Save setting for future}
    vConfigurations.WriteBool(PlotterINIsection,'Voltage',VoltageVisible);

    VoltageText.Visible:=VoltageVisible;
    VoltageTextLabel.Visible:=VoltageVisible;
    Chart1.AxisList[3].Visible:=VoltageVisible;

    if VoltageVisible then
     VoltSeries.LinePen.Style:=psSolid
    else
     VoltSeries.LinePen.Style:=psClear;

  end;
end;

procedure TPlotterForm.WeekUpDownClick(Sender: TObject; Button: TUDBtnType);
begin
 if Button=btNext then
    inc(TimeOffset,604800)
 else
 dec(TimeOffset,604800);

TimeOffsetSpinEdit.Text:=IntToStr(TimeOffset);

ReplotButtonClick(nil);
end;

procedure TPlotterForm.ZeroPlotCheckBoxEditingDone(Sender: TObject);
begin
 ZeroPlot:=ZeroPlotCheckBox.Checked;
 vConfigurations.WriteBool(PlotterINIsection,'ZeroPlot',ZeroPlot);
end;

procedure TPlotterForm.AddFilesToList(FilePathName : String);
var
  FileSize : integer = 0; {Filesize, undetermined = 0}
  Row : integer = 1; {Start at row 1}
  FileName, FilePath, FileDate : string;
  sr : TSearchRec;
  PlotFileFilter : String;
begin
  FilePath := ExtractFilePath(FilePathName + DirectorySeparator);
  PlotFileFilter:=LeftStr(FilterComboBox.Text,5);
  PlotFilesStringGrid.RowCount:=1; {Reset file list}

  {Check if any files match criteria}
  if FindFirstUTF8(FilePath+PlotFileFilter,faAnyFile,sr)=0 then
    repeat
      {Get formatted file properties}
      FileName := ExtractFileName(sr.Name);
      FileSize := sr.Size;
      FileDate:=DateTimeToStr(FileDateToDateTime(sr.Time),FDateSettings);
      {Display found filename and timestamp}
      PlotFilesStringGrid.InsertRowWithValues(Row,[FileName, IntToStr(FileSize), FileDate ]);
      {Prepare for next file display}
      Row := Row + 1;
    until FindNextUTF8(sr)<>0;
  FindCloseUTF8(sr);

  {Initial sorting}
  PlotFilesStringGrid.SortColRow(true, 0);

end;

procedure TPlotterForm.FileSelect();
var
    InFilename,SelectedDateFile, PreviousDateFile,NextDateFile:String;
    File1: TextFile;
    Str: String;
    pieces: TStringList;

    MoonElevation: extended = 0.0;
    MoonAzimuth: extended = 0.0;

    UTCRecord, SelectedDate, PreviousDate, NextDate :TDateTime;
    PreviousDayValid : Boolean = False;
    SelectedDayValid : Boolean = False;
    NextDayValid : Boolean = False;
    MultiFileLoaded: Boolean = False;

    { 25 possible multi-plots are available with repeating line styles and cycling colors.}
    PlotLineStyles : array[0..24] of TFPPenStyle = (
                   psSolid, psDash, psDot, psDashDot, psDashDotDot,
                   psSolid, psDash, psDot, psDashDot, psDashDotDot,
                   psSolid, psDash, psDot, psDashDot, psDashDotDot,
                   psSolid, psDash, psDot, psDashDot, psDashDotDot,
                   psSolid, psDash, psDot, psDashDot, psDashDotDot
                   );
    PlotLineColors : array[0..24] of TColor = (
                   clRed, clGreen, clBlue, clFuchsia, clAqua,
                   clGreen, clBlue, clFuchsia, clAqua, clRed,
                   clBlue, clFuchsia, clAqua, clRed, clGreen,
                   clFuchsia, clAqua, clRed, clGreen, clBlue,
                   clAqua, clRed, clGreen, clBlue, clFuchsia
                   );

begin
  GetCustomizations();

  CurrentDirectory:=AddBackSlash(PlotDirectoryEdit.Text);

  pieces := TStringList.Create;

  { Cross hair enabled only for one chart }
  ChartToolset1DataPointCrosshairTool1.Enabled:=(PlotNumberGroup.ItemIndex=0);
  ReadingText.Visible:=ChartToolset1DataPointCrosshairTool1.Enabled;

  InFilename:= CurrentDirectory+PlotFilesStringGrid.Cells[0,PlotFilesStringGrid.Row];

  if ((not FileExists(InFilename)) or (InFilename='')) then
    exit;

  case FileSelectionMode of

    { In Single mode, new plot for each file clicked. }
    'Single': ClearAllButtonClick(nil);

    { TODO : what to do for ACCUMULATE file mode? }
    'Accumulate':;

    { In Multiple file mode, insert newly selected file into multi-plot list }
    'Multiple': begin
       {check if selected file does not exist in list}
       for MultiIndex:=0 to MultiFileStringGrid.RowCount-1 do begin
         if MultiFileStringGrid.Cells[1,MultiIndex]=ExtractFileName(InFilename) then begin
           MultiFileLoaded:=True;
           MultiChart[MultiIndex].Clear;
         end;
       end;

       if not MultiFileLoaded then begin

        {Get new row number, was initially set to 0 rows in form.create. }
        MultiIndex:=MultiFileStringGrid.RowCount;

        {Add the new row.}
        MultiFileStringGrid.RowCount:=MultiIndex+1;

        {Put the filename into the list.}
        MultiFileStringGrid.Cells[1,MultiIndex]:=ExtractFileName(InFilename);
        {Plot the new file:
          - in MPSASSeries if row=0.
          - in a newly created series if row>0}
        MultiChart[MultiIndex]:=TLineSeries.Create(self);
        MultiChart[MultiIndex].LinePen.Width:=2;
        MultiChart[MultiIndex].LinePen.Style:=PlotLineStyles[MultiIndex];
        MultiChart[MultiIndex].SeriesColor:=PlotLineColors[MultiIndex];
        MultiChart[MultiIndex].AxisIndexY:=1; //MPSAS axis
        Chart1.AddSeries(MultiChart[MultiIndex]);
       end;
    end;
   end;

  SelectedFilename:=InFilename;
  PlotterForm.Caption:='Plotter : '+SelectedFilename;

  {.txt files (from SQM Reader) }
  {.dat files}
  if ((ExtractFileExt(InFilename)= '.txt') or (ExtractFileExt(InFilename)= '.dat')) then begin
     PlotFile(InFilename);
  end;

  {.log should have date in filename}
  if (ExtractFileExt(InFilename)= '.log') then begin
      { Date should be encoded into filename, otherwise do not plot }
      try
        SelectedDate:=ScanDateTime('yyyymmdd',LazFileUtils.ExtractFileNameOnly(InFilename));
        SelectedDateFile:=InFilename;
        SelectedDayValid:=True;
      except
        on E: Exception do begin
          ShowMessage( 'Error: '+ E.ClassName  + #13#10 + E.Message + #13#10 + 'Error trying to get date from selected filename.');
          SelectedDayValid:=False;
        end;
      end;

      //Check day before and after for more plotting around selected date
      if ThreeDayCheckBox.Checked then begin
        try
          PreviousDate:=IncDay(SelectedDate,-1);
          PreviousDateFile:=CurrentDirectory+FormatDateTime('yyyymmdd',PreviousDate)+ExtractFileExt(InFilename);
          PreviousDayValid:=True;
        except
          on E: Exception do begin
            ShowMessage( 'Error: '+ E.ClassName  + #13#10 + E.Message + #13#10 + 'Error trying to get date from previous dated filename.');
            PreviousDayValid:=False;
          end;
        end;

        try
          NextDate:=IncDay(SelectedDate);
          NextDateFile:=CurrentDirectory+FormatDateTime('yyyymmdd',NextDate)+ExtractFileExt(InFilename);
          NextDayValid:=True;
        except
          on E: Exception do begin
            ShowMessage( 'Error: '+ E.ClassName  + #13#10 + E.Message + #13#10 + 'Error trying to get date from next dated filename.');
            NextDayValid:=False;
          end;
        end;
      end;

      try
        if PreviousDayValid then PlotFile(PreviousDateFile);
        if SelectedDayValid then PlotFile(SelectedDateFile);
        if NextDayValid     then PlotFile(NextDateFile);
      except
        on E: Exception do
          ShowMessage( 'Error: '+ E.ClassName  + #13#10 + E.Message + #13#10 + 'Error trying to plot file(s).');
      end;

  end;

  if Assigned(pieces) then FreeAndNil(pieces);

//.............................................................................
//.............................................................................

 exit;
(*
  {$I+}
  try
    Reset(File1);

    repeat
      // Read one line at a time from the file.
      Readln(File1, Str);
      // Get location data from header.
      if AnsiStartsStr('# Position',Str) then begin
          //Prepare for parsing.
          pieces.Delimiter := ',';
          pieces.StrictDelimiter := True; //Do not parse spaces.
          //Remove comment from beginning of line.
          pieces.DelimitedText := AnsiRightStr(Str,length(Str) - RPos(':',Str));
          if (pieces.Count<2) then begin
              MessageDlg('Error', 'No location data found in header.', mtError, [mbOK],0);
              MessageDlg('Location Error',
                'Error: No location data found in header.',
                mtError,
                [mbOK],0);
              //LatitudeDisplay.Text:='nul';
              //LongitudeDisplay.Text:='nul';
              //break;
          end else begin
             //MyLatitude:=StrToFloat(pieces.Strings[0]);
             //if ((MyLatitude<-90.0) or (MyLatitude>90.0)) then begin
             //    WriteLnDebug('Error: Latitude ',MyLatitude,' out of range');
             //    break;
             //  end;
             //MyLongitude:=StrToFloat(pieces.Strings[1]);
             //if ((MyLongitude<-180.0) or (MyLongitude>180.0)) then begin
             //    WriteLnDebug('Error: Longitude ',MyLongitude,' out of range');
             //    break;
             //  end;
             // LatitudeDisplay.Text:=Format('%.3f',[MyLatitude]);
             // LongitudeDisplay.Text:=Format('%.3f',[MyLongitude]);
            end;
        end;

      //Get and modify record header string
      //if AnsiStartsStr('# YYYY-MM-DDTHH:mm:ss.fff;',Str) then
      //  WriteLnDebug('UTC '+AnsiRightStr(Str,length(Str) - 2)+';MoonPhaseDeg;MoonElevDeg;MoonIllum%');

      //Ignore comment lines which have # as first character.
      if not AnsiStartsStr('#',Str) then begin
          //Separate the fields of the record.
          pieces.Delimiter := ';';
          pieces.DelimitedText := Str;

          //Make sure there are enough fields
          if ((pieces.Count<5) or (pieces.Count>6)) then begin
              MessageDlg('Error', 'Incorrect number of fields in record.', mtError, [mbOK],0);
              break;
            end
          else
          begin

          //parse the fields, and convert as necessary.
          //Convert UTC string 'YYYY-MM-DDTHH:mm:ss.fff' into TDateTime
          UTCRecord:=ScanDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz',pieces.Strings[0]);

          //Calculate Moon position
          //Change sign for Moon calculations
          Moon_Position_Horizontal(
                                   StrToDateTime(DateTimeToStr(UTCRecord)),
                                   -1.0*MyLongitude,
                                   MyLatitude,
                                   MoonElevation,
                                   MoonAzimuth);

          end;//End of checking number of fields in record.

        end;
    until(EOF(File1)); // EOF(End Of File) The the program will keep reading new lines until there is none.
    CloseFile(File1);
  except
    on E: EInOutError do begin
     MessageDlg('Error', 'File handling error occurred. Details: '+E.ClassName+'/'+E.Message, mtError, [mbOK],0);
    end;
  end;
*)
end;

function FileSizeFormat(bytes:Double):string;
begin

  // below 1024 bytes
  if bytes < 1024 then
    Result:=floattostr(bytes) + 'b'

  // kilobytes; 1kb = 1024bytes
  else if (bytes >= 1024) and (bytes < 1024**2) then
    Result:=FormatFloat('0.00', bytes/1024)+'kb'

  // megabytes; 1mb = 1024*1024bytes
  else if (bytes >= 1024**2) and (bytes < 1024**3) then
    Result:=FormatFloat('0.00', bytes/1024/1024)+'Mb'

  // gigabytes; 1gb = 1024*1024*1024bytes
  else if (bytes >= 1024**3) and (bytes < 1024**4) then
    Result:=FormatFloat('0.00', bytes/1024/1024/1024)+'Gb'

  // for everything above, we show it in terrabytes
  else
    Result:=FormatFloat('0.00', bytes/1024/1024/1024/1024)+'Tb';

end;

function AddBackSlash(Instring:String):String;
begin
  Result:=Instring+DirectorySeparator;
end;

function DialogCentered(const aCaption: String;
                  const aMsg: AnsiString;
                  DlgType: TMsgDlgType;
                  Buttons: TMsgDlgButtons;
                  HelpCtx: longint;
                  Form: TForm
                  ): TModalResult;
var f: TForm;
begin
  Result := 0;
  f:=CreateMessageDialog(aMsg, DlgType, Buttons);
  try
    try
      f.Caption:=aCaption;
      f.Position:=poDesigned;
      f.Left:=PlotterForm.Left+(PlotterForm.Width div 2)-f.Width div 2;
      f.Top:=PlotterForm.Top + (PlotterForm.Height div 2)-f.Height div 2;
      Result:=f.ShowModal;
    except
      ShowMessage('Plotter: DialogCentered exception');
    end;
  finally
    f.Free;
  end;
end;

Procedure TPlotterForm.FillTimezones();
begin
  if (FileExists(appsettings.TZDirectory+PlotterTZRegion) and (length(PlotterTZRegion)>0))then begin
     ptzRegion.Destroy;
     ptzRegion := TPascalTZ.Create();
     try
       ptzRegion.ParseDatabaseFromFile(appsettings.TZDirectory+PlotterTZRegion);
     except
       ShowMessage(Format('Failed getting zones from %s',[PlotterTZRegion]));
     end;
     ptzRegion.GetTimeZoneNames(AZones,true);  //only geo name = true, does not show short names
     TZLocationBox.Items.Clear;
     TZLocationBox.Items.AddStrings(AZones);

  end;
end;

initialization
  {$I plotter.lrs}

end.

