Sunday, June 27, 2021

Automatic Train Controller System Code

TrainController class starts two infinite loops in separate threads:
  1. User Override Control Loop
  2. Automatic Control Loop

In this post, the Train class contains a few characteristics such as distance travelled from Origin and its speed, which can be abstracted into a separate class and can be handled in a more centralized manner inside TrainController class.

Also a few more features such as directionality of train movement and maximum distance from origin will be introduced.

Code
package com.example.lib;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class TrainController {
    private final List mTrains = new ArrayList<>();
    private final List mManuallyStoppedTrains = new ArrayList<>();
    private final InstructionParser mInstructionParser = new InstructionParser();
    public static void main(String[] args) {
        final TrainController trainController = new TrainController();
        trainController.createAndStartTrains(5);
         // This is the main controller loop.
         // 1. It monitors the distance between the pairs of trains and sends stop and start signals appropriately.
         // 2. This is run in a separate thread so that the user input can be captured in the main thread.
        Runnable controllerRunnable = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    trainController.checkAndControlTrains();
                }
            }
        };
        Thread controllerThread = new Thread(controllerRunnable);
        controllerThread.start();
        trainController.printAllCommands();
        // Accept user input in a loop.
        while (true) {
            Scanner scanner = new Scanner(System.in);
            int option = scanner.nextInt();
            trainController.executeOption(option);
            System.out.println("Waiting for the next input");
        }
    }

    public void printAllCommands() {
        System.out.println("Choose from the below options");
        System.out.println("1. Start 5 trains");
        System.out.println("2. Get status of all trains");
        System.out.println("3. Stop all trains");
        System.out.println("4. Start all trains");
        System.out.println("5. Exit");
        System.out.println("6. Control individual train");
        System.out.println("7. Print all commands");
    }

    public void executeOption(final int option) {
        switch (option) {
            case 1:
                createAndStartTrains(5);
                break;
            case 2:
                printCurrentStateOfAllTrains();
                break;
            case 3:
                stopAllTrains();
                break;
            case 4:
                startAllTrains();
                break;
            case 5:
                stopAllTrains();
                System.exit(0);
            case 6:
                String instruction = System.console().readLine();
                System.out.println("Instruction received: " + instruction);
                if (instruction.toLowerCase().split(" ").length == 2) {
                    try {
                        int index = mInstructionParser.getIndex(instruction);
                        switch (instruction.toLowerCase().split(" ")[0]) {
                            case "start":
                                startTrain(index);
                                break;
                            case "stop":
                                stopTrain(index);
                                break;
                        }
                    } catch (IllegalArgumentException e) {
                        // Do nothing.
                    }
                }
                break;
            case 7:
                printAllCommands();
                break;
        }
    }

    /**
     * Initialize trains with different speeds and start them on different threads.
     * @param count of trains to start
     */
    public void createAndStartTrains(final int count) {
        final String trainName = "Train ";
        for (int i = 0; i < count; i++) {
            Train train = new Train(trainName + i, i * 1000);
            mTrains.add(train);
            train.startTrain();
        }
    }

    /**
     * Checks that the distance between successive trains is more than 1000 units.
     * Whenever the distance between successive trains is less than 1000 units, it calls {@link Train#stopTrain()} on the rear train.
     * Whenever the distance between successive trains is more than 1000 units, it calls {@link Train#startTrain()} on the rear train.
     */
    private void checkAndControlTrains() {
        // Loop to stop a train if it is within 1000 units of the train ahead of it.
        for (int i = 0; i < mTrains.size() - 1; i++) {
            if (mTrains.get(i + 1).getDistance() - mTrains.get(i).getDistance() - 3 * mTrains.get(i).getSpeed() <= 1000) {
                mTrains.get(i).stopTrain();
            }
        }
        // Loop to start a train if it is more than 1000 units away from the train ahead of it and it was not stopped manually.
        for (int i = 0; i < mTrains.size() - 1; i++) {
            if (mTrains.get(i + 1).getDistance() - mTrains.get(i).getDistance() - mTrains.get(i).getSpeed() > 1000) {
                Train train = mTrains.get(i);
                if (!mManuallyStoppedTrains.contains(train) && !train.isRunning()) {
                    train.startTrain();
                }
            }
        }
    }

    private void printCurrentStateOfAllTrains() {
        if (mTrains.size() == 0) {
            System.out.println("No train is in running state");
            return;
        }
        System.out.println("Printing status of all trains");
        for (Train train : mTrains) {
            train.printCurrentState();
        }
    }

    private void stopAllTrains() {
        for (Train train : mTrains) {
            train.stopTrain();
        }
    }

    private void startAllTrains() {
        for (Train train : mTrains) {
            train.startTrain();
        }
    }

    /**
     * Stop the train with the provided index.
     * @param trainIndex the index of the train to stop
     */
    private void stopTrain(int trainIndex) {
        // First add the train to the list of manually stopped trains. Otherwise the checkAndControl loop will start the train again.
        mManuallyStoppedTrains.add(mTrains.get(trainIndex));
        mTrains.get(trainIndex).stopTrain();
    }

    /**
     * Start the train with the provided index safely.
     * This checks if the train ahead is more than 1000 units ahead before starting the train.
     * @param trainIndex the index of the train to start
     */
    private void startTrain(int trainIndex) {
        if (trainIndex < mTrains.size() - 1) {
            if (mTrains.get(trainIndex + 1).getDistance() - mTrains.get(trainIndex).getDistance() <= 1000) {
                System.out.println("Cannot start train " + trainIndex + " since the train ahead is within 1000");
                return;
            }
            System.out.println("Starting train " + trainIndex + " since the train ahead is more than 1000");
        }
        // First remove the train from the list of manually stopped trains. Otherwise the checkAndControl loop will not start the train again.
        mManuallyStoppedTrains.remove(mTrains.get(trainIndex));
        mTrains.get(trainIndex).startTrain();
    }

    private static class InstructionParser {
        public int getIndex(final String instruction) {
            String lowerCaseInstruction = instruction.toLowerCase();
            if (!lowerCaseInstruction.startsWith("start") &&
                    !lowerCaseInstruction.startsWith("stop")) {
                System.out.println("Invalid instruction");
                throw new IllegalArgumentException("Invalid instruction provided. Cannot proceed");
            }
            return Integer.parseInt(lowerCaseInstruction.split(" ")[1]);
        }
    }

    private static class Train extends Thread {
        private final int mSpeed;
        private boolean mIsRunning;
        private int mDistance = 0;
        private int dummyLooper = 0;
        private boolean mIsInitialized;

        public Train(String name, int speed) {
            super(name);
            mSpeed = speed;
        }

        @Override
        public void run() {
            while (true) {
                dummyLooper++; // dummyLooper is just to keep this thread from becoming No-op when mIsRunning is false.
                while (mIsRunning) {
                    dummyLooper = 0;
                    try {
                        // The train runs at mSpeed units per second.
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    mDistance += mSpeed;
                }
                if (dummyLooper > 0) {
                    mIsRunning = false;
                }
            }
        }

        public int getDistance() {
            return mDistance;
        }

        public int getSpeed() {
            return mSpeed;
        }

        public boolean isRunning() {
            return mIsRunning;
        }

        /**
         * Sets the mIsRunning to true starts this thread.
         */
        public void startTrain() {
            mIsRunning = true;
            // Calling start always causes {@link IllegalThreadStateException}. Hence mIsInitialized is used as a check.
            if (!mIsInitialized) {
                mIsInitialized = true;
                start();
            }
        }

        public void stopTrain() {
            mIsRunning = false;
        }

        public void printCurrentState() {
            if (mIsRunning) {
                System.out.println(getName() + " is running and has reached " + mDistance);
            } else {
                System.out.println(getName() + " is stopped and has reached " + mDistance);
            }
        }
    }
}