unit ldn128u1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls,
  StdCtrls,Math{for DegToRad};

const
  vers='24 Aug 26, 11:10';
  //ldn128Orrery started about 10 Aug 26
  //LDN199, the basis if this, started 05 Feb 15


//== v v v ========== Some notes ==========================

(*Major "flaw" in this... I have no plans to allow for the fact
that the planets' orbits are not circles. Nor for the fact that
the speed they travel along their orbits is not constant.

Allowing for the speed variation might not be too bad. But I'm
content to know that sometimes my display will be a little out.
But at least the "ahead of where showm" and "behind where shown"
should cancel out, so the error shouldn't grow... due to *this*
consideration.

Tiny rounding errors may be very significant, unless you are
careful about how you calculate the planet's new bearing from
the sun. Think hard about this bit!
*)

(*Personal note... history of this... LDN128, Orrery, grew out
  of LDN199. It was started in mid August 26 when I was fed up with...
  STUFF. Not least the move to FTTP.
  I decided to take a quiet happy hour writing "a bit of"
  the Orrery project I've wanted to do for years. (The Aug 26 solar
  eclipse probably had something to so with this, too.)*)


(*Helpful!... https://www.theplanetstoday.com/index.html
  When asked "please give me a table showing the time in earth years
  that it takes the planets and pluto to orbit the sun", Leo said...

"Celestial Body  Orbital Period (Earth Years)
  Mercury   	  0.24
  Venus	          0.62
  Earth	          1.00
  Mars         	  1.88
  Jupiter	 11.86
  Saturn	 29.46
  Uranus	 84.01
  Neptune	164.79
  Pluto	        247.94

  Pluto, classified as a dwarf planet, takes approximately 248
  Earth years to orbit the Sun. Since its discovery in 1930, it
  has not yet completed a single full orbit,
  which is expected to occur in 2178.
  --- (End Leo)
  Neptune was discovered in 1846, so while it's completed one
  orbit, it still has a way to go before it completes it's second.*)

(*===START section "OLD LDN199 NOTES...
  LDN199 is a "complex" use of arrays to generate a graphics display which
  runs quickly, despite a lot going on.

  Basis for a variety of simulations, say of single celled organisms
  living on a 2-D world

  See http:sheepdogguides.com/Lut/lt3amoeba.htm for tutorial about this.

  See http:sheepdogguides.com/Lut/PixelProblem.htm for some of the basics
  of pixel plotting with Lazarus.

  Personal note regarding LT199...
  Plan of this hatched at Charlie H's, 5 Feb 15, when I was coming down
  from first NoviceGuard (PCB261/ 262... 262 was daughter boards for 261, and
  was first PCB- via- OSH Park to go out the door) push, day after I posted
  first job (PCB262) to OSH Park.

  - - -END "OLD LDN199 notes"===
  *)

  kMaxImageIndexX=400;//Sets width of Image1
  kMaxImageIndexY=400;//Sets height
  kMaxPixelsIndex=18;//Pixels occupied, each critter, incl head and tail.
      //"4" means there are 5 array elements: One for a 1 pixel
      //amoeba, 3 for current tail pixels, and one for "last pixel
      //of tail"... which must be painted color of background (i.e.
      //not actually part of tail) to "erase" oldest pixel each
      //time amoeba moves forward.
  kBkGrndColor=clYellow;

  kMaxMaxPlanets=10;//Set to 2 to provide for 3 planets
     //Set it too small, and you can only do that many planets
     //Set it too large, and you waste memory with unused array
     //  elements when ui8MaxPlanets is < kMaxMaxPlanets
     //  (The two are needed thus...
     //  kMaxMaxPlanets: for declaring arrays
     //  ui8MaxPlanets so the number of planets shown can be
     //    set in ini file.
type

  { Tldn128Orreryf1 }

  Tldn128Orreryf1 = class(TForm)
    buGo: TButton;
    buQuit: TButton;
    buRestart: TButton;
    cbPause: TCheckBox;
    Image1: TImage;
    Label1: TLabel;
    Label2: TLabel;
    laOrbits0: TLabel;
    laOrbits1: TLabel;
    laOrbits2: TLabel;
    laOrbits3: TLabel;
    laPlanetName0: TLabel;
    laPlanetName1: TLabel;
    laPlanetName2: TLabel;
    laPlanetName3: TLabel;
    laTxtCycles: TLabel;
    LaTxtImgSize: TLabel;
    laTutRef: TLabel;
    laCycles: TLabel;
    Timer1: TTimer;
    procedure buQuitClick(Sender: TObject);
    procedure buRestartClick(Sender: TObject);
    procedure cbPauseChange(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure Crosshairs(xL,yL,radL:longint);

  private
    { private declarations }
    ui8Tmp,ui8MaxPlanets,ui8PlanetRadius:uint8;
    Bitmap:TBitmap;
    bColPointerHead:byte;//Tells us which column (of array Where) currently
         //has coords of head. Moves to right (higher array index) as time
         //passes. What is IN each array element doesn't change until
         //overwritten because it is oldest pixel at time. But, with each
         //cycle, the color used for a given pixel changes.
         //Points to the pixel at the head of the "trail", which will be
         //the amoeba itself.
    clPlanetColor:array [0..kMaxMaxPlanets] of TColor;
    ui32TimerInterval:uint32;//Data type chosen to match TTimer spec.
    liSunX,liSunY:longint;
    wCycles:word;//Why type "word"... not sure. Legacy. I may have had a
      //reason when I first chose it!
    liYearsOrbX100,liRadOrb:array [0..kMaxMaxPlanets] of longint;
    degPlanetAngle,degStart,degChangePerIteration:array[0..kMaxMaxPlanets] of uint16;
    sPlanetName:array [0..kMaxMaxPlanets] of string;
    ui16OrbitsDone:array [0..kMaxMaxPlanets] of uint16;



    procedure FillConstants;//Things like the "color for tail posn" tables.
    procedure OrreryPosnCalc (var liX,liY:longint;degAngle:uint16;{not returned}
                                 dblRadius:double);

    //It may be a bad idea to have two "circle" procedures...
    //  Maybe someday make a "DrawCircle", with a switch to choose filled/ not.
    procedure DrawAFilledCircle(xFC,yFC,liRadius:longint;clFillFC:TColor);
    procedure DrawAnEmptyCircle(xFC,yFC,liRadius:longint;clFillFC:TColor);
    procedure OrreryProc;//Was what was done on timeout until replaced with
      //DoOnTimeOut on 23 Aug 26
    procedure DoOnTimeOut;

  public
    { public declarations }

  end;

var
  ldn128Orreryf1: Tldn128Orreryf1;

implementation

{$R *.lfm}
{$R+}    //Range checking on. Take out? (for speed?) in due course.
         //(Taking out didn't seem to make much difference)

{ Tldn128Orreryf1 }

procedure Tldn128Orreryf1.FormCreate(Sender: TObject);
var siTmp:single;
    dblTmp:double;
begin
  laTutRef.hide;
  Timer1.enabled:=false;
  application.title:='LDN128';
  ldn128Orreryf1.caption:='LDN128Orrery, version '+vers;
  randomize;

  wCycles:=0;

  //===START section "To go in ini file"- - -
  //An "ini file" is a small text file that (usually) resides in
  //  the same folder as the program's .exe
  //It is easily tweaked with a text editor... or by the program
  //  it was created for.
  //
  //Ini files are an "old-fashioned" idea... but that doesn't make them
  //  bad. All is explained in my tutorial about ini files...
  //   https://sheepdogguides.com/lut/lt2Nc.htm
  //
  //-----------------------------------------------------------------
  //The next two put the running app at 20,20 on your screen so that
  //  the window for working on the form isn't hidden under it.
  //When the ini file is implemented, you'll probably want to use it
  //  to make the program remember where it was when it was closed,
  //  what state it was in, and it's width and height, and use those
  //  values when restarting the program. Not hard to do, just tedious.
  //  How you do that is

  ldn128orreryf1.top:=20;
  ldn128orreryf1.left:=20;

  ui32TimerInterval:=120;//Time for a timout in ms
  ui8MaxPlanets:=3;//Make this 2 if you want 3 plants shown.

  liYearsOrbX100[0]:=40;
  liYearsOrbX100[1]:=100;
  liYearsOrbX100[2]:=180;
  liYearsOrbX100[3]:=360;

  //In due course, to be calc'd from liRadOuterOrbit
  liRadOrb[0]:=50;
  liRadOrb[1]:=80;
  liRadOrb[2]:=110;
  liRadOrb[3]:=140;

  clPlanetColor[0]:=clRed;
  clPlanetColor[1]:=clBlack;
  clPlanetColor[2]:=clBlue;
  clPlanetColor[3]:=clFuchsia;

  sPlanetName[0]:='Mercury';
  sPlanetName[1]:='Venus';
  sPlanetName[2]:='Earth';
  sPlanetName[3]:='Mars';

  degStart[0]:=270;
  degStart[1]:=270;
  degStart[2]:=270;
  degStart[3]:=270;

  (*
  //Alternative set of orbital speeds.
  //Pattern repeats after Mercury orbits 60 times
  degChangePerIteration[0]:=12;
  degChangePerIteration[1]:=6;
  degChangePerIteration[2]:=4;
  degChangePerIteration[3]:=1;
  *)

  degChangePerIteration[0]:=13;
  degChangePerIteration[1]:=7;
  degChangePerIteration[2]:=5;
  degChangePerIteration[3]:=1;

  //- - -END "To go in ini file"===

  ui16OrbitsDone[0]:=0;
  ui16OrbitsDone[1]:=0;
  ui16OrbitsDone[2]:=0;
  ui16OrbitsDone[3]:=0;

  laPlanetName0.font.color:=clPlanetColor[0];laPlanetName0.caption:=sPlanetName[0];
  laPlanetName1.font.color:=clPlanetColor[1];laPlanetName1.caption:=sPlanetName[1];
  laPlanetName2.font.color:=clPlanetColor[2];laPlanetName2.caption:=sPlanetName[2];
  laPlanetName3.font.color:=clPlanetColor[3];laPlanetName3.caption:=sPlanetName[3];

  laOrbits0.font.color:=clPlanetColor[0];laOrbits0.caption:='';
  laOrbits1.font.color:=clPlanetColor[1];laOrbits1.caption:='';
  laOrbits2.font.color:=clPlanetColor[2];laOrbits2.caption:='';
  laOrbits3.font.color:=clPlanetColor[3];laOrbits3.caption:='';

  //Initial angles will need to be calculated eventually, if you want
  //  your simulation to mirror the real world. You'll also have to
  //  provide the date/time your simulation is to start.
  degPlanetAngle[0]:=degStart[0];
  degPlanetAngle[1]:=degStart[1];
  degPlanetAngle[2]:=degStart[2];
  degPlanetAngle[3]:=degStart[3];

  ui8PlanetRadius:=5;

  if ui8MaxPlanets>kMaxMaxPlanets then
     showmessage('Change ini file. The max value allowed for "max number '+
       'of planets" is '+inttostr(kMaxMaxPlanets));

  Timer1.interval:=ui32TimerInterval;

  //ESTABLISH DRAWING AREA... down to "end of..."
  Image1.width:=kMaxImageIndexX;
  Image1.height:=kMaxImageIndexY;

  Bitmap:=TBitmap.create;

  Bitmap.width:=kMaxImageIndexX;
  Bitmap.height:=kMaxImageIndexY;

  //Next two determine background color. Delphi bitmaps default to
  //  white, Lazarus to black.
  Bitmap.canvas.pen.color:=kBkGrndColor;
  Bitmap.canvas.brush.color:=kBkGrndColor;

  Bitmap.canvas.FillRect
    (0,0,kMaxImageIndexX,kMaxImageIndexY);
  //Note rectangle dimensions: "5,5" fills TO, (excluding
  //  the cell identified by "5,5". If the image was
  //  set up with WIDTH 5, the IDENTIFIER of the
  //  lower right hand cell is "4,4", because the
  //  upper left hand cell is "0,0"

  Image1.Picture.Graphic:=Bitmap; //Assign the bitmap
    //to the image component The size of the image
    //seems to adjust to the size of the bitmap assigned
    //to it, at least if you use the default properties
    //for Image1, which include Align= alNone,
    //AutoSize= false, Stretch= false.
    //I'm not quite sure why Autosize doesn't need
    //to be "true"... it may relate to something else.

    //end of ESTABLISH DRAWING AREA

  FillConstants;//May or may not be useful to Orrery

//  (*
  //Experiments with the platform-independent floating-point data types.
  //               1         2
  //      12345678901234567890123456789
   siTmp:=1234.5678901234567890123456789;
   label1.caption:=FormatFloat('0.00000000000', siTmp);
  dblTmp:=1234.5678901234567890123456789;
  label2.caption:=FormatFloat('0.000000000000', dblTmp);
//  *)
 // Label1.hide;//Turn on again if you want to re-run the experiments!
 // Label2.hide;




  Timer1.enabled:=true;
end; //FormCreate

procedure Tldn128Orreryf1.Timer1Timer(Sender: TObject);
begin
  DoOnTimeout;
  //WAS OrreryProc; in early days of devel.
  (*scraps  Showamoebas;
  Moveamoebas;
  *)
  inc(wCycles);
  if wCycles>60000 then wCycles:=0;//Could add "cycles of cycles" counter, to
    //track overflows.... but want to keep this fast!
  laCycles.caption:=inttostr(wCycles);
end;

procedure Tldn128Orreryf1.buQuitClick(Sender: TObject);
begin
  close;
end;

procedure Tldn128Orreryf1.buRestartClick(Sender: TObject);
begin
  FormCreate(self);//Crude "solution", probably... but it WORKS!
end;

procedure Tldn128Orreryf1.cbPauseChange(Sender: TObject);
begin
  if cbPause.checked=true then Timer1.Enabled:=false //no ; here
    else Timer1.Enabled:=true;
end;

procedure Tldn128Orreryf1.FillConstants;
var bTmp,bAddR,bAddB,bAddG:byte;
begin
  liSunX:=kMaxImageIndexX div 2;
  liSunY:=kMaxImageIndexY div 2;
end;//FillConstants;

procedure Tldn128Orreryf1.Crosshairs(xL,yL,radL:longint);
//Do crosshairs centered on xL,yL. Arms radL long, i.e. the whole crosshairs
//  will be 2x radL wide.
begin
yL:=kMaxImageIndexY-yL;//To make y-axis zero-at-bottom
  with Image1.Picture.Bitmap.canvas do begin
    moveto(xL-radL,yL);
    lineto(xL+radL,yL);

    moveto(xL,yL-radL);
    lineto(xL,yL+radL);
  end;//with
end; //Crosshairs

procedure Tldn128Orreryf1.OrreryProc;
var liDiscX,liDiscY,RSize,degAngle,liOffX,liOffY:longint;

begin
  Timer1.enabled:=false;
  liOffX:=90;liOffY:=90;

  //Clear screen...
  Image1.Canvas.Brush.Color := kBkGrndColor;
  Image1.Canvas.FillRect(Rect(0, 0, Image1.Width, Image1.Height));

  (*Leo says..
  // Draw filled circle: Ellipse(Left, Top, Right, Bottom)
  // Center (200, 200), Radius 50 -> Bounding box 150,150 to 250,250
  Canvas.Ellipse(150, 150, 250, 250);
  Image1.Canvas.Brush.Style := bsSolid;
  Image1.Canvas.Brush.Color := clBlue;
  *)

  //for ui8P:=0 to 3 do begin
  Image1.Canvas.Brush.Color := clblue;
  RSize:=100;//Smallest orbit
  //ui8Tmp:=RSize+ui8P*15;//Calc radius for this orbit.
  degAngle:=0;
  while degAngle<91 do
    begin
    //Compiler will warn that liDiscX,liDiscY are not initialized.
    //  That's true. But they don't need to be for our wants.
    OrreryPosnCalc(liDiscX,liDiscY,degAngle,ui8Tmp);

    DrawAFilledCircle(liDiscX+liSunX,liDiscY+liSunY,3,clBlue);//+200's: Bodge

    degAngle:=degAngle+20;
    end;//while
  //end;//for ui8p...

  //Below here gives a green circle on crosshairs...
  Crosshairs(liSunX,liSunY,40);
  RSize:=35;
  DrawAFilledCircle(liSunX,liSunY,RSize,clGreen);

end; //OrreryProc


procedure Tldn128Orreryf1.DrawAFilledCircle(xFC,yFC,liRadius:longint;clFillFC:TColor);
//Draw a filled circle. The center will be at xFC,yFC.
//   Circle's radius will be liRadius
//The CrossHairs procedure may be useful during debugging.
var clPenTmp,clBrushTmp:TColor;

begin
  with Image1.canvas do begin
  clPenTmp:=Pen.Color;//save to restore
  clBrushTmp:=Brush.Color;

  Pen.Color:= clFillFC;
  Brush.Color := clFillFC;

  yFC:=kMaxImageIndexY-yFC; //To invert things, so that if y=0, the
  //circle is at BOTTOM of drawing area.

  Ellipse(xFC-liRadius,yFC-liRadius,
             xFC+liRadius,yFC+liRadius);

  Pen.Color:=clPenTmp;//restore
  Brush.Color:=clBrushTmp;


end;//with
end;//DrawAFilledCircle


procedure Tldn128Orreryf1.DrawAnEmptyCircle(xFC,yFC,liRadius:longint;clFillFC:TColor);
//Draw an empty circle. The center will be at xFC,yFC.
//   Circle's radius will be liRadius
//The CrossHairs procedure may be useful during debugging.
var clPenTmp,clBrushTmp:TColor;
    tbsBrushStyleTmp:TBrushStyle;

begin
  with Image1.canvas do begin
  clPenTmp:=Pen.Color;//save to restore
  clBrushTmp:=Brush.Color;
  tbsBrushStyleTmp:=Brush.Style;

  Pen.Color:= clFillFC;
  Brush.Color := clFillFC;
  Brush.Style:= bsclear;//to leave circle center undisturbed

  yFC:=kMaxImageIndexY-yFC; //To invert things, so that if y=0, the
  //circle is at BOTTOM of drawing area.

  Ellipse(xFC-liRadius,yFC-liRadius,
             xFC+liRadius,yFC+liRadius);

  Pen.Color:=clPenTmp;//restore
  Brush.Color:=clBrushTmp;
  Brush.Style:=tbsBrushStyleTmp;

end;//with
end;//DrawAnEmptyCircle

procedure Tldn128Orreryf1.OrreryPosnCalc (var liX,liY:longint;
             degAngle:uint16;{not returned} dblRadius:double);

(*SR mostly written by Leo on 11 Aug 26. (Leo is the free AI behind the AI
   summary at the top of search.brave.com results)

 (Y=0 to be at BOTTOM of graph as far as programmer knows. The Y coords are
    inverted inside DrawAFilledCircle)

 I asked Leo to...
 "Calculate the x/y coord of a point on a circle given that the circle is
  centered on liX,liY, with a radius of RSize. Calculate the x/y coord
  for a point degAngle degrees around the circle
  from 'East' or 'Three o'clock'".

*)

(*From Leo, 11 Aug 26...
Coordinate System: This places 0° at the right (East).

Leo went on to say...
   If your application requires 0° at the top (North) and clockwise rotation,
   adjust the angle before calculation: AngleDegrees := 90 - AngleDegrees;.
Center Offset: The above code calculates coordinates relative to the
   origin (0,0). To offset from a center point (Cx, Cy), use: X :=
         Cx + Radius * Cos(DegToRad(AngleDegrees)) and Y :=
             Cy + Radius * Sin(DegToRad(AngleDegrees)).
(End Leo contribution)

Instead of many (90 minus Angle) calculations, I decided to work with this
  orientation choice. Turned out to be very easy! If I wanted to start a
  planet at the top of the circle, instead of setting the bearing to zero
  I set it to 270. Simples!

Oh! And I had to add something to make the size of a bearing increase you
  went clockwise arount the circle. Without that, the bearings increase
  in the other direction.

The COS function returns a non-integer value. I reviewed options and
decided type double best for this. Double is a signed data type.

DegToRad: type double also chosen for this after careful thought.
*)

var dblTmp:double;

begin
  // Convert degrees to radians and calculate coordinates
  // Angle subtracted from 360 to make results go around circle
  //   clockwise as the angle increases. Is there a way to avoid
  //   having to have this here? It may increase processing load.
  dblTmp:=dblRadius * Cos(DegToRad(360-degAngle));
  liX:=round(dblTmp);
  dblTmp:=dblRadius *
  Sin(DegToRad(360-degAngle));
  liY:=round(dblTmp);
end;//OrreryPosnCalc

procedure Tldn128Orreryf1.DoOnTimeOut;
//Replaced OrreryProc 23 Aug 26
var liDiscX,liDiscY,RSize,{degAngle,}liOffX,liOffY:longint;
    ui8WhichP:uint8;
    dblTmp:double;
begin
  (*Used while getting to grips with the parameters you must provide
  to the Lazarus "Ellipse" procedure...
  Leo says..
     //To draw filled circle: Ellipse(Left, Top, Right, Bottom)
     //  Center (200, 200), Radius 50 -> Bounding box 150,150 to 250,250
     Image1.Canvas.Brush.Style := bsSolid;
     Image1.Canvas.Brush.Color := clBlue;
     Image1.Canvas.Ellipse(150, 150, 250, 250);
  *)

  for ui8WhichP:=0 to ui8MaxPlanets do begin

    //First, erase the previous representation of the planet...
    OrreryPosnCalc(liDiscX,liDiscY,degPlanetAngle[ui8WhichP],liRadOrb[ui8WhichP]);
    DrawAFilledCircle(liDiscX+liSunX,liDiscY+liSunY,ui8PlanetRadius,kBkGrndColor);

    //Repair Orbit circle... Crude, labor intensive solution
    DrawAnEmptyCircle(liSunX,liSunY,liRadOrb[ui8WhichP],clPlanetColor[ui8WhichP]);

    //Advance the planet in it's journey...
    degPlanetAngle[ui8WhichP]:=degPlanetAngle[ui8WhichP]+
       degChangePerIteration[ui8WhichP];

    if degPlanetAngle[ui8WhichP]>360 then begin
      degPlanetAngle[ui8WhichP]:=degPlanetAngle[ui8WhichP]-360;
      //Would doing a mod each time be more efficient?
      ui16OrbitsDone[ui8WhichP]:=ui16OrbitsDone[ui8WhichP]+1;
      end;
      Case ui8WhichP of
        0:laOrbits0.caption:=inttostr(ui16OrbitsDone[0]);
        1:laOrbits1.caption:=inttostr(ui16OrbitsDone[1]);
        2:laOrbits2.caption:=inttostr(ui16OrbitsDone[2]);
        3:laOrbits3.caption:=inttostr(ui16OrbitsDone[3]);
       else
        showmessage('Error 26823a. Code needs inmprovement');
       end;//case

    //Calc new posn...
    OrreryPosnCalc(liDiscX,liDiscY,degPlanetAngle[ui8WhichP],liRadOrb[ui8WhichP]);

    //Redraw...
    DrawAFilledCircle(liDiscX+liSunX,liDiscY+liSunY,ui8PlanetRadius,clPlanetColor[ui8WhichP]);

    end;//for...

  (*
  //Below would give a green circle on crosshairs...

  Crosshairs(liSunX,liSunY,40);
  RSize:=35;
  DrawAFilledCircle(liSunX,liSunY,RSize,clGreen);
  *)
end; //DoOnTimeout

end.

