(*********************************************************************)
(*                                                                   *)
(* Module PlotLibrary Copyright © 1994 by Computer Inspirations      *)
(*                                                                   *)
(*********************************************************************)

IMPLEMENTATION MODULE PlotLibrary;

(* Amiga-specific operating system modules *)
FROM Intuition   IMPORT ScreenPtr, CloseScreen, NewScreen,
                        ScreenFlagSet, CustomScreen,
                        OpenScreen;
FROM Pens        IMPORT SetAPen, SetBPen, Move, Draw,
                        RectFill, SetDrPt, SetDrMd;
FROM Rasters     IMPORT RastPortPtr, Jam1, Jam2;
FROM Text        IMPORT Text;
FROM Views       IMPORT SetRGB4, ViewModeSet, ViewModes;

(* General Modula-2 modules *)
FROM MathLib0    IMPORT log, power;
FROM Storage     IMPORT ALLOCATE, DEALLOCATE;
FROM SYSTEM      IMPORT ADR, BYTE;

(* M2Sprint-specific module *)
FROM RealSupport IMPORT OpenRealTrans;

(* Custom module developed to support PlotLibrary *)
FROM StringUtils IMPORT LengthStr, ConvRealToStr;


CONST
  BackColour = 0;
  Colour1    = 1;
  Colour2    = 2;
  Colour3    = 3;

  CharHeight = 8;
  CharWidth  = 8;


TYPE
  ClassType = (Left, Right, Top, Bottom);
  ClassSet  = SET OF ClassType;
  PlotSet   = SET OF PlotKindType;

CONST
  XLog = PlotSet{LogX, Log};
  YLog = PlotSet{LogY, Log};


TYPE
  PlotType = POINTER TO PlotRecord;
  PlotRecord =
    RECORD
      Screen                 : ScreenPtr;
      Drawing                : RastPortPtr;
      PlotIs                 : PlotKindType;
      GridColour             : CARDINAL;
      TextColour             : CARDINAL;
      PlotColour             : CARDINAL;
      UserLabels             : BOOLEAN;
      GetLabel               : LabelProc;(* label routine *)
      ScatterPlot            : BOOLEAN;  (* scatter plots *)
      ScatterChar            : ARRAY [0..1] OF CHAR;
      ZeroLineOn             : BOOLEAN;  (* line on/off   *)
      Minx, Miny, Maxx, Maxy : REAL;     (* real coords.  *)
      GMnx, GMny, GMxx, GMxy : INTEGER;  (* screen outline*)
      Scrw, Scrh             : INTEGER;  (* & dimensions. *)
      Divx, Divy,                        (* major divs.   *)
      SubDivx, SubDivy,                  (* minor divs.   *)
      Decx, Decy             : CARDINAL; (* decimal places*)
      xscale, yscale         : REAL;     (* x/y scaling   *)
      xoff, yoff             : INTEGER;  (* plot offsets  *)
      GridDrawn              : BOOLEAN;  (* draw once     *)
      CopyOfAPlot            : BOOLEAN;  (* not original  *)
    END;

(*-------------------------------------------------------*)
(* The following are a number of utility routines used   *)
(* only within this module to facilitate the graphing.   *)

PROCEDURE CreateScreen(width, height, depth : INTEGER;
                       HighResolution       : BOOLEAN;
                       Interlaced           : BOOLEAN;
                       title                : ARRAY OF CHAR)
		       : ScreenPtr;
VAR
  ScreenInfo : NewScreen;
  Views : ViewModeSet;
BEGIN
  Views := ViewModeSet{};
  IF HighResolution THEN INCL(Views, HiRes) END;
  IF Interlaced     THEN INCL(Views, Lace) END;
  WITH ScreenInfo DO
    LeftEdge    := 0;
    TopEdge     := 0;
    Width       := width;
    Height      := height;
    Depth       := depth;
    DetailPen   := BYTE(0);
    BlockPen    := BYTE(1);
    ViewModes   := Views;
    Type        := CustomScreen;
    Font        := NIL;
    DefaultTitle:= NIL;
    Gadgets     := NIL;
    CustomBitMap:= NIL;
    IF title[0] # 0C THEN
      DefaultTitle := ADR(title);
    END;
  END;

  RETURN OpenScreen(ADR(ScreenInfo));
END CreateScreen;


PROCEDURE Round(In : REAL) : INTEGER;
BEGIN
  RETURN TRUNC(In + 0.5);
END Round;


PROCEDURE TenTo(x : REAL) : REAL;
BEGIN
  RETURN power(10.0, x);
END TenTo;


PROCEDURE LogOf(x : REAL) : REAL;
BEGIN
  IF x <= 0.0 THEN (* capture illegal values *)
    RETURN 0.0;    (* and return log(1)      *)
  END;
  RETURN log(x);
END LogOf;


PROCEDURE xyToCoords(Plot       : PlotType;
                     x, y       : REAL;
                     VAR xG, yG : INTEGER);
(* Convert floating point world coordinates to graph space
   coordinates. *)
BEGIN
  WITH Plot^ DO
    xG := Round((x-Minx)*FLOAT(GMxx-GMnx)/(Maxx-Minx))+GMnx;
    yG := GMxy-Round((y-Miny)*FLOAT(GMxy-GMny)/(Maxy-Miny));
  END;
END xyToCoords;


PROCEDURE CoordsToxy(Plot     : PlotType;
                     xG, yG   : INTEGER;
                     VAR x, y : REAL);
(* Convert graph space coordinates to floating point world
   coordinates. *)
BEGIN
  WITH Plot^ DO
    x := FLOAT(xG-GMnx)*(Maxx-Minx)/FLOAT(GMxx-GMnx)+Minx;
    y := FLOAT(GMxy-yG)*(Maxy-Miny)/FLOAT(GMxy-GMny)+Miny;
  END;
END CoordsToxy;


PROCEDURE OffsetPoint(Plot : PlotType; VAR x, y : INTEGER);
(* Offset the graph points by the plot amounts *)
BEGIN
  INC(x, Plot^.xoff);
  INC(y, Plot^.yoff);
END OffsetPoint;


PROCEDURE DrawLine(Plot               : PlotType;
                   Colour             : CARDINAL;
                   Gx1, Gy1, Gx2, Gy2 : INTEGER);
(* Line clipping algorithm : two points which make up a
   line are passed (Gx1, Gy1) and (Gx2, Gy2) to be
   clipped within the Plot space.  NOTE: The Amiga screen
   coordinates have y = 0 at the top of the screen.

   From an algorithm published in "Microcomputer Graphics
   for the IBM PC" by Roy E. Myers. *)
VAR
  x, y    : ARRAY [1..2] OF INTEGER;
  xl, xh  : INTEGER;
  yl, yh  : INTEGER;
  Offset  : INTEGER;
  Bound   : ARRAY [1..2] OF ClassSet;
  Discard : BOOLEAN;
  i       : CARDINAL;

  PROCEDURE Classify (VAR Boundary : ClassSet;
                      x, y         : INTEGER);
  BEGIN
    Boundary := ClassSet{}; (* assume inside bounds  *)
    WITH Plot^ DO
      IF x < xl THEN
        INCL(Boundary, Left);   (* outside left bounds   *)
      ELSIF x > xh THEN
        INCL(Boundary, Right);  (* outside right bounds  *)
      END;
      IF y > yh THEN
        INCL(Boundary, Bottom); (* outside bottom bounds *)
      ELSIF y < yl THEN
        INCL(Boundary, Top);    (* outside top bounds    *)
      END;
    END;
  END Classify;

  PROCEDURE Clip (Boundary : ClassSet);
  VAR
    dy, dx : INTEGER;
  BEGIN
    WITH Plot^ DO
      dy := y[2] - y[1];
      dx := x[2] - x[1];
      IF Left IN Boundary THEN
        x[i] := xl;
        y[i] := y[1] + dy * (xl - x[1]) DIV dx;
      ELSIF Right IN Boundary THEN
        x[i] := xh;
        y[i] := y[1] + dy * (xh - x[1]) DIV dx;
      ELSIF Top IN Boundary THEN
        x[i] := x[1] + dx * (yl - y[1]) DIV dy;
        y[i] := yl;
      ELSIF Bottom IN Boundary THEN
        x[i] := x[1] + dx * (yh - y[1]) DIV dy;
        y[i] := yh;
      END;
    END;
  END Clip;

  PROCEDURE PlotLine(x1, y1, x2, y2 : INTEGER);
  (* Draw a clipped line to the screen. *)
  CONST
    HalfHeight = CharHeight DIV 2;
    HalfWidth  = CharWidth  DIV 2;
  BEGIN
    WITH Plot^ DO
      SetAPen(Drawing, Colour);    (* set line colour *)
      IF ScatterPlot THEN
        (* move to the character centre and place char *)
        Move(Drawing, x1-HalfWidth, y1+HalfHeight);
        Text(Drawing, ADR(ScatterChar), 1);
      ELSE
        (* move to 1st pt and draw to 2nd point *)
        Move(Drawing, x1, y1);
        Draw(Drawing, x2, y2);
      END;
    END;
  END PlotLine;

BEGIN
  (* Initialize clipping routines *)
  x[1] := Gx1;  y[1] := Gy1;
  x[2] := Gx2;  y[2] := Gy2;
  WITH Plot^ DO
    IF GridDrawn THEN
      Offset := 1;         (* allow the outer grid    *)
    ELSE                   (* lines to be drawn first *)
      Offset := 0;
    END;
    xl := GMnx + Offset;   (* prevent overwriting the *)
    xh := GMxx - Offset;   (* grid boundary           *)
    yl := GMny + Offset;
    yh := GMxy - Offset;
  END;

  (* Classify the endpoints *)
  Classify(Bound[1], x[1], y[1]);
  Classify(Bound[2], x[2], y[2]);

  (* Perform the line clipping *)
  IF Bound[1] + Bound[2] = ClassSet{} THEN
    (* Within plot area -- just draw the line *)
    PlotLine(x[1], y[1], x[2], y[2]);
  ELSE
    (* Just output a clipped line segment *)
    i := 1;
    REPEAT
      Discard := Bound[1] * Bound[2] # ClassSet{};
      IF NOT Discard THEN
        IF Bound[i] # ClassSet{} THEN
          (* Handle the line clipping *)
          Clip(Bound[i]);
          Classify(Bound[i], x[i], y[i]);
        ELSE
          INC(i);
        END;
      END;
    UNTIL (i > 2) OR Discard;
    IF NOT Discard THEN
      PlotLine(x[1], y[1], x[2], y[2]);  (* Clipped line *)
    END;
  END;
END DrawLine;


PROCEDURE IntPlotLine(Plot           : PlotType;
                      Colour         : CARDINAL;
                      x1, y1, x2, y2 : REAL);
(* Draw a line between points (x1,y1) and (x2,y2).

   This routine accepts log(x) values and displays them
   properly during log plots. *)
VAR
  Gx1, Gy1, Gx2, Gy2 : INTEGER;
BEGIN
  xyToCoords(Plot, x1, y1, Gx1, Gy1);  (* to pixel coords *)
  xyToCoords(Plot, x2, y2, Gx2, Gy2);
  OffsetPoint(Plot, Gx1, Gy1);         (* offset plot *)
  OffsetPoint(Plot, Gx2, Gy2);
  DrawLine(Plot, Colour, Gx1, Gy1, Gx2, Gy2);
END IntPlotLine;


PROCEDURE PlotLine(Plot           : PlotType;
                   Colour         : CARDINAL;
                   x1, y1, x2, y2 : REAL);
(* Draw a line between points (x1,y1) and (x2,y2).

   This routine converts points to logarithmic points
   during log plots to make external appearances
   identical to the caller of this routine. *)
BEGIN
  CASE Plot^.PlotIs OF
    LogX :
      IntPlotLine(Plot, Colour, LogOf(x1), y1,
                                LogOf(x2), y2);
    |
    LogY :
      IntPlotLine(Plot, Colour, x1, LogOf(y1),
                                x2, LogOf(y2));
    |
    Log :
      IntPlotLine(Plot, Colour, LogOf(x1), LogOf(y1),
                                LogOf(x2), LogOf(y2));
    ELSE
      IntPlotLine(Plot, Colour, x1, y1, x2, y2);
  END;
END PlotLine;


PROCEDURE PlotBar(Plot   : PlotType;
                  Colour : CARDINAL;
                  width  : REAL;
                  x, y   : REAL);
(* Draw a bar centered around x of height y and width
   across. *)
VAR
  Half : REAL;
  Gxl, Gyl, Gxu, Gyu : INTEGER;
BEGIN
  WITH Plot^ DO
    Half := ABS(width) / 2.0;
    IF y >= 0.0 THEN
      xyToCoords(Plot, x-Half, y, Gxl, Gyl);
      xyToCoords(Plot, x+Half, 0.0, Gxu, Gyu);
    ELSE
      xyToCoords(Plot, x-Half, 0.0, Gxl, Gyl);
      xyToCoords(Plot, x+Half, y, Gxu, Gyu);
    END;

    IF Gyl # Gyu THEN
      OffsetPoint(Plot, Gxl, Gyl);    (* offset bar *)
      OffsetPoint(Plot, Gxu, Gyu);
      SetAPen(Drawing, Colour);
      RectFill(Drawing, Gxl, Gyl, Gxu, Gyu);
    END;
  END;
END PlotBar;


PROCEDURE PlotXLabel(Plot : PlotType;
                     int  : INTEGER;
                     x, y : REAL);
(* Place a label on the x-axis *)
VAR
  Gx, Gy : INTEGER;
  xlog   : REAL;
  str    : ARRAY [0..40] OF CHAR;
BEGIN
  WITH Plot^ DO
    (* compensate for log scales along x-axis *)
    IF PlotIs IN XLog THEN
      xlog := TenTo(x);
    ELSE
      xlog := x;
    END;

    (* produce the x-axis labels *)
    IF UserLabels THEN
      GetLabel(int, str);   (* call-back to get label   *)
    ELSIF NOT ConvRealToStr(str, xlog, 2, Decx, ' ') THEN
      str := "***";         (* number conversion failed *)
    END;

    (* label the axis *)
    xyToCoords(Plot, x, y, Gx, Gy);
    DEC(Gx, LengthStr(str) * CharWidth DIV 2);
    INC(Gy, 5 * CharHeight DIV 4);
    LabelX(Plot, str, Gx, Gy);
  END;
END PlotXLabel;


PROCEDURE PlotYLabel(Plot : PlotType;
                     x, y : REAL);
(* Place a label on the y-axis *)
VAR
  Gx, Gy : INTEGER;
  ylog   : REAL;
  str    : ARRAY [0..40] OF CHAR;
BEGIN
  WITH Plot^ DO
    (* compensate for log scales along y-axis *)
    IF PlotIs IN YLog THEN
      ylog := TenTo(y);
    ELSE
      ylog := y;
    END;

    (* produce the y-axis labels *)
    IF NOT ConvRealToStr(str, ylog, 2, Decy, ' ') THEN
      str := "***";
    END;
    xyToCoords(Plot, x, y, Gx, Gy);
    DEC(Gx, (LengthStr(str) + 1) * CharWidth);
    INC(Gy, CharHeight DIV 4);
    LabelX(Plot, str, Gx, Gy);
  END;
END PlotYLabel;


PROCEDURE DrawGrid(Plot : PlotType);
(* Create the graph grid. *)
CONST
  DashLine = BITSET{1,3,5,7,9,11,13,15};
  NormLine = BITSET{0..15};
VAR
  vCnt, hCnt : CARDINAL;
  oldx, oldy : INTEGER;
  x, y, xint, yint : REAL;
  ScatterState : BOOLEAN;

  PROCEDURE PlotMinorX;
  VAR
    Cnt : CARDINAL;
    xm, xi : REAL;
  BEGIN
    WITH Plot^ DO
      IF SubDivx > 1 THEN
        (* Change to a dashed line *)
        SetDrPt(Drawing, DashLine);

        (* Output the minor grid lines *)
        IF PlotIs IN XLog THEN
          xi := TenTo(xint) / FLOAT(SubDivx+1);
          FOR Cnt := 2 TO SubDivx DO
            xm := x + LogOf(FLOAT(Cnt) * xi);
            IntPlotLine(Plot, GridColour, xm, Miny,
                                          xm, Maxy);
          END;
        ELSE
          xi := xint / FLOAT(SubDivx);
          FOR Cnt := 1 TO SubDivx-1 DO
            xm := x + FLOAT(Cnt) * xi;
            IntPlotLine(Plot, GridColour, xm, Miny,
                                          xm, Maxy);
          END;
        END;

        (* Back to a normal line *)
        SetDrPt(Drawing, NormLine);
      END;
    END;
  END PlotMinorX;

  PROCEDURE PlotMinorY;
  VAR
    Cnt : CARDINAL;
    ym, yi : REAL;
  BEGIN
    WITH Plot^ DO
      IF SubDivy > 1 THEN
        (* Change to a dashed line *)
        SetDrPt(Drawing, DashLine);

        (* Output the minor grid lines *)
        IF PlotIs IN YLog THEN
          yi := TenTo(yint) / FLOAT(SubDivy+1);
          FOR Cnt := 2 TO SubDivy DO
            ym := y + LogOf(FLOAT(Cnt) * yi);
            IntPlotLine(Plot, GridColour, Minx, ym,
                                          Maxx, ym);
          END;
        ELSE
          yi := yint / FLOAT(SubDivy);
          FOR Cnt := 1 TO SubDivy-1 DO
            ym := y + FLOAT(Cnt) * yi;
            IntPlotLine(Plot, GridColour, Minx, ym,
                                          Maxx, ym);
          END;
        END;

        (* Back to a normal line *)
        SetDrPt(Drawing, NormLine);
      END;
    END;
  END PlotMinorY;

BEGIN
  WITH Plot^ DO
    IF GridDrawn THEN RETURN END;

    (* Draw the vertical grid lines or label bars *)
    ScatterState := Plot^.ScatterPlot;
    Plot^.ScatterPlot := FALSE;  (* draw lines!     *)
    ClearPlot(Plot);             (* clear plot area *)
    oldx := xoff;
    oldy := yoff;
    SetPlotOffset(Plot, 0, 0);   (* no grid offset  *)
    SetDrMd(Plot^.Drawing, Jam1);
    IF Divx > 0 THEN
      xint := (Maxx - Minx) / FLOAT(Divx);
      FOR vCnt := 0 TO Divx DO
        x := Minx + FLOAT(vCnt) * xint;
        PlotMinorX;
        IntPlotLine(Plot, GridColour, x, Miny, x, Maxy);
        PlotXLabel(Plot, vCnt, x, Miny);
      END;
    END;

    (* Draw the horizontal grid lines *)
    IF Divy > 0 THEN
      yint := (Maxy - Miny) / FLOAT(Divy);
      FOR hCnt := 0 TO Divy DO
        y := Miny + FLOAT(hCnt) * yint;
        PlotMinorY;
        IntPlotLine(Plot, GridColour, Minx, y, Maxx, y);
        PlotYLabel(Plot, Minx, y);
      END;
    END;
    SetPlotOffset(Plot, oldx, oldy);   (* user offset *)
    SetDrMd(Plot^.Drawing, Jam2);
    GridDrawn := TRUE;
    Plot^.ScatterPlot := ScatterState;
  END;
END DrawGrid;


(*-------------------------------------------------------*)
(* The main graphing routines.                           *)

PROCEDURE Empty() : PlotType;
(* Used to initialize or pass empty plot parameters *)
BEGIN
  RETURN NIL;
END Empty;


PROCEDURE SetPlotLimits(Plot                   : PlotType;
                        xMin, xMax, yMin, yMax : REAL);
(* Alter the current plot limits -- useful for overlaying
   plots with different minima/maxima *)
BEGIN
  WITH Plot^ DO
    Miny := yMin;  Maxy := yMax;
    Minx := xMin;  Maxx := xMax;
    IF PlotIs IN XLog THEN
      Minx := LogOf(xMin);
      IF xMax <= 0.0 THEN (* make a log multiple of xMin *)
        Maxx := Minx + FLOAT(Divx);
      ELSE
        Maxx := LogOf(xMax);
      END;
    END;
    IF PlotIs IN YLog THEN
      Miny := LogOf(yMin);
      IF yMax <= 0.0 THEN (* make a log multiple of yMin *)
        Maxy := Miny + FLOAT(Divy);
      ELSE
        Maxy := LogOf(yMax);
      END;
    END;
  END;
END SetPlotLimits;


PROCEDURE SetPlotOffset(Plot       : PlotType;
                        xoff, yoff : INTEGER);
(* Alter the current plot base position -- useful for
   realigning bar charts or giving three dimensional
   looking graphs *)
BEGIN
  Plot^.xoff := xoff;
  Plot^.yoff := yoff;
END SetPlotOffset;


PROCEDURE SetPlotScale(Plot           : PlotType;
                       xscale, yscale : REAL);
(* Alter the plot scaling along x- and y-axes *)
BEGIN
  Plot^.xscale := ABS(xscale);
  Plot^.yscale := ABS(yscale);

  (* limit to 1/10 reduction *)
  IF Plot^.xscale < 0.1 THEN
    Plot^.xscale := 0.1;
  END;
  IF Plot^.yscale < 0.1 THEN
    Plot^.yscale := 0.1;
  END;
END SetPlotScale;


PROCEDURE SetColourMap(Plot             : PlotType;
                       ColourIndex      : CARDINAL;
                       Red, Green, Blue : CARDINAL);
(* Change the set of predefined colours for the plots *)
BEGIN
  WITH Plot^ DO
    SetRGB4(ADR(Screen^.VPort),ColourIndex,Red,Green,Blue);
  END;
END SetColourMap;


PROCEDURE SetScatterPlot(Plot  : PlotType);
(* Set to plot single points or a scatter plot *)
BEGIN
  Plot^.ScatterPlot := TRUE;
  Plot^.ScatterChar := "O";   (* default character *)
END SetScatterPlot;


PROCEDURE SetScatterChar(Plot  : PlotType;
                         Ch    : CHAR);
(* Change the default `.' character for scatter plots *)
BEGIN
  Plot^.ScatterChar[0] := Ch;
END SetScatterChar;


PROCEDURE InitOffsetPlot
                  (VAR Plot               : PlotType;
                   PlotKind               : PlotKindType;
                   xMin, xMax, yMin, yMax : REAL;
                   width, height          : INTEGER;
                   xDiv, yDiv             : CARDINAL;
                   xSubDiv, ySubDiv       : CARDINAL;
                   xDec, yDec             : CARDINAL;
                   xOffset, yOffset       : INTEGER;
                   OldPlot                : PlotType)
                   : BOOLEAN;
CONST
  TopBorder = 20;
VAR
  Ok      : BOOLEAN;
  Border1 : INTEGER;
  Border2 : INTEGER;
  DumStr  : ARRAY [0..40] OF CHAR;
BEGIN
  ALLOCATE(Plot, SIZE(Plot^));
  IF Plot = NIL THEN
    RETURN FALSE;
  END;


  WITH Plot^ DO
    (* If OldPlot is defined use screen *)
    IF OldPlot # NIL THEN
      Screen := OldPlot^.Screen;

      (* Extract the RastPort drawing surface information *)
      Drawing := ADR(Screen^.RPort);
    END;

    (* Initialize the plot variables for later use *)
    IF ConvRealToStr(DumStr, yMax, 2, yDec, ' ') THEN
      Border1 := (LengthStr(DumStr) + 3) * CharWidth;
    END;
    IF ConvRealToStr(DumStr, yMin, 2, yDec, ' ') THEN
      Border2 := (LengthStr(DumStr) + 3) * CharWidth;
    END;
    IF Border1 > Border2 THEN
      GMnx := Border1;
    ELSE
      GMnx := Border2;
    END;
    GMny := TopBorder + yOffset;
    GMxx := width - GMnx DIV 2 + xOffset;
    GMxy := height - CharHeight * 3 + yOffset;
    INC(GMnx, xOffset);
    Divx := xDiv;  SubDivx := xSubDiv; Decx := xDec;
    Divy := yDiv;  SubDivy := ySubDiv; Decy := yDec;
    Scrw := width; Scrh := height;
    SetPlotOffset(Plot, 0, 0);  (* no initial offset   *)

    (* No scaling initially *)
    xscale := 1.0;
    yscale := 1.0;

    (* Miscellaneous plot values *)
    PlotColour  := Colour1;     (* default plot colour *)
    TextColour  := Colour1;     (* default text colour *)
    GridColour  := Colour2;     (* default grid colour *)
    PlotIs      := PlotKind;
    GridDrawn   := FALSE;
    UserLabels  := FALSE;       (* use internal labels *)
    ScatterPlot := FALSE;       (* not a scatter plot  *)
    ZeroLineOn  := TRUE;        (* zero line enabled   *)
    CopyOfAPlot := TRUE;        (* this is a copy      *)

    (* Initialize the plot minimum and maximums *)
    SetPlotLimits(Plot, xMin, xMax, yMin, yMax);
  END;
  RETURN TRUE;
END InitOffsetPlot;


PROCEDURE InitPlot(VAR Plot               : PlotType;
                   MainTitle              : ARRAY OF CHAR;
                   PlotKind               : PlotKindType;
                   xMin, xMax, yMin, yMax : REAL;
                   width, height          : INTEGER;
                   xDiv, yDiv             : CARDINAL;
                   xSubDiv, ySubDiv       : CARDINAL;
                   xDec, yDec             : CARDINAL;
                   NumberOfColours        : CARDINAL)
                   : BOOLEAN;
(* Set up the initial plot characteristics and create
   the initial custom graph screen. *)
VAR
  Depth  : INTEGER;
  Screen : ScreenPtr;
BEGIN
  (* Create the graphics screen *)
  CASE NumberOfColours OF
    0..2  : Depth := 1 |
    3..4  : Depth := 2 |
    5..8  : Depth := 3 |
    9..16 : Depth := 4 |
    ELSE    Depth := 5
  END;
  Screen := CreateScreen(width, height, Depth,
                         width > 320, height > 200,
                         MainTitle);
  IF Screen = NIL THEN
    RETURN FALSE;
  END;

  (* Initialize most of the plot variables *)
  IF NOT InitOffsetPlot(Plot, PlotKind,
                        xMin, xMax, yMin, yMax,
                        width, height, xDiv, yDiv,
                        xSubDiv, ySubDiv,
                        xDec, yDec, 0, 0, NIL) THEN
    RETURN FALSE;
  END;

  (* Extract the RastPort drawing surface information *)
  Plot^.Screen := Screen;
  Plot^.Drawing := ADR(Screen^.RPort);
  Plot^.CopyOfAPlot := FALSE; (* this is the original *)
  RETURN TRUE;
END InitPlot;


PROCEDURE DonePlot(VAR Plot : PlotType);
(* Close off the plot -- when done *)
BEGIN
  IF Plot # NIL THEN
    WITH Plot^ DO
      IF (Screen # NIL) & NOT CopyOfAPlot THEN
        CloseScreen(Plot^.Screen);
      END;
    END;
    DEALLOCATE(Plot, SIZE(Plot^));
    Plot := NIL;
  END;
END DonePlot;


PROCEDURE PlotZero(Plot : PlotType);
(* Plot the zero line *)
BEGIN
  (* emphasize y = 0 line *)
  WITH Plot^ DO
    IF ZeroLineOn THEN
      IF ScatterPlot THEN
        SetDrMd(Drawing, Jam2);
        ScatterPlot := FALSE;
        IF NOT (PlotIs IN YLog) THEN
          IntPlotLine(Plot, Colour3, Minx, 0.0, Maxx, 0.0);
        END;
        ScatterPlot := TRUE;
      ELSE
        IF NOT (PlotIs IN YLog) THEN
          IntPlotLine(Plot, Colour3, Minx, 0.0, Maxx, 0.0);
        END;
      END;
    END;
  END;
END PlotZero;


PROCEDURE PlotFx(Plot : PlotType; Fx : PlotFunction);
(* Plot the passed function *)
VAR
  lastx, lasty, x, y, int, dx, dy : REAL;
  OldMinx, OldMaxx, OldMiny, OldMaxy : REAL;
BEGIN
  WITH Plot^ DO
    (* save the old plot window dimensions *)
    OldMinx := Minx;  OldMaxx := Maxx;
    OldMiny := Miny;  OldMaxy := Maxy;

    (* calculate scaled plot window dimensions *)
    dx := (Maxx - Minx) / xscale;
    dy := (Maxy - Miny) / yscale;
    Minx := Minx + dx * (xscale - 1.0) * 0.5;
    Maxx := Minx + dx;
    Miny := Miny + dy * (yscale - 1.0) * 0.5;
    Maxy := Miny + dy;

    (* draw the plot grid *)
    DrawGrid(Plot);

    (* initialize the plot variables *)
    lastx := Minx;
    lasty := Fx(Minx);
    x := lastx;
    y := lasty;
    IF PlotIs IN PlotSet{Bar, Line} THEN
      int := (Maxx - Minx) / FLOAT(Divx);
    ELSE
      int := (Maxx - Minx) / FLOAT(GMxx - GMnx);
    END;

    IF ScatterPlot THEN
      SetDrMd(Plot^.Drawing, Jam1);
    END;

    (* go through the range of x values and plot y values *)
    REPEAT
      IF PlotIs = Bar THEN
        PlotBar(Plot, PlotColour, int * 0.25, x, y);
      ELSE
        IntPlotLine(Plot, PlotColour, lastx, lasty, x, y);
      END;
      lastx := x;
      lasty := y;
      x := x + int;
      CASE PlotIs OF
        LogX :
          y := Fx(TenTo(x));
          |
        LogY :
          y := LogOf(Fx(x));
          |
        Log :
          y := LogOf(Fx(TenTo(x)));
          |
        ELSE
          y := Fx(x);
      END;
    UNTIL x > Maxx;
    PlotZero(Plot);

    (* restore the old plot window dimensions *)
    Minx := OldMinx;  Maxx := OldMaxx;
    Miny := OldMiny;  Maxy := OldMaxy;
  END;
END PlotFx;


PROCEDURE SetPlotColour(Plot   : PlotType;
                        Colour : CARDINAL);
(* Change the plot colour *)
BEGIN
  Plot^.PlotColour := Colour;
END SetPlotColour;


PROCEDURE SetTextColour(Plot   : PlotType;
                        Colour : CARDINAL);
(* Change the plot colour *)
BEGIN
  Plot^.TextColour := Colour;
END SetTextColour;


PROCEDURE SetGridColour(Plot   : PlotType;
                        Colour : CARDINAL);
(* Change the plot colour *)
BEGIN
  Plot^.GridColour := Colour;
END SetGridColour;


PROCEDURE SetZeroLine(Plot : PlotType;
                      On   : BOOLEAN);
(* Enable/disable zero line *)
BEGIN
  Plot^.ZeroLineOn := On;
END SetZeroLine;


PROCEDURE SetLabelRoutine(Plot         : PlotType;
                          LabelRoutine : LabelProc);
(* Overrides the numerical labels with user-defined
   labels for each grid position along the x-axis *)
BEGIN
  Plot^.GetLabel   := LabelRoutine;
  Plot^.UserLabels := TRUE;          (* use LabelRoutine *)
END SetLabelRoutine;


PROCEDURE ClearPlot(Plot : PlotType);
(* Clear the plot area *)
BEGIN
  WITH Plot^ DO
    SetAPen(Drawing, BackColour); (* background colour *)
    RectFill(Drawing, GMnx, GMny, GMxx, GMxy);
    GridDrawn := FALSE;
  END;
END ClearPlot;


PROCEDURE LabelX(Plot  : PlotType;
                 Label : ARRAY OF CHAR;
                 x, y  : INTEGER);
(* Place text horizontally on the plot *)
BEGIN
  (* Change bounds of screen to allow text outside plot *)
  WITH Plot^ DO
    IF (x >= 2)  & (x <= Screen^.Width-1) &
       (y >= 12) & (y <= Screen^.Height-1) THEN
      SetAPen(Drawing, TextColour); (* set text colour *)
      Move(Drawing, x, y);          (* set text position *)
      Text(Drawing, ADR(Label), LengthStr(Label));
    END;
  END;
END LabelX;


PROCEDURE LabelY(Plot  : PlotType;
                 Label : ARRAY OF CHAR;
                 x, y  : INTEGER);
(* Place text vertically on the plot *)
VAR
  ChCnt : CARDINAL;
  Str   : ARRAY [0..1] OF CHAR;
BEGIN
  Str[1] := 0C;
  FOR ChCnt := 1 TO LengthStr(Label) DO
    Str[0] := Label[ChCnt-1];
    LabelX(Plot, Str, x, y);
    INC(y, CharHeight);
  END;
END LabelY;


PROCEDURE CenterLabelX(Plot  : PlotType;
                       Label : ARRAY OF CHAR;
                       y     : INTEGER);
(* Center the text label horizontally *)
VAR
  x : INTEGER;
BEGIN
  WITH Plot^ DO
    x := (GMxx - GMnx) DIV 2 + GMnx;   (* center position *)
    DEC(x, LengthStr(Label) * CharWidth DIV 2); (* offset *)
    LabelX(Plot, Label, x, y);
  END;
END CenterLabelX;


PROCEDURE CenterLabelY(Plot  : PlotType;
                       Label : ARRAY OF CHAR;
                       x     : INTEGER);
(* Center the text label vertically *)
VAR
  y : INTEGER;
BEGIN
  WITH Plot^ DO
    y := (GMxy - GMny) DIV 2 + GMny;   (* center position *)
    DEC(y, LengthStr(Label) * CharHeight DIV 2);
    LabelY(Plot, Label, x, y);
  END;
END CenterLabelY;


PROCEDURE LabelMinMax(Plot : PlotType;
                      Fx   : PlotFunction);
(* Label local minima and maxima by tracking whether the
   first derivative of Fx changes sign *)
TYPE
  SignType = (Positive, Negative);
VAR
  sign, signp : SignType;
  x, xlog, xplot, y, yp, yplot, delta : REAL;
  Gx, Gy : INTEGER;
  str : ARRAY [0..40] OF CHAR;
BEGIN
  WITH Plot^ DO
    x := Minx;
    signp := Positive;
    IF PlotIs IN PlotSet{Bar, Line} THEN
      delta := (Maxx - Minx) / FLOAT(Divx);
    ELSE
      delta := (Maxx - Minx) / FLOAT(GMxx - GMnx);
    END;
    CASE PlotIs OF
      LogX :
        yp := Fx(TenTo(x - delta));
        |
      LogY :
        yp := LogOf(Fx(x - delta));
        |
      Log :
        yp := LogOf(TenTo(Fx(x - delta)));
        |
      ELSE
        yp := Fx(x - delta);
    END;
    REPEAT
      IF PlotIs IN XLog THEN
        xlog := TenTo(x);
      ELSE
        xlog := x;
      END;
      IF PlotIs IN YLog THEN
        y := LogOf(Fx(xlog));
      ELSE
        y := Fx(xlog);
      END;
      IF y - yp >= 0.0 THEN
        sign := Positive;
      ELSE
        sign := Negative;
      END;
      IF (sign # signp) OR
         ((PlotIs = Bar) & (yp # 0.0)) THEN
        (* found a local minima/maxima *)
        IF PlotIs IN PlotSet{Bar, Line} THEN
          xplot := x - delta;
          yplot := yp;
        ELSE
          xplot := x;
          yplot := y;
        END;
        xyToCoords(Plot, xplot, yplot, Gx, Gy);
        IF yplot <= 0.0 THEN
          INC(Gy, CharHeight);  (* value below curve *)
        ELSE
          DEC(Gy, CharHeight DIV 3);  (* above curve *)
        END;
        IF ConvRealToStr(str, yplot, 2, Decy+2, ' ') THEN
          DEC(Gx, LengthStr(str) * CharWidth DIV 2);
          LabelX(Plot, str, Gx, Gy);
        END;
        signp := sign;
      END;
      x := x + delta;
      yp := y;
    UNTIL x > Maxx;
  END;
END LabelMinMax;


PROCEDURE InformationBox(Plot          : PlotType;
                         Position      : PositionType;
                         Lines         : ARRAY OF LineType;
                         FillColour    : CARDINAL;
                         OutlineColour : CARDINAL);
(* Place an information box in the plot area *)
VAR
  Gx, Gy    : INTEGER;
  Gxs, Gys  : INTEGER;
  Gw, Gh    : INTEGER;
  ChCnt     : CARDINAL;
  LineCnt   : CARDINAL;
  OldColour : CARDINAL;
BEGIN
  Gxs := 2 * CharWidth;       (* side border width  *)
  Gys := 2 * CharHeight;      (* upper/lower border *)

  (* Determine the text box width and height *)
  Gh := 2 * Gys + INTEGER(HIGH(Lines) + 1) * CharHeight;
  Gw := 2 * Gxs;
  ChCnt := 0;
  FOR LineCnt := 0 TO HIGH(Lines) DO
    IF LengthStr(Lines[LineCnt]) > ChCnt THEN
      ChCnt := LengthStr(Lines[LineCnt]);
    END;
  END;
  INC(Gw, ChCnt * CharWidth);

  (* Determine the text box position *)
  WITH Plot^ DO
    CASE Position OF
      UpperLeft :
        Gx := GMnx;
        Gy := GMny;
      |
      UpperMiddle :
        Gx := (GMxx - GMnx - Gw) DIV 2 + GMnx;
        Gy := GMny;
      |
      UpperRight :
        Gx := GMxx - Gw;
        Gy := GMny;
      |
      LowerLeft :
        Gx := GMnx;
        Gy := GMxy - Gh;
      |
      LowerMiddle :
        Gx := (GMxx - GMnx - Gw) DIV 2 + GMnx;
        Gy := GMxy - Gh;
      |
      LowerRight :
        Gx := GMxx - Gw;
        Gy := GMxy - Gh;
      |
    END;

    (* Draw the box *)
    SetAPen(Drawing, OutlineColour);
    RectFill(Drawing, Gx, Gy, Gx+Gw, Gy+Gh);
    SetAPen(Drawing, FillColour);
    RectFill(Drawing, Gx+2, Gy+2, Gx+Gw-2, Gy+Gh-2);

    (* Output the text strings *)
    INC(Gx, Gxs);
    INC(Gy, Gys);
    SetBPen(Drawing, FillColour); (* set text background *)
    FOR LineCnt := 0 TO HIGH(Lines) DO
      INC(Gy, CharHeight);
      LabelX(Plot, Lines[LineCnt], Gx, Gy);
    END;
    SetBPen(Drawing, BackColour); (* normal background   *)
  END;
END InformationBox;


BEGIN
  IF OpenRealTrans() THEN
    (* dummy statement *)
  END;
END PlotLibrary.