/** RoadNetwork.java -- classes that describe a road network * @author Douglas Jones * @version 2017-03-31 * This version of the code builds on the version distributed on 03-22 by * adding behavior for uncontrolled intersections and stoplights. * It compiles without error and runs. * The output is a rather useless trace of vehicles passing through * intersections, proving that the simulation mechanism works. */ import java.util.LinkedList; import java.io.File; import java.io.FileNotFoundException; import java.util.Scanner; import java.util.regex.Pattern; import java.util.PriorityQueue; import java.util.Random; /** Utility package for error handling */ class Errors { private Errors(){}; // you may never instantiate this class private static int count = 0; // warning count, really public read only /** Provide public read only access to the count of warnings. */ public static int count() { return count; } /** Call this to warn of non fatal errors */ public static void warn( String message ) { System.err.println( "Warning: " + message ); count = count + 1; } /** Call this to report fatal errors */ public static void fatal( String message ) { System.err.println( "Fatal error: " + message ); System.exit( -1 ); } } /** Support methods for scanning * @see Errors */ class ScanSupport { /** Pattern for identifers */ public static final Pattern name // letter followed by alphanumeric = Pattern.compile( "[a-zA-Z][a-zA-Z0-9_]*|" ); /** Pattern for whitespace excluding things like newline */ public static final Pattern whitespace = Pattern.compile( "[ \t]*" ); /** Get next name without skipping to next line (unlike sc.Next()) * @param sc the scanner from which end of line is scanned * @return the name, if there was one, or an empty string */ public static String nextName( Scanner sc ) { sc.skip( whitespace ); // the following is weird code, it skips the name // and then returns the string that matched what was skipped sc.skip( name ); return sc.match().group(); } /** Class used only for deferred parameter passing to lineEnd */ public interface EndMessage { public abstract String myString(); } /** Advance to next line and complain if is junk at the line end; * call this when all useful content has been consumed from the line * then call this to skip optional line-end comments to the next line * @see Errors * @param sc the scanner from which end of line is scanned * @param message will be evaluated only when there is an error; * it is typically passed as a lambda expression, for example, * ScanSupport.lineEnd( sc, () -> "this " + x + " that" ); */ public static void lineEnd( Scanner sc, EndMessage message ) { sc.skip( whitespace ); String lineEnd = sc.nextLine(); if ( (!lineEnd.equals( "" )) && (!lineEnd.startsWith( "--" )) ) { Errors.warn( "" + message.myString() + " followed unexpected by '" + lineEnd + "'" ); } } } /** Simulation support framework */ class Simulation { /** Interface allowing actions to be passed as lambda expressions */ public interface Action { void trigger( float time ); } /** Events are queued for Simulation.run to find */ private static class Event { float time; Action act; // constructor Event( float t, Action a ) { time = t; act = a; }; void trigger() { act.trigger( time ); } } private static PriorityQueue eventSet = new PriorityQueue ( (Event e1, Event e2)->Float.compare( e1.time, e2.time ) ); /** Users call schedule to schedule one action at some time. * @param time specifies when the event should occur. * @param act specifies what to do at that time. * Typically, act is a lambda expression, so a call to schedule could * look like this: *
	 *  Simulation.schedule( t, (float t)->object.method( params, t ) )
	 *  
*/ public static void schedule( float time, Action act ) { eventSet.add( new Event( time, act ) ); } /** the main program should build the model, * this inolves scheduling some initial events * and then, just once, it should call run. */ public static void run() { while (!eventSet.isEmpty()) { Event e = eventSet.remove(); e.trigger(); } } } class PRNG { private static Random rand = new Random(); //Bug: seeding on the above is dumb public static int nextInt( int cieling ) { return rand.nextInt( cieling ); } //Notice: other distributions should be computed from rand here } /** Roads link intersections * @see Intersection */ class Road { private final float travelTime; // how long to travel down road private final Intersection destination; // where road goes, or null private final Intersection source; // source of road, or null private int dir; // direction of this road // Road name is the source-destination names /** initializer scans and processes one road definition * @param sc the Scanner from which the Road description is read */ public Road( Scanner sc ) { String srcName = ScanSupport.nextName( sc ); String dstName = ScanSupport.nextName( sc ); // if there are no next names on this line, these are "" // therefore, the findIntersection calls below will fail source = RoadNetwork.findIntersection( srcName ); if (source == null) { Errors.warn( "Road '" + srcName + "' '" + dstName + "' source undefined." ); } destination = RoadNetwork.findIntersection( dstName ); if (destination == null) { Errors.warn( "Road '" + srcName + "' '" + dstName + "' destination undefined." ); } if (sc.hasNextFloat()) { travelTime = sc.nextFloat(); } else { travelTime = Float.NaN; } ScanSupport.lineEnd( sc, () -> "Road '" + srcName + "' '" + dstName + "'" ); // do sanity checks on fields, at this point, toString is legal if (travelTime < 0.0f) Errors.warn( this.toString() + ": has a negative travel time?" ); if (travelTime != travelTime) Errors.warn( // odd condition above! True when travelTime is NaN this.toString() + ": no travel time given." ); // let the source and destination know about this road if (destination != null) dir = destination.addIncoming( this ); if (source != null) source.addOutgoing( this ); } /** check this road to see if it meets global sanity constraints */ public void check() { // nothing to check. } /** output this road in a format like that used for input */ public String toString() { String srcName; String dstName; if (source == null) { srcName = "???"; } else { srcName = source.name; } if (destination == null) { dstName = "???"; } else { dstName = destination.name; } return( "road " + srcName + " " + dstName + " " + travelTime ); } /********** simulation code for this road **********/ /** simulation method for arrival at this road * @param time the time at which the vehicle arrives * @param v the vehicle that arrives */ public void arrivalEvent( float time, Vehicle v ) { // schedule arrival event at the next intersection Simulation.schedule( time + travelTime, (float t) -> departureEvent( t, v ) ); } /** simulation method for departure from this road * @param time the time at which the vehicle departs * @param v the vehicle that departs */ public void departureEvent( float time, Vehicle v ) { // schedule arrival event at the next intersection Simulation.schedule( time, (float t) -> destination.arrivalEvent( t, v, dir ) ); // could just call destination.arrivalEvent( time, v, dir ); } } /** Intersections are linked by one-way roads * only subclasses of Intersection may be instantiated * @see Road * @see Traversable * @see NoStop * @see StopLight * @see Source * @see Sink */ abstract class Intersection { /** list of from to this intersection */ protected final LinkedList outgoing = new LinkedList (); /** getter method for outgoing road list. * Vehicles use this to "read their road map" to navigate. * @returns the size of the list, without offering access to the list */ public int outgoingSize() { return outgoing.size(); } /** getter method for outgoing road list. * Vehicles use this to "read their road map" to navigate. * @returns a selected element of the list, without offering access */ public Road outgoingGet( int element ) { return outgoing.get( element ); } /** setter method, how the intersection learns what roads leave it. * @param r a road that leaves this intersection */ public void addOutgoing( Road r ) { outgoing.add( r ); } /** list of roads to this intersection; this protectected so that * subclasses can have one queue per incoming road */ protected final LinkedList incoming = new LinkedList (); /** setter method, how the intersection learns what roads enter it. * @param r a road that enters this intersection * @returns index of the incoming road */ public int addIncoming( Road r ) { incoming.add( r ); return incoming.size() - 1; } public String name; // the name of the intersection /** factory scans and processes one intersection definition * @param sc Scanner from which the Intersectios description is read * @return either a new Intersection or null if description is broken */ public static Intersection newIntersection( Scanner sc ) { String myName = ScanSupport.nextName( sc ); if ((myName == null) || ("".equals( myName ))) { Errors.warn( "Intersection has no name" ); sc.nextLine(); return null; } if (RoadNetwork.findIntersection( myName ) != null) { Errors.warn( "Intersection '" + myName + "' redefined." ); sc.nextLine(); return null; } String kind = ScanSupport.nextName( sc ); if ((kind == null) || ("".equals( kind ))) { // default NoStop return new NoStop( sc, myName ); } else if ("stoplight".equals( kind )) { // stoplight return new StopLight( sc, myName ); } else if ("source".equals( kind )) { // source of vehicles return new Source( sc, myName ); } else if ("sink".equals( kind )) { // sink consumes vehicles return new Sink( sc, myName ); } else { // error Errors.warn( "Intersection '" + myName + "' '" + kind + "' not an intersection kind" ); sc.nextLine(); } return null; } /** check this Intersection to see if it meets global sanity constraints */ public void check() { if (incoming.isEmpty()) { Errors.warn( this.toString() + ": no incoming roads" ); } if (outgoing.isEmpty()) { Errors.warn( this.toString() + ": no outgoing roads" ); } } /** output this Intersection in a format like that used for input */ public String toString() { return( "intersection " + name ); } /** simulation method for arrival at this intersection * @param time the time at which the vehicle arrives * @param v the vehicle that arrives * @param v arrival direction */ public abstract void arrivalEvent( float time, Vehicle v, int dir ); /** simulation method for departure from this intersection * @param time the time at which the vehicle departs * @param v the vehicle that departs */ public abstract void departureEvent( float time, Vehicle v ); } /** Traversable Intersections * @see Intersection * @see NoStop * @see StopLight */ abstract class Traversable extends Intersection { protected float traversalTime; protected void getTraversalTime( Scanner sc ) { if (sc.hasNextFloat()) { traversalTime = sc.nextFloat(); if (traversalTime < 0.0f) Errors.warn( "Intersection '" + name + "' '" + traversalTime + "' has a negative traversal time?" ); } else { Errors.warn( "Intersection '" + name + "' no traversal time given." ); traversalTime = 99.999f; } } } /** Uncontrolled Intersections, cars pass through first-come first-served. * @see Intersection */ class NoStop extends Traversable { /** initializer scans and processes one uncontrolled Intersection * @param sc Scanner from which the Intersectios description is read * @param myName the value to be put in the name field */ public NoStop( Scanner sc, String myName ) { // now keyword for intersection type was scanned name = myName; getTraversalTime( sc ); ScanSupport.lineEnd( sc, () -> "Intersection '" + name + "'" ); } /** output this uncontrolled intersection in a format like the input */ public String toString() { return( super.toString() + " " + traversalTime ); } /********** simulation code for this intersection **********/ // control over vehicles waiting to get through the intersection private boolean occupied = false; // the intersection is initially open private final LinkedList waiting = new LinkedList (); /** simulation method for arrival at this uncontrolled intersection * @param time the time at which the vehicle arrives * @param v the vehicle that arrives * @param dir arrival direction */ public void arrivalEvent( float time, Vehicle v, int dir ) { if (occupied) { // make this vehicle wait waiting.add( v ); } else { // let this vehicle continue onward Simulation.schedule( //Bug: actual traversal could involve vehicle time + traversalTime, (float t)-> this.departureEvent( t, v ) ); occupied = true; } } /** simulation method for departure from this intersection * @param time the time at which the vehicle departs * @param v the vehicle that departs */ public void departureEvent( float time, Vehicle v ) { // first, make vehicle v arrive at an outgoing road now Simulation.schedule( time, (float t)->v.selectRoad( this ).arrivalEvent( t, v ) ); System.out.println( "At " + time + " vehicle " + v + " left " + name ); // second, see if someone else can enter this intersection if (waiting.isEmpty()) { // if someone arrives later, they won't wait occupied = false; } else { // get a waiting vehicle and let it continue Vehicle v1 = waiting.remove(); Simulation.schedule( //Bug: actual traversal could involve vehicle time + traversalTime, (float t)-> this.departureEvent( t, v1 ) ); } } } /** StopLight protected Intersections, cars pass through on green lights. * @see Intersection */ class StopLight extends Traversable { // attributes of the stop light timing cycle private final float greenTime; private final float yellowTime; /** initializer scans and processes one stoplight Intersection * @param sc Scanner from which the Intersectios description is read * @param myName the value to be put in the name field */ public StopLight( Scanner sc, String myName ) { // now keyword for StopLight was scanned name = myName; getTraversalTime( sc ); if (sc.hasNextFloat()) { greenTime = sc.nextFloat(); if (greenTime < 0.0f) Errors.warn( "Intersection '" + name + "' Stoplight '" + greenTime + "' has a negative green time?" ); } else { Errors.warn( "Intersection '" + name + "' stoplight, no green time given." ); greenTime = 99.999f; } if (sc.hasNextFloat()) { yellowTime = sc.nextFloat(); if (yellowTime < 0.0f) Errors.warn( "Intersection '" + name + "' Stoplight '" + greenTime + "' '" + yellowTime + "' has a negative yellow time?" ); } else { Errors.warn( "Intersection '" + name + "' stoplight, no yellow time given." ); yellowTime = 99.999f; } ScanSupport.lineEnd( sc, () -> "Intersection '" + name + "' stoplight '" + greenTime + "' '" + yellowTime ); // start the logical process for this stoplight Simulation.schedule( greenTime + yellowTime, //Bug: the above is simplistic, isn't yellow different? (float t) -> lightChangeEvent( t ) ); } /** output this Intersection in a format like that used for input */ public String toString() { return( super.toString() + " stoplight" + traversalTime + " " + greenTime + " " + yellowTime ); } /********** simulation code for this stoplight **********/ private int lightDir = 0; // initial green light direction private boolean occupied = false; // is there a vehicle here private LinkedList waiting[]; // queues for each incoming road private int incomingSize; // incoming.size() for efficiency /** check this StopLight to see if it's sane and finish initialization */ public void check() { super.check(); // do the regular checks // finish the initialization incomingSize = incoming.size(); // I wish I could write this code, but Java is broken // waiting = new LinkedList [incomingSize]; // So, I fake it with this class LinkedListVehicle extends LinkedList {} waiting = new LinkedListVehicle [incomingSize]; for (int i = 0; i < incomingSize; i++) { waiting[i] = new LinkedListVehicle (); } } /** simulation method for arrival at this stoplight * @param time the time at which the vehicle arrives * @param v the vehicle that arrives * @param dir the arrival direction */ public void arrivalEvent( float time, Vehicle v, int dir ) { if ((lightDir != dir) || (occupied)) { // make this vehicle wait waiting[dir].add( v ); } else { // let this vehicle continue onward Simulation.schedule( //Bug: actual traversal could involve vehicle time + traversalTime, (float t)-> this.departureEvent( t, v ) ); occupied = true; } } /** simulation method for departure from this stoplight * @param time the time at which the vehicle departs * @param v the vehicle that departs */ public void departureEvent( float time, Vehicle v ) { // first, make vehicle v arrive at an outgoing road now Simulation.schedule( time, (float t)->v.selectRoad( this ).arrivalEvent( t, v ) ); System.out.println( "At " + time + " vehicle " + v + " left " + name ); // second, see if someone else can enter this intersection if (waiting[lightDir].isEmpty()) { // if someone arrives later, they won't wait occupied = false; } else { // get a waiting vehicle and let it continue Vehicle v1 = waiting[lightDir].remove(); Simulation.schedule( //Bug: actual traversal could involve vehicle time + traversalTime, (float t)-> this.departureEvent( t, v1 ) ); } } /** simulation method for state changes at this stoplight * @param time the time at which the vehicle departs */ private void lightChangeEvent( float time ) { // change the light lightDir = lightDir + 1; if (lightDir >= incomingSize) lightDir = 0; // see if light change lets a car into the intersection if ((!waiting[lightDir].isEmpty()) && (!occupied)) { // get a waiting vehicle and let it continue Vehicle v = waiting[lightDir].remove(); Simulation.schedule( //Bug: actual traversal could involve vehicle time + traversalTime, (float t)-> this.departureEvent( t, v ) ); } // schedule the next change Simulation.schedule( time + greenTime + yellowTime, //Bug: the above is simplistic, isn't yellow different? (float t) -> lightChangeEvent( t ) ); } } /** Source Intersections create vehicles * @see Intersection */ class Source extends Intersection { // attributes of the source intersection final float arrivalTime; /** initializer scans and processes one source Intersection * @param sc Scanner from which the Intersectios description is read * @param myName the value to be put in the name field */ public Source( Scanner sc, String myName ) { // now keyword for Source was scanned name = myName; if (sc.hasNextFloat()) { arrivalTime = sc.nextFloat(); if (arrivalTime < 0.0f) Errors.warn( this.toString() + "' has a negative interarrival time?" ); } else { arrivalTime = 99.999f; Errors.warn( this.toString() + "' stoplight, no interarrival time given." ); } ScanSupport.lineEnd( sc, () -> this.toString() ); //launch this source vehicle generation process Simulation.schedule( arrivalTime, (float t)->this.departureEvent( t, new Vehicle() ) ); } /** output this Intersection in a format like that used for input */ public String toString() { return( super.toString() + " source " + arrivalTime ); } /** check this Source to see if it meets global sanity constraints */ public void check() { if (!incoming.isEmpty()) { Errors.warn( this.toString() + ": no incoming roads allowed" ); } if (outgoing.isEmpty()) { Errors.warn( this.toString() + ": no outgoing roads" ); } } /** simulation method for arrival at this intersection * @param time the time at which the vehicle arrives * @param v the vehicle that arrives * @param dir arrival direction */ public void arrivalEvent( float time, Vehicle v, int dir ) { //Bug: No vehicles ever arrive at a source, thow something. } /** simulation method for departure from this intersection * @param time the time at which the vehicle departs * @param v the vehicle that departs */ public void departureEvent( float time, Vehicle v ) { // first, make vehicle v arrive at an outgoing road now Simulation.schedule( time, (float t)->v.selectRoad( this ).arrivalEvent( t, v ) ); // we could have done v.selectRoad( ).arrivalEvent( ) System.out.println( "At " + time + " create vehicle " + v + " at " + name ); // second, schedule the next departure from the source Simulation.schedule( time + arrivalTime, //Bug: the above suggests a really stupid arrival model (float t)->this.departureEvent( t, new Vehicle() ) ); } } /** Sink Intersections destroy vehicdles and could collect statistics * @see Intersection */ class Sink extends Intersection { // attributes of the sink intersection? None! /** initializer scans and processes one Sink Intersection * @param sc Scanner from which the Intersectios description is read * @param myName the value to be put in the name field */ public Sink( Scanner sc, String myName ) { // now keyword for Source was scanned name = myName; ScanSupport.lineEnd( sc, () -> this.toString() ); } /** output this Intersection in a format like that used for input */ public String toString() { return( super.toString() + " sink" ); } /** check this Sink to see if it meets global sanity constraints */ public void check() { if (incoming.isEmpty()) { Errors.warn( this.toString() + ": no incoming roads" ); } if (!outgoing.isEmpty()) { Errors.warn( this.toString() + ": no outgoing roads allowed" ); } } /** simulation method for arrival at this intersection * @param time the time at which the vehicle arrives * @param v the vehicle that arrives * @param dir arrival direction */ public void arrivalEvent( float time, Vehicle v, int dir ) { //nothing happens unless we add code to gather statistics System.out.println( "At " + time + " crushed vehicle " + v + " at " + name ); } /** simulation method for departure from this intersection * @param time the time at which the vehicle departs * @param v the vehicle that departs */ public void departureEvent( float time, Vehicle v ) { //Bug: No vehicles ever depart from a sink, thow something. } } /** Simulated vehicles */ class Vehicle { /** Vehicles are the ones that make navigation decisions at * intersection. */ public Road selectRoad( Intersection i ) { //Bug: we use the most stupid algorithm, random decision return i.outgoingGet( PRNG.nextInt( i.outgoingSize() ) ); } } /** RoadNetwork -- main program that reads and writes a road network * @see Road * @see Intersection * @see Errors * @see main */ public class RoadNetwork { // lists of roads and intersectins static LinkedList roads = new LinkedList (); static LinkedList inters = new LinkedList (); /** utility method to look up an intersection by name * @param s is the name of the intersection, a string * @return is the Intersection object with that name */ public static Intersection findIntersection( String s ) { for ( Intersection i: inters ) { if (i.name.equals( s )) return i; } return null; } /** read a road network */ public static void initializeNetwork( Scanner sc ) { while (sc.hasNext()) { // until we hit the end of the file String command = ScanSupport.nextName( sc ); if (("intersection".equals( command )) || ("i".equals( command )) ) { Intersection i = Intersection.newIntersection(sc); if (i != null) inters.add( i ); } else if (("road".equals( command )) || ("r".equals( command )) ) { roads.add( new Road( sc ) ); } else if ("".equals( command )) { // blank line // line holding -- ends up here! ScanSupport.lineEnd( sc, () -> "Line" ); } else { Errors.warn( "Command '" + command + "' is not road or intersection" ); sc.nextLine(); // skip the rest of the error } } } /** check the sanity of the network */ public static void checkNetwork() { for ( Intersection i: inters ) { i.check(); } for ( Road r: roads ) { r.check(); } } /** write out a road network */ public static void writeNetwork() { for ( Intersection i: inters ) { System.out.println( i.toString() ); } for ( Road r: roads ) { System.out.println( r.toString() ); } } /** main program that reads and writes a road network */ public static void main( String[] args ) { // verify that the argument exists. if (args.length < 1) { Errors.fatal( "Missing file name on command line" ); } else if (args.length > 1) { Errors.fatal( "Unexpected command line args" ); } else try { initializeNetwork( new Scanner( new File( args[0] ) ) ); checkNetwork(); if (Errors.count() > 0) { writeNetwork(); } else { Simulation.run(); } } catch (FileNotFoundException e) { Errors.fatal( "Could not read '" + args[0] + "'" ); } } }