/* Road Network Simulator * Author: Douglas Jones * Status: * Version: 10/15/2020 */ import java.util.LinkedList; import java.util.Iterator; import java.util.Scanner; import java.util.PriorityQueue; import java.io.File; import java.io.FileNotFoundException; // Utility classes /** * Error handling */ abstract class Error{ private Error(){} // prevent instantiation of this class private static int errorCount = 0; private static final int errorLimit = 10; public static void warn( String message ) { System.err.println( message + "\n" ); errorCount = errorCount + 1; if (errorCount > errorLimit) System.exit( 1 ); } public static void fatal( String message ) { warn( message ); System.exit( 1 ); } public static void quitIfAny() { if (errorCount > 0) System.exit( 1 ); } } /** * Wrapper or Adapter for scanners that integrates error handling * @see java.util.Scanner * @see Error */ class MyScanner { Scanner self; // the scanner this object wraps /** * Parameter carrier interface for deferred string construction * used only for error message parameters to getXXX() methods */ static interface ErrorMessage { String myString(); } // constructor we wish to inherit from Scanner but can't because it's Java public MyScanner( File f ) throws FileNotFoundException { self = new Scanner( f ); } // methods we wish could inherit from Scanner but can't beause it's final public boolean hasNext() { return self.hasNext(); } public boolean hasNext( String s ) { return self.hasNext( s ); } public boolean hasNextFloat() { return self.hasNextFloat(); } public String next() { return self.next(); } public float nextFloat() { return self.nextFloat(); } public String nextLine() { return self.nextLine(); } // new methods we add to this class /** Get the next string, if one is available * @param def the default value if no string is available * @param msg the error message to print if no string is available */ public String getNext( String def, ErrorMessage msg ) { if (self.hasNext()) return self.next(); Error.warn( msg.myString() ); return def; } /** Get the next float, if one is available * @param def the default value if no string is available * @param msg the error message to print if no string is available */ public float getNextFloat( float def, ErrorMessage msg ) { if (self.hasNextFloat()) return self.nextFloat(); Error.warn( msg.myString() ); return def; } } /** Framework for discrete event simulation. */ abstract class Simulator { private Simulator(){} // prevent anyone from instantiating this class /** Interface to allow lambda parameters to schedule() * as such, no external code ever uses Action */ public interface Action { // actions contain the specific code of each event void trigger( double time ); } private static class Event { public double time; // the time of this event public Action act; // what to do at that time } private static PriorityQueue eventSet = new PriorityQueue ( (Event e1, Event e2)-> Double.compare( e1.time, e2.time ) ); /** Call schedule to make act happen at time. * Users typically pass the action as a lambda expression: *
     *  Simulator.schedule( t, ( double time )-> method( ... time ... ) )
     *  
*/ static void schedule( double time, Action act ) { Event e = new Event(); e.time = time; e.act = act; eventSet.add( e ); } /** run the simulation. * Call run() after scheduling some initial events to run the simulation. */ static void run() { while (!eventSet.isEmpty()) { Event e = eventSet.remove(); e.act.trigger( e.time ); } } } // Simulation Model Classes /** * Roads connect intersections * @see Intersection */ class Road { public static class ConstructorFail extends Exception {}; // instance variables private final double travelTime; private final Intersection destination; private final Intersection source; // the collection of all instances private static final LinkedList allRoads = new LinkedList (); /** The only constructor * @param sc MyScanner from which description comes * Input format scanned from sc: source-name destination-name travel-time *
where source-name is the name of the source intersection and *
destination-name is the name of the destination intersection and *
travel-time is a floating point time in seconds */ public Road( MyScanner sc ) throws ConstructorFail { // keyword Road was already scanned final String src; // where does it come from final String dst; // where does it go src = sc.getNext( "???", () -> "road: from missing source" ); dst = sc.getNext( "???", () -> "road " + src + ": to missing destination" ); travelTime = sc.getNextFloat( Float.NaN, () -> "road " + src + " " + dst + ": missing travel time" ); if ((src == "???") || (dst == "???") || Double.isNaN( travelTime )) { throw new ConstructorFail(); } destination = Intersection.lookup( dst ); if (destination == null) { Error.warn( "road " + src + " " + dst + " undefined: " + dst ); throw new ConstructorFail(); } source = Intersection.lookup( src ); if (source == null) { Error.warn( "road " + src + " " + dst + " undefined: " + src ); throw new ConstructorFail(); } if (travelTime < 0.0) { Error.warn( this.toString() + ": negative travel time?" ); throw new ConstructorFail(); } allRoads.add( this ); // this is the only place items are added! } /** Primarily for debugging * @return textual name and travel time of the road */ public String toString() { return "Road " + source.name + " " + destination.name + " " + travelTime; // BUG throws exception when source/destination is null } /** Allow outsiders to iterate over all roads * @return textual name and travel time of the road */ public static Iterator iterator() { return allRoads.iterator(); } } /** * Intersections are connected by roads, only subclasses are ever instantiated * @see Road */ abstract class Intersection { public static class ConstructorFail extends Exception {}; // instance variables final String name; final Double travelTime; private final LinkedList outgoing = new LinkedList (); private final LinkedList incoming = new LinkedList (); // the collection of all instances private static final LinkedList allIntersections = new LinkedList (); /** Constructor needed to initialize final fields * @param name of the intersection */ Intersection( String name, double travelTime ) { this.name = name; this.travelTime = travelTime; } /** The the factory for intersections * @arg sc the scanner from which the Intersection description is scanned * Input format scanned from sc: name sub *
where name is a string *
where subclass is a string *
The constructor for the subclass deals with other details, if any */ public static Intersection factory( MyScanner sc ) throws ConstructorFail { // pick off name of intersection; String name = sc.getNext( "???", () -> "Intersection name missing" ); double time = sc.getNextFloat( Float.NaN, () -> "Intersection " + name + ": missing travel time" ); if ((name == "???") || Double.isNaN( time )) { throw new ConstructorFail(); } if (lookup( name ) != null) { Error.warn( "Intersection " + name + ": redefined" ); throw new ConstructorFail(); } if (time < 0.0) { Error.warn( "Intersection " + name + " " + time + ": negative travel time?" ); throw new ConstructorFail(); } Intersection self; // this is the intersection the factory is working on // find out what kind of intersection call right constructor if (sc.hasNext( "stoplight" )) { sc.next(); // skip keyword stoplight self = new StopLight( sc, name, time ); } else if (sc.hasNext( "source" )) { sc.next(); // skip keyword source self = new Source( sc, name, time ); } else { // plain uncontrolled intersection self = new NoStop( sc, name, time ); } allIntersections.add( self ); return self; } /** Primarily for debugging * @return textual name and travel time of the road */ public String toString() { return "Intersection " + name + " " + travelTime; } /** Allow outsiders to iterate over all roads * @return textual name and travel time of the road */ public static Iterator iterator() { return allIntersections.iterator(); } /** Allow finding intersections by name * @return textual name and travel time of the road */ public static Intersection lookup( String n ) { for (Intersection i: allIntersections) { if (i.name.equals( n )) return i; } return null; } } /** * Uncontrolled Intersections * @see Interesction */ class NoStop extends Intersection { /** Constructor * @param sc scanner from which to scan any extra details * @param name the name of the intersection */ NoStop( MyScanner sc, String name, double time ) { super( name, time ); // needed because of final fields } /* Primarily for debugging * @return textual name and attributes of the intersection */ // public String toString() { // // the superclass version does this! // } } /** * Stoplight intersection * @see Interesction */ class StopLight extends Intersection { private final double period; // interval between light changes /** Constructor * @param sc scanner from which to scan any extra details * @param name the name of the intersection */ StopLight( MyScanner sc, String name, double time ) { super( name, time ); // needed because Interesection.name is final period = sc.getNextFloat( 0.1F, ()-> super.toString() +" stoplight: expected stoplight period" ); if (period <= 0) { Error.warn( this.toString() + ": stoplight period not positive" ); } else { Simulator.schedule( period, // BUG -- should multiply by random from 0.0 and 1.0 (double t)-> this.lightChange( t ) ); } } // simulation methods for StopLight private void lightChange( double time ) { Simulator.schedule( time + period, (double t)-> this.lightChange( t ) ); // BUG -- light changes should change which roads are blocked // BUG -- light changes should unblock any waiting vehicles System.out.println( this.toString() +": light change at t ="+ time ); } /** Primarily for debugging * @return textual name and attributes of the intersection */ public String toString() { return super.toString() + " Stoplight " + period; } } /** * Source intersections that produce traffic * @see Interesction */ class Source extends Intersection { private final double period; // average interval between vehicle production /** Constructor * @param sc scanner from which to scan any extra details * @param name the name of the intersection */ Source( MyScanner sc, String name, double time ) { super( name, time ); // needed because Interesection.name is final period = sc.getNextFloat( 0.1F, ()-> super.toString() +" source: expected source average interval" ); if (period <= 0) { Error.warn( this.toString() + ": source interval not positive" ); } else { Simulator.schedule( period, // BUG -- need to randomize time of first production (double t)-> this.produce( t ) ); } } // simulation methods for Source private void produce( double time ) { Simulator.schedule( time + period, // BUG -- need to randomize time of next production (double t)-> this.produce( t ) ); // BUG -- produce a vehicle v // BUG -- pick an outgoing road r // BUG -- r.enter( time, v ); System.out.println( this.toString() +": produces at t ="+ time ); } /** Primarily for debugging * @return textual name and attributes of the intersection */ public String toString() { return super.toString() + " Source " + period; } } /** * Main class builds model and will someday simulate it * @see Road * @see Intersection */ public abstract class RoadNetwork { private static void readNetwork( MyScanner sc ) { while (sc.hasNext()) { // until the input file is finished String command = sc.next(); if ("intersection".equals( command )) { try { Intersection.factory( sc ); } catch ( Intersection.ConstructorFail e ) {}; } else if ("road".equals( command )) { try { new Road( sc ); } catch ( Road.ConstructorFail e ) {}; } else if ("endtime".equals( command )) { float endTime = sc.getNextFloat( 0, ()-> "endtime: time expected" ); Simulator.schedule( endTime, (double t)-> System.exit( 0 ) ); } else if ("//".equals( command )) { sc.nextLine(); } else { Error.warn( "unknown command: " + command ); } } } private static void writeNetwork() { for (Iterator i = Intersection.iterator(); i.hasNext(); ){ System.out.println( i.next().toString() ); } for (Iterator i = Road.iterator(); i.hasNext(); ){ System.out.println( i.next().toString() ); } } public static void main( String[] args ) { if (args.length < 1) { Error.fatal( "Missing file name argument" ); } else try { readNetwork( new MyScanner( new File( args[0] ) ) ); } catch ( FileNotFoundException e ) { Error.fatal( "Can't open file: " + args[0] ); } if (args.length > 1) { try { float endTime = Float.parseFloat( args[1] ); Simulator.schedule( endTime, (double t)-> System.exit( 0 ) ); } catch ( NumberFormatException e ) { Error.fatal( "RoadNetwork "+ args[0] +" "+ args[1] +": Negative end?" ); } } Error.quitIfAny(); writeNetwork(); // BUG -- this code is for debugging only Simulator.run(); } }