unit GraphWind;
  (* This produces a graph that collects the wind data for one day.  Because
  SQL cannot group rows by a calculated value (in this case, the day, we need
  a separate strategy to collect the day-by-day ranges before plotting *)

  interface
    uses
      AdsData       (* tAdsStringField *),
      AdsFunc       (* for AdsTable *),
      AdsTable      (* tAdsQuery *),
      ArrowCha      (* tArrowSeries *),
      Chart         (* tChart *),
      Classes       (**),
      Controls      (* tControl, crHourglass, tCursor, tDate *),
      DB            (* *),
      DBChart       (* tDBChart *),
      ExtCtrls      (* tRadioGroup; also used by ?DBChart *),
      Forms         (* tForm, Screen *),
      HC3DM         (* Global, UsualFormColor *),
      Menus         (* tMainMenu, tMenuItem *),
      Messages      (* tMessage, WM_ExitSizeMove *),
      PickDateRange (* tDateRange, wtBoth, wtNew, wtOld *),
      Series        (* tLineSeries, tPointSeries; also used by ArrowCh *),
      StdCtrls      (* used by ExtCtrls *),
      TeEngine      (* tChartAxis, tChartSeries; also used by Chart *),
      TeeProcs      (* used by Chart *),
      Windows       (**);

    type
      tfmGraphWind = class(tForm)
          qPM25:          tAdsQuery;
          qPM25NewMax:    tAdsQuery;
          qPM25OldestMax: tAdsQuery;
          qPM25OldMax:    tAdsQuery;
          qWind:          tAdsQuery;
          qWindNewMax:    tAdsQuery;
          qWindNZ:        tAdsQuery;
          qWindOldestMax: tAdsQuery;
          qWindOldMax:    tAdsQuery;
          dbChartWind: tDBChart;
            seriesWindRange: tArrowSeries;
            seriesWindAverage: tLineSeries;
            seriesPM25: tPointSeries;
          menuWind: tMainMenu;
            menuPrint: tMenuItem;
            menuDebug: tMenuItem;
              menuDebugTiming: tMenuItem;
              menuDebugDebug: tMenuItem;
            menuPlot: tMenuItem;
          rgDateRange: tRadioGroup;
    seriesWindDirection: TLineSeries;
          procedure FormCreate          (Sender: tObject);
          procedure FormDblClick        (Sender: tObject);
          procedure FormDestroy         (Sender: tObject);
          procedure FormShow            (Sender: tObject);
          procedure menuPrintClick      (Sender: tObject);
          procedure menuPlotClick       (Sender: tObject);
          procedure menuDebugTimingClick(Sender: tObject);
          procedure menuDebugDebugClick (Sender: tObject);
          procedure rgDateRangeClick    (Sender: tObject);

          procedure dbChartWindAllowScroll(Sender: tChartAxis;
                                           var AMin, AMax: double;
                                           var AllowScroll: boolean);
          procedure dbChartWindGetAxisLabel(Sender: tChartAxis;
                                            Series: tChartSeries;
                                            ValueIndex: integer;
                                            var LabelText: string);
        private
          DebuggingThisGraph: boolean;
          ShowTiming (* how long did plotting take? *): boolean;
          LastPlotted: tDateTime;
          TheDateRange: tDateRange;
          procedure RedrawTheGraph;
          procedure WMExitSizeMove(var TheMessage: tMessage);
                                        message WM_ExitSizeMove;
        public
          procedure PrintTheGraph;
          procedure SetDebugging(const WantDebugging: boolean);
          procedure SetPrinter(const P: string);
          procedure ShowTheGraph(TheDay: tDate);
          procedure UseNewData;
        end (* tfmGraphWind *);

    var
      fmGraphWind: tfmGraphWind;

  implementation
    uses
      DateUtils     (* DateOf, DaysBetween, MillisecondsBetween, SecondsBetween *),
      HC4Main       (* fmHC4Main *),
      HC3Utilities  (* DateTest, LastSecondOfTheDay *),
      LabelAdjuster (* ResizeText *),
      LogManager    (* fmLogManager *),
      ObservedAndWM (* tuSPS30, tuWind *),
      Math          (* max, MaxValue *),
      MyConsts      (* BLANK, nullstring, UsualFormColor *),
      MyUtils       (* ResizeText *),
      SysUtils      (* Format, FormatDateTime, Now, rfIgnoreCase, StringReplace,
                         StrToIntDef *),
      TableManager  (* fmTableManager *);
      {$R *.dfm}

    const
      DefaultTimingCaption = 'timing';

{****** infrastructure ********************************************************}
    procedure tfmGraphWind.FormCreate(Sender: tObject);
      (* create dependent objects *)
    
      begin (* tfmGraphWind.FormCreate *)
        DebuggingThisGraph := false;
        TheDateRange := tDateRange.Create('date range for wind graph');
        dbChartWind.Color := UsualFormColor
      end   (* tfmGraphWind.FormCreate *);

    procedure tfmGraphWind.FormDblClick(Sender: tObject);
      (* home form is probably hidden behind the graph *)

      begin (* tfmGraphWind.FormDblClick *)
        fmHC4Main.Show
      end   (* tfmGraphWind.FormDblClick *);

    procedure tfmGraphWind.FormDestroy(Sender: tObject);
      (* clean up *)

      begin (* tfmGraphWind.FormDestroy *)
        TheDateRange.Free
      end   (* tfmGraphWind.FormDestroy *);

    procedure tfmGraphWind.FormShow(Sender: tObject);
      (* set UI *)

      begin (* tfmGraphWind.FormShow *)
        self.Color := UsualFormColor;
        menuDebugTiming.Caption := DefaultTimingCaption;
        ShowTiming := menuDebugTiming.Checked;
        LastPlotted := 0;
        rgDateRange.OnClick(nil)
      end   (* tfmGraphWind.FormShow *);

    procedure tfmGraphWind.SetDebugging(const WantDebugging: boolean);
      (* xxx *)

      begin (* tfmGraphWind.SetDebugging *)
        DebuggingThisGraph := WantDebugging;
        menuDebugDebug.Checked := DebuggingThisGraph
      end   (* tfmGraphWind.SetDebugging *);

    procedure tfmGraphWind.SetPrinter(const P: string);
      (* show what we're printing on *)

      begin (* tfmGraphWind.SetPrinter *)
        menuPrint.Caption := 'print on ' + P
      end   (* tfmGraphWind.SetPrinter *);

{****** chart-specific ********************************************************}
    procedure tfmGraphWind.dbChartWindAllowScroll(Sender: tChartAxis;
                                                  var AMin, AMax: double;
                                                  var AllowScroll: boolean);
      (* prevent silly scrolling *)

      begin (* tfmGraphWind.dbChartWindAllowScroll *)
        AllowScroll := false
      end   (* tfmGraphWind.dbChartWindAllowScroll *);

    procedure tfmGraphWind.dbChartWindGetAxisLabel(Sender: tChartAxis;
                                                   Series: tChartSeries;
                                                   ValueIndex: integer;
                                                   var LabelText: string);
      (* from https://www.steema.com/support/faq/NewVCL/FAQ_VCL_AXES.htm
      Setting the right-axis title to null seems to be the only way to
      keep it from overprinting the labels.  LabelsSize doesn't work. *)
 

      var
        Direction, OldText: string;
 
      begin (* tfmGraphWind.dbChartWindGetAxisLabel *)
        if Sender = dbChartWind.RightAxis then
          begin (* might need annotation *)
            OldText := LabelText;
            Direction := nullstring;
            case StrToIntDef(OldText, -1) of
                0,
              360: Direction := 'N';
               45: Direction := 'NE';
               90: Direction := 'E';
              135: Direction := 'SE';
              180: Direction := 'S';
              225: Direction := 'SW';
              270: Direction := 'W';
              315: Direction := 'NW'
              end (* case *);
            if Direction <> nullstring then
              begin (* really do it *)
                LabelText := Format('%s (%s°)', [Direction, OldText]);
                Sender.Title.Caption := nullstring
              end   (* really do it *)
          end   (* might need annotation *)
      end   (* tfmGraphWind.dbChartWindGetAxisLabel *);

    procedure tfmGraphWind.WMExitSizeMove(var TheMessage: tMessage);
      (* After resizing, adjust the legends *)

      const
        MinFontSize =  5;
        MaxFontSize = 13;

      var
        CaptionLimit: integer;

      begin (* tfmGraphWind.WMExitSizeMove *)
        with dbChartWind do
          begin (* TheChart *)
            CaptionLimit := 9 * Height div 10;
            ResizeText(LeftAxis.Title.Caption,  LeftAxis.Title.Font,
                         CaptionLimit, MinFontSize, MaxFontSize);
            ResizeText(RightAxis.Title.Caption, RightAxis.Title.Font,
                         CaptionLimit, MinFontSize, LeftAxis.Title.Font.Size)
          end   (* TheChart *)
      end   (* tfmGraphWind.WMExitSizeMove *);

{****** user interface ********************************************************}

    procedure tfmGraphWind.menuDebugDebugClick(Sender: tObject);
      (* xxx *)

      begin (* tfmGraphWind.menuDebugDebugClick *)
        DebuggingThisGraph := menuDebugDebug.Checked
      end   (* tfmGraphWind.menuDebugDebugClick *);

    procedure tfmGraphWind.menuDebugTimingClick(Sender: tObject);
      (* xxx *)

      begin (* tfmGraphWind.menuDebugTimingClick *)
        ShowTiming := menuDebugTiming.Checked;
        if not ShowTiming then
          menuDebugTiming.Caption := DefaultTimingCaption
      end   (* tfmGraphWind.menuDebugTimingClick *);

    procedure tfmGraphWind.menuPlotClick(Sender: tObject);
      (* xxx *)

      begin (* tfmGraphWind.menuPlotClick *)
        RedrawTheGraph
      end   (* tfmGraphWind.menuPlotClick *);

    procedure tfmGraphWind.menuPrintClick(Sender: tObject);
      (* local access to print routine *)

      begin (* tfmGraphWind.menuPrintClick *)
        PrintTheGraph
      end   (* tfmGraphWind.menuPrintClick *);

    procedure tfmGraphWind.rgDateRangeClick(Sender: tObject);
      (* set the range, and redraw *)

      begin (* tfmGraphWind.rgDateRangeClick *)
        with TheDateRange do
          begin (* TheDateRange *)
            case rgDateRange.ItemIndex of
              0: SetToToday;
              1: SetToLastNDays(7);
              2: PickRange
              end (* case *);
          end   (* TheDateRange *);
        RedrawTheGraph
      end   (* tfmGraphWind.rgDateRangeClick *);

{****** unclassified **********************************************************}
    procedure tfmGraphWind.PrintTheGraph;
      (* printer name set in GraphManager *)
    
      begin (* tfmGraphWind.PrintTheGraph *)
        dbChartWind.PrintLandscape
      end   (* tfmGraphWind.PrintTheGraph *);

    procedure tfmGraphWind.RedrawTheGraph;
      (* close the query
         reopen it
         update the form's caption to be wind, mm/dd @ hhmm
         rescaling adaptive axis
         plot
      *)

      const
        SQLSchema = 'select          ' +
                    '  Direction,    ' +
                    '  WhenRecorded, ' +
                    '  WindMPHMax,   ' +
                    '  WindMPHMin,   ' +
                    '  WindMPHRaw    ' +
                    'from            ' +
                    '  Wind_old      ' +
                    'where           ' +
                       DateTest;

      var
        OldCursor: tCursor;
        PlotStarted: tDateTime;
        MaxMPHSeen, MaxPM25Seen, MaxFoundValue: real;
        PartialCaption: string;

      procedure BuildTheQueries;
        (* Set the text of the queries *)

        const
          DateTest = '  convert(WhenRecorded, SQL_DATE) between :BeginDate and :EndDate ';

          OrderBy = 'order by      ' +
                    '  WhenRecorded';

        function GetMaxFrom(MaxQuery: tAdsQuery): real;  forward;
        procedure SupplyDates(TheQuery: tAdsQuery);      forward;

        procedure DoPM25;
          (* debulk *)

          const
            PM25OldSQL = 'select                 ' +
                         '  WhenRecorded,        ' +
                         '  Mass02p5             ' +
                         'from                   ' +
                         '  SPS30_old            ' +
                         'where                  ' +
                         '  ObservedUID = 13 and ' +
                            DateTest;

          begin (* DoPM25 *)
            SupplyDates(qPM25);
            TheDateRange.SchemaToSQL(tuSPS30, PM25OldSQL, qPM25, OrderBy);

            case TheDateRange.WhichTables(tuSPS30) of
                  wtAll: MaxPM25Seen := MaxValue([GetMaxFrom(qPM25OldestMax),
                                                  GetMaxFrom(qPM25OldMax),
                                                  GetMaxFrom(qPM25NewMax)]);
                  wtNew: MaxPM25Seen := GetMaxFrom(qPM25NewMax);
                  wtOld: MaxPM25Seen := GetMaxFrom(qPM25OldMax);
               wtOldest: MaxPM25Seen := GetMaxFrom(qPM25OldestMax);
               wtOldNew: MaxPM25Seen := max(GetMaxFrom(qPM25OldMax),    GetMaxFrom(qPM25NewMax));
            wtOldestOld: MaxPM25Seen := max(GetMaxFrom(qPM25OldestMax), GetMaxFrom(qPM25OldMax))
            end (* case *);
          end   (* DoPM25 *);

        procedure DoWind;
          (* debulk *)

          const
            WindOldSQL   = 'select          ' +
                           '  WhenRecorded, ' +
                           '  Direction,    ' +
                           '  WindMPHMax,   ' +
                           '  WindMPHMin,   ' +
                           '  WindMPHRaw    ' +
                           'from            ' +
                           '  Wind_old      ' +
                           'where           ' +
                               DateTest;
            WindNZOldSQL = 'select                    ' +
                           '  WhenRecorded,           ' +
                           '  WindMPH                 ' +
                           'from                      ' +
                           '  Wind_old                ' +
                           'where                     ' +
                           '  WindMPH is not null and ' +
                              DateTest;

          begin (* DoWind *)
            SupplyDates(qWindNZ);
            SupplyDates(qWind);
            TheDateRange.SchemaToSQL(tuWind, WindOldSQL,   qWind,   OrderBy);
            TheDateRange.SchemaToSQL(tuWind, WindNZOldSQL, qWindNZ, OrderBy);

            case TheDateRange.WhichTables(tuWind) of
                    wtAll: MaxMPHSeen := MaxValue([GetMaxFrom(qWindOldestMax),
                                                    GetMaxFrom(qWindOldMax),
                                                    GetMaxFrom(qWindNewMax)]);
                    wtNew: MaxMPHSeen := GetMaxFrom(qWindNewMax);
                    wtOld: MaxMPHSeen := GetMaxFrom(qWindOldMax);
                 wtOldest: MaxMPHSeen := GetMaxFrom(qWindOldestMax);
                 wtOldNew: MaxMPHSeen := max(GetMaxFrom(qWindOldMax),    GetMaxFrom(qWindNewMax));
              wtOldestOld: MaxMPHSeen := max(GetMaxFrom(qWindOldestMax), GetMaxFrom(qWindOldMax))
              end (* case *);
          end   (* DoWind *);

        function GetMaxFrom(MaxQuery: tAdsQuery): real;
          (* single result *)

          begin (* GetMaxFrom *)
            with TheDateRange, MaxQuery do
              begin (* TheDateRange, MaxQuery *)
                SupplyDates(MaxQuery);
                Open;
                Result := Fields[0].AsFloat;
                Close
              end   (* TheDateRange, MaxQuery *)
          end   (* GetMaxFrom *);

        procedure SupplyDates(TheQuery: tAdsQuery);
          (* plug in parameters *)

          begin (* SupplyDates *)
            with TheDateRange, TheQuery do
              begin (* TheDateRange, MaxQuery *)
                Close;
                ParamByName('BeginDate').AsDate := DomainBegin;
                ParamByName('EndDate'  ).AsDate := DomainEnd
              end   (* TheDateRange, MaxQuery *)
          end   (* SupplyDates *);

        begin (* BuildTheQueries *)
          DoWind;
          DoPM25;
          MaxFoundValue  := max(MaxPM25Seen, MaxMPHSeen);

          qWind.Open;
          qWindNZ.Open;
          qPM25.Open
        end   (* BuildTheQueries *);

      procedure SetXAxisScale;
        (* If the date range is more than a few days, scale should be by date.
         Otherwise, it can be by hour. *)

        const
          FewDays = 3;
          OneDay = 1.0;
          OneHour = OneDay / 24.0;

        var
          Days: integer;

        begin (* SetXAxisScale *)
          with TheDateRange do
            begin (* TheDateRange *)
              Days := DaysBetween(DomainEnd, DomainBegin) + 1;

              with dbChartWind.BottomAxis do
                begin (* organize bottom axis *)
                  Minimum := 0;
                  Maximum := Global.DistantFuture;
                  Minimum := DomainBegin;
                  Maximum := LastSecondOfTheDay(DomainEnd);
                  case Days of
                    0 .. FewDays: begin (* short time *)
                                    Increment := OneHour;
                                    DateTimeFormat := 'hhmm'
                                  end   (* short time *)

                             else begin (* longer *)
                                    Increment := OneDay;
                                    DateTimeFormat := 'M/d'
                                  end   (* longer *)
                    end (* case *)
                end   (* organize bottom axis *)
            end   (* TheDateRange *)
        end   (* SetXAxisScale *);

      procedure SetYAxisScale;
        (* range should be a little bigger than max seen *)

        const
          VerticalMargin = 0.02;

        begin (* SetYAxisScale *)
          with dbChartWind.LeftAxis do
            begin (* dbChartWind.LeftAxis *)
              if MaxFoundValue = 0 then
                MaxFoundValue := 40;
              Maximum := 1.05 * MaxFoundValue;
              Minimum := - VerticalMargin * Maximum;

                   if MaxFoundValue > 40 then Increment := 10
              else if MaxFoundValue > 10 then Increment :=  5
              else if MaxFoundValue >  5 then Increment :=  2
              else if MaxFoundValue >  2 then Increment :=  1
              else                            begin (* near calm *)
                                                Maximum := 1.05;
                                                Increment := 0.2
                                              end   (* near calm *)
            end   (* dbChartWind.LeftAxis *);

          dbChartWind.RightAxis.Maximum := (1 + VerticalMargin) * 360;
          dbChartWind.RightAxis.Minimum := -VerticalMargin * 360;
        end   (* SetYAxisScale *);

      begin (* tfmGraphWind.RedrawTheGraph *)
        OldCursor := Screen.Cursor;
        Screen.Cursor := crHourglass;
        
        try     (* must restore cursor *)
          PlotStarted := Now;

          BuildTheQueries;
          SetXAxisScale;
          SetYAxisScale;
          PartialCaption := 'wind & particulates, ' + TheDateRange.DateRangeString;
          dbChartWind.Title.Text.Clear;
          dbChartWind.Title.Text.Add(PartialCaption);
          self.Caption := Format('%s, plotted @ %s', [PartialCaption,
                                                      FormatDateTime('hhnn', Now)]);
          LastPlotted := Now;
          if ShowTiming then
            menuDebugTiming.Caption := Format('plot took %.2f seconds',
                                              [MillisecondsBetween(Now, PlotStarted)/1000.0])
        finally (* must restore cursor *)
          Screen.Cursor := OldCursor
        end     (* must restore cursor *)
      end   (* tfmGraphWind.RedrawTheGraph *);

    procedure tfmGraphWind.ShowTheGraph(TheDay: tDate);
      (* Show the form and redraw the graph. *)

      var
        OldCursor: tCursor;

      begin (* tfmGraphWind.ShowTheGraph *)
        OldCursor := Screen.Cursor;
        Screen.Cursor := crHourglass;
        try     (* restore cursor *)
          Show
        finally (* restore cursor *)
          Screen.Cursor := OldCursor
        end     (* restore cursor *)
      end   (* tfmGraphWind.ShowTheGraph *);

    procedure tfmGraphWind.UseNewData;
      (* if now is in the date range then replot *)

      begin (* tfmGraphWind.UseNewData *)
        if self.Visible then
          if TheDateRange.NowActive then
            if SecondsBetween(Now, LastPlotted) > Global.DecentGraphInterval then
              RedrawTheGraph
      end   (* tfmGraphWind.UseNewData *);

end.
