{ Copyright (c) by  T. W. Lougheed  24 April 1981 }

{ The following program plots dots on the screen, putting an X
and Y axis on the screen and scaling automaticly, when not well
instructed, to fit all the points on the screen with the given
constraint of the number of rows and columns allowed. }


{ First version  15 September 1981

  By    T. W. Lougheed
        Dept. T. & A. Mechanics
        Thurston Hall, Cornell U.
        Ithaca, NY 14853

   Last version  24 April 1981 }

{ This program is in the public domain and may not be sold by any
person or corperation without express permission of the author. }


MODULE PLOT15;

CONST   ROWS =  23;  { Dimensions of the screen. }
        COLS =  80;

TYPE    POINT  = RECORD  X, Y :REAL END;


{ This procedure plots an  X,Y  graph of the given  DATA  on the whole
screen, plotting its own axis grid.  All management of the screen takes
place here. }

PROCEDURE  PLOT (
  L         : INTEGER;  { Number of points to plot. }
  VAR DATA  : ARRAY[ M..N :INTEGER ] OF POINT;   { Stuff to plot; conformant
              array; "VAR" simply to avoid passing excessive paramiters. }
  ORIGIN    : POINT;  { Location of origin -- if out-of-bounds it is
               set to lie at the upper corner of the screen. }
  X_MIN,
  X_MAX,     { Bounds for X & Y values.  If MAX <= MIN then .. }
  Y_MIN,     { .. scaling is done automaticly for that axis.   }
  Y_MAX     : REAL
 );


VAR  C            : CHAR;      { Dummy input. }
     I, J, K      : INTEGER;   { Dummy indicies. }
     ORIGINAL     : BYTE;      { Remember the VIO mode on entry. }
     QUIT         : BOOLEAN;   { "Bad input"  flag. }
     SCREEN       : ABSOLUTE[ $F000 ] ARRAY[ 1..24, 1..80 ] OF BYTE;
                               { Direct access to screen image. }
     SX,                       { Scaling ration for X values. }
     SY           : REAL;      { Scaling constant for Y values. }
     VIO_CONTROLL : ABSOLUTE[ $F7FF ] BYTE;
                               { VIO board controll byte. }


{ This procedure checks for the paramiters. }

PROCEDURE CHECKOUT;
   VAR  K : INTEGER;
   BEGIN

   QUIT := FALSE;  { Assume all is Okay until told otherwise. }

   { Do not allow more than the maximum number of points. }
   IF M >= N THEN BEGIN
      WRITELN;
      WRITELN( 'Problem in procedure PLOT:  indexes are backwards:  ',
        N:4, ' - ', M:4, ' is not positive.' );
      WRITELN( 'Be advised to check the paramiters in the procedure call.' );
      QUIT := TRUE;
      END;

   { Be sure not to over-run the end of DATA. }
   IF L - 1 > N - M THEN BEGIN
      WRITELN;
      WRITELN( 'Problem in procedure PLOT:  the number of points ', L:5,
       ' is larger than the allowed number ', N:5, '-', M:5, '+1.' );
      WRITELN( 'Be advised to check the paramiters in the procedure call.' );
      QUIT := TRUE;
      END;

   IF QUIT THEN  BEGIN  WRITELN( 'Quitting PLOT.' );  EXIT  END;


   { The preceding errors were "fatal" the following are recoverable. }


   { If a propper X_MAX, X_MIN  pair is not provided, get one. }

   IF X_MIN >= X_MAX THEN BEGIN
      DATA[M].X := X_MAX; X_MIN := X_MAX;
      FOR K := M + 1 TO M + L - 1 DO WITH DATA[ K ] DO
         IF X > X_MAX THEN  X_MAX := X
            ELSE IF X < X_MIN THEN  X_MIN := X;
      END;

   IF X_MIN = X_MAX THEN BEGIN   { If correction fails. }
      WRITELN;
      WRITELN( 'Note well:  upper and lower bounds for X are identical  (',
       X_MIN, ' ).' );
      X_MIN := X_MIN - 1;  X_MAX := X_MAX + 1;
      END;


   { Make sure origin is correctly defined on the X axis. }

   IF NOT( (X_MIN <= ORIGIN.X) AND (ORIGIN.X <= X_MAX) )
      THEN IF (X_MIN <= 0) AND (0 <= X_MAX) THEN  ORIGIN.X := 0
         ELSE  ORIGIN.X := X_MIN;  { Defaults to upper corner. }


   { If a propper Y_MIN, Y_MAX  pair is not provided, get one. }

   IF Y_MIN >= Y_MAX THEN BEGIN
      DATA[M].Y := Y_MAX; Y_MIN := Y_MAX;
      FOR K := M + 1 TO M + L - 1 DO WITH DATA[ K ] DO
         IF Y > Y_MAX THEN  Y_MAX := Y
            ELSE IF Y < Y_MIN THEN  Y_MIN := Y;
      END;

   IF Y_MIN = Y_MAX THEN BEGIN  { If correction fails. }
      WRITELN;
      WRITELN( 'Note well:  upper and lower limits for Y are identical  (',
        Y_MIN, ').' );
      Y_MIN := Y_MIN - 1;  Y_MAX := Y_MAX + 1;
      END;


   { Likewise make sure the origin's  Y-co-ordinate is sensible. }

   IF NOT( (Y_MIN <= ORIGIN.Y) AND (ORIGIN.Y <= Y_MAX) )
      THEN IF (Y_MIN <= 0) AND (0 <= Y_MAX) THEN  ORIGIN.Y := 0
         ELSE ORIGIN.Y := Y_MIN;  { Defaults to upper corner. }


   END;  { of CHECKOUT }



{ This procedure erases the screen. }

PROCEDURE CLEAR;
  { Writing a controll-Z causes the VIO firmware to clear the screen,
  as long as the VIO board is in the TEXT or EXTENDED text mode. }
  BEGIN WRITE( CHR($1A) )  END;


{ This procedure sets the screen to full graphics mode (graphics and text,
but no reverse-video characters. }

PROCEDURE GRAPHICS_MODE;
   CONST  ESC    = $1B;   { The  ASCII  <ESCAPE> character code. }
   BEGIN
   CLEAR; { Start with a clean slate. }
   { Prepare for restoration made at exit of the enclosing procedure. }
   ORIGINAL := VIO_CONTROLL;  { Memorize the original VIO mode setting. }
   { The  VIO  firmware will put the screen into the mode upon recieving
   the  " <ESC> 'G' "  sequence.  The primary benefit of not going directly
   to the VIO-port is to get the fuzzy cursor turned on. }
   WRITE( CHR(ESC), 'G' );
   END;



{ The following procedure returns the screen to the mode it was in
when the program entered.   Note that this procedure requires the
procedure  GRAPHICS_MODE  to have been called previously to work
propperly, though it will, probably, default to good options anyway. }

PROCEDURE RESTORE_MODE;
   CONST  ESC    = $1B;   { The  ASCII  <ESCAPE> character code. }
   BEGIN
   { On entry, one is in POSITIVE video mode, MODE III set on,
   and a 24 lines by 80 column screen.  To what extent this was
   not the case in the original it is reset. }

   { Restore number of columns if needed. }
   IF (ORIGINAL & $01) <> 0 THEN WRITE( CHR(ESC), 'C' );
   { Restore number of rows if needed. }
   IF (ORIGINAL & $02) <> 0 THEN WRITE( CHR(ESC), 'L' );
   { Restore mode if not set propperly. }
   CASE  ORIGINAL & $0C  OF
      $00,       { Null mode, which is undesirable, defaults to TEXT. }
      $08 : WRITE( CHR(ESC), 'T' );      { TEXT mode -- chrs $20..$7F }
      $04 : WRITE( CHR(ESC), 'E' );  { EXTENDED mode -- chrs $A0..$FF }
      $0C :;      { For completeness -- GRAPHICS mode with full text. }
      END; { of CASE }
   { Turn on reverse video if that's the way it was. }
   IF (ORIGINAL & $10) <> 0 THEN WRITE( CHR(ESC), 'V' );
   CLEAR;  { Be neat and leave a blank screen. }

   END;



{ The following uses an "Escape Sequence" to position the cursor.
An escape sequence is part of the  VIO  firmware, and an explanation
can be found in the  "IMSAI VDP-80 Referance Manual"  section III,
page 43  ("the addressable cursor").  }

PROCEDURE CURSOR( ROW, COLUMN :INTEGER);
   CONST  OFFSET = $1F;   { The  VIO  software offsets all addresses. }
          ESC    = $1B;   { The  ASCII  <ESCAPE> character code. }
   VAR  A, B  : CHAR;
   BEGIN
   A := CHR( OFFSET + ROW );
   B := CHR( OFFSET + COLUMN );
    { The '=' character signals a relocation of the cursor. }
   WRITE( CHR(ESC), '=', A, B );
   END;



{ This procedure draws a pair of axes with origin at row I column J. }

PROCEDURE  AXES( I, J :INTEGER );
   VAR  K, L  :INTEGER;    { Dummy index. }
   BEGIN

   { Idiot proofing upon idiot proofing. }
   IF I > ROWS THEN I := ROWS  ELSE IF I < 1 THEN I := 1;
   IF J > COLS THEN J := COLS  ELSE IF J < 1 THEN J := 1;

   { Prepare the screen. }
   CURSOR( 24, 1 );  { Get the cursor out of the way. }
   { For some reason, a stray dot occasionally remains on the screen on
   the top row.    We expurgate it by directly putting blanks on the
   whole screen. }
   FOR K := 1 TO COLS DO FOR L := 1 TO ROWS DO SCREEN[ L, K ] := ' ';

   { Make ordinate (Y axis). }
   { Note that it runs horizontally, contrary to custom. }
   FOR K := 1 TO COLS-1 DO IF (K - J) MOD 6 = 0 { One tick every 12 dots. }
      THEN  SCREEN[ I, K ] := CHR( $CB )        { Ticked bar 'T'. }
      ELSE  SCREEN[ I, K ] := CHR( $CA );       { Plain dash '-' . }
   SCREEN[ I, COLS ] := 'y';                    { Label axis. }

   { Make abscissa (X-axis). }
   { Note that it runs vertically, also uncustomary. }
   FOR K := 1 TO ROWS-1 DO IF (K - I) MOD 3 = 0 { One tick every 9 dots. }
      THEN  SCREEN[ K, J ] := CHR( $CD )        { Ticked line 'L'. }
      ELSE  SCREEN[ K, J ] := CHR( $C5 );       { Plain, vertical line '|'. }
   SCREEN[ ROWS, J ] := 'x';                    { Label axis. }

   { Put in origin. }
   SCREEN[ I, J ] := CHR( $CF );                { A '+' sign. }

   END;  { AXES }



{ This procedure places a dot in the appropriate position on the
screen, using the character-block plotting set.   The co-ordinates
of the dot are expected to be  1 <= I <= 72,  1 <= J <= 160. }

PROCEDURE  PLACE_DOT ( I, J :INTEGER );

   VAR  BIT,                   { Block in cell to be plotted. }
        K, L,                  { Character cell to be plotted in. }
        CELL    : INTEGER;     { Order of character on the screen. }

   BEGIN

   { If the dot is off screen, then plot it on the margin. }
   IF  I < 1 THEN  I := 1  ELSE IF I > 3*ROWS THEN I := 3*ROWS;
   IF  J < 1 THEN  J := 1  ELSE IF J > 2*COLS THEN J := 2*COLS;

   { Co-ordinates of the dot's cell on the screen. }
   K := (I - 1) DIV 3 +  1;  L := (J - 1) DIV 2 + 1;

   { Cell now becomes the contents of the memory. }
   CELL := ORD( SCREEN[ K, L ] );

   { If not a block character, set to a blank block. }
   IF (CELL < $80) OR ($BF < CELL) THEN  CELL := $80;

   { Add the bit of the desired dot to the existing block pattern. }
   BIT := 5 - (I - 1) MOD 3 - 3*((J - 1) MOD 2);
   SETBIT( CELL, BIT );

   { Install the revised pattern on the screen. }
   SCREEN[ K, L ] := CHR( CELL );

   END; { of POINT }


BEGIN

{ Make sure all the paramiters make sense. }
CHECKOUT;   IF QUIT THEN EXIT;

{ Turn on VIO board's full-set mode (includes both graphics & letters). }
GRAPHICS_MODE;


{ Make the axes. }

I := 1 + ROUND( (ROWS - 1)*(ORIGIN.X - X_MIN)/(X_MAX - X_MIN) );
J := 1 + ROUND( (COLS - 1)*(ORIGIN.Y - Y_MIN)/(Y_MAX - Y_MIN) );
AXES( I, J );   { Plot co-ordinate lines. }


{ Plotting. }

{ Scale factors. }
SX := (3*ROWS - 1)/(X_MAX - X_MIN);
SY := (2*COLS - 1)/(Y_MAX - Y_MIN);

{ Note that if a point is out-of-bounds, it will be
placed on the appropriate margin without comment. }
FOR K := M TO L+M-1 DO WITH DATA[ K ] DO
   PLACE_DOT( ROUND( SX*(X - X_MIN) ) + 1,  ROUND( SY*(Y - Y_MIN) ) + 1 );

 
{ Postlude. }

CURSOR( 24, 1 );
WRITE( 'dx =', 9/SX, '  dy =', 12/SY,
 '      Hit any key when done looking...' );
{ When input is recieved, return VIO to mode when entered. }
READ( C );
RESTORE_MODE;

END; { of PLOT }

MODEND.

