// RoadNetwork.java // author Doug Jones // version 2019-03-04 import java.io.FileInputStream; import java.io.FileNotFoundException; import java.util.List; import java.util.LinkedList; import java.util.PriorityQueue; import java.util.regex.MatchResult; import java.util.regex.Pattern; import java.util.Scanner; import java.util.Random; /** Error reporting package * Provide a standard prefix and behavior for error reporting * @author Douglas W. Jones * @version 2019-02-13 */ class Errors { private static String prefix = "??: "; // prefix string for error messages private static int errorCount = 0; // count of errors reported /** Set prefix on error reports, should be done before any error reports * @arg p the prefix on any error messages */ public static void setPrefix( String p ) { prefix = p; } /** Report nonfatal errors, output a message and return * @arg m the message to output */ public static void warn( String m ) { System.err.println( prefix + ": " + m ); errorCount = errorCount + 1; } /** Report fatal errors, output a message and die * @arg m the message to output */ public static void fatal( String m ) { warn( m ); System.exit( 1 ); } /** Tell user if there were any errors * @returns true if none, false otherwise */ public static boolean ok() { return errorCount == 0; } } /** Support package for application dependent extensions to class Scanner * @author Douglas Jones * @version 2019-03-04 * Based on MP3.java version 2019-03-04 * which was based on RoadNetwork.java version 2019-03-01 * augmented to use lambda expressions for error messages * and to scan floating point numbers * @see Errors * If class Scanner wasn't final, this could be a subclass */ class ScanSupport { // patterns used in methods below private static final Pattern name = // match one legal name Pattern.compile( "[A-Za-z]*[A-Za-z0-9]" ); private static final Pattern blanks = // match any number of blanks Pattern.compile( "[ \t]*" ); private static final Pattern theRest = // match text up to newline Pattern.compile( "[^\n]*" ); /** Support interface for passing deferred computation of message text * set up so that lambda notation can be used */ public static interface MessageCarrier { String content(); } /** Scan and return one name, if available * @arg sc the scanner to read from * @arg msg the message to output if there is no name * @returns the name, or null if there is no name * Typically called as ScanSupport.scanName( sc, () -> string + expr ) */ public static String scanName( Scanner sc, MessageCarrier msg ) { if (sc.hasNext( name )) return sc.next(name); Errors.warn( msg.content() ); return null; } /** Scan and return one nonnegative int, if available * @arg sc the scanner to read from * @arg msg the message prefix to output if there is no int * @returns the value, or 0 if there is no number * Typically called as ...scanPositiveInt( sc, () -> string + expr ) */ public static int scanPositiveInt( Scanner sc, MessageCarrier msg ) { if (sc.hasNextInt()) { int value = sc.nextInt(); if (value >= 0) return value; Errors.warn( msg.content() + " " + value + ": must be positive" ); } Errors.warn( msg.content() + ": integer value expected" ); return 0; } /** Scan and return one nonnegative float, if available * @arg sc the scanner to read from * @arg msg the message prefix to output if there is no float * @returns the value, or NaN if there is no number * Typically called as ...scanPositiveFloat( sc, () -> string + expr ) */ public static float scanPositiveFloat( Scanner sc, MessageCarrier msg ) { if (sc.hasNextFloat()) { float value = sc.nextFloat(); if (value >= 0.0F) return value; Errors.warn( msg.content() + " " + value + ": must be positive" ); } Errors.warn( msg.content() + ": float value expected" ); return Float.NaN; } /** Skip the rest of this line, and complain if it's nonblank noncomment * @arg sc the scanner to read from * @arg msg the object that computes the message to output if needed * Typically called as ScanSupport.finishLine( sc, () -> string + expr ) * This code improves on the posted solution for homework 6, problem 2 */ public static void finishLine( Scanner sc, MessageCarrier msg ) { // first skip trailing blanks sc.skip( blanks ); // then get everything that remains on this line sc.skip( theRest ); final String remainder = sc.match().group( 0 ); // then see if it all makes sense if ("".equals( remainder )) return; if (remainder.startsWith ("--")) return; Errors.warn( msg.content() + ": " + remainder ); } } /** Simulation framework * @author Douglas W. Jones * @version 2019-03-08 */ class Simulation { // this interface is so we can schedule events with lambda expressions public interface Action { void trigger( float time ); } // Events are scheduled on the event set private static class Event { final float time; // the simulated time of the event final Action act; // the action to trigger at that time Event( float t, Action a ) { time = t; act = a; } } // the central organizing data structure of the simulation private static final PriorityQueue eventSet = new PriorityQueue ( ( Event e1, Event e2 ) -> Float.compare( e1.time, e2.time ) ); /** schedule a new event * @param t the time at which the event should occur * @param a the Action that should occur at that time * A typical call will look like this * Simulation.schedule( someTime, (float t)->codeToRunAt( t ) ); * That is, the Action will be constructed by a lambda expression */ public static void schedule( float t, Action a ) { eventSet.add( new Event( t, a ) ); } /** run a simulation * Call this after scheduling at least one event. * The simulation will run until either there are no more events or * some event terminates the program. * From this point on, a typical simulation program will be event driven * with the ordering of computations determined by chronological ordering * of scheduled events. */ public static void run() { while (!eventSet.isEmpty()) { Event e = eventSet.remove(); e.act.trigger( e.time ); } } } /** Utility class for a single pseudo random stream everyone uses * @author Douglas Jones * @version 2019-03-15 */ class MyRandom { /** The one and only random stream */ public static final Random stream = new Random( 5 ); } /** Roads are one-way paths between Intersections * @author Douglas Jones * @version 2019-03-14 * @see Intersection */ class Road { private final float travelTime; // how long to get to the other end private final Intersection destination; // where does this road go private final Intersection source; // where does this road come from private final int which; // which road into dest is this // BUG Need attributes of a road /** construct a new Road * @param sc the scanner used to get the attributes of this road * When called, the keyword "road" has already been scanned, * so we are ready to scan the source and destination plus other stuff. */ public Road( Scanner sc ) { final String srcName; // source intersection's name final String dstName; // destination intersection's name final String errName; srcName = ScanSupport.scanName( sc, () -> "Road ???" ); if (srcName == null) { errName = "???"; } else { errName = srcName; } dstName = ScanSupport.scanName( sc, () -> "Road " + errName + " ???" ); // names are defined here but may be null if missing source = RoadNetwork.findIntersection( srcName ); destination = RoadNetwork.findIntersection( dstName ); // it is legal to call toString now because all fields are initialized // deal with errors if ((source == null) && (srcName != null)) { Errors.warn( "" + this + " ill defined source" ); } if ((destination == null) && (dstName != null)) { Errors.warn( "" + this + " ill defined destination" ); } // deal with delay travelTime = ScanSupport.scanPositiveFloat( sc, () -> this.toString() ); ScanSupport.finishLine( sc, () -> this + " followed by Junk" ); // tell source and destination intersections about this road if (source != null) source.registerOutgoing( this ); if (destination!= null) { which = destination.registerIncoming( this ); } else { which = 0; } // BUG -- what about telling the destination about this road? } public String toString() { // prepare for missing fields of a badly declared intersection String src = "???"; String dst = "???"; // find real values if ((source != null) && (source.name != null)) { src = source.name; } if ((destination != null) && (destination.name != null)) { dst = destination.name; } return "Road " + src + ' ' + dst + ' ' + travelTime; } // check sanity and finish initialization /** check sanity and finish initializing this road */ public void check() { // nothing to do here } // simulation methods /** push one vehicle into this road * @param now tells when the vehicle enters */ public void enter( float now ) { System.out.println( now + ": vehicle enters " + this ); Simulation.schedule( now + travelTime, (float t)->this.exit( t ) ); } // a vehicle exits this road into the destination interseciton private void exit( float now ) { destination.enter( now, which ); } } /** Intersections are joined by Roads * @author Douglas Jones * @version 2019-03-01 * @see Road * There are many subclasses of Intersection * @see StopLight * @see Source */ abstract class Intersection { /** The name of this interseciton */ public String name; // the interesection's name or null if broken // simulation variables protected final float delay; // how long it takes to go through this protected boolean occupied = false; // is a vehicle currently in this // where this intersection connects protected final LinkedList outgoing = new LinkedList (); protected final LinkedList incoming = new LinkedList (); /** construct a new generic Intersection * @param sc the scanner used to get the attributes of this intersection * When called, the keyword "intersection" has already been scanned, * so we are ready to scan the additional attributes, if any. * This does not deal with the rest of the line, because each * specific kind of Intersection is assumed to do that. */ public Intersection( Scanner sc ) { name = ScanSupport.scanName( sc, () -> "Intersection ???" ); delay = ScanSupport.scanPositiveFloat( sc, () -> "Intersection "+name ); if (RoadNetwork.findIntersection( name ) != null) { Errors.warn( "Name reused for intersection " + name ); name = "reused-" + name; // BUG -- would we be better off throwing an exception here? } // BUG -- what what about other attributes? } /** tell an intersection that it has an outgoing road * @param r the road that leads from this intersection */ public void registerOutgoing( Road r ) { outgoing.add( r ); } /** tell an intersection that it has an incoming road * @param r the road that leads to this intersection * @returns the number of this road into this intersection */ public int registerIncoming( Road r ) { incoming.add( r ); return incoming.size() - 1; } public String toString() { if (name != null) { return "Intersection " + name; } else { return "Intersection ???"; } } // check sanity and finish initialization /** check sanity and finish initializing this intersection */ public void check() { if ( (incoming.isEmpty()) && (outgoing.isEmpty()) ) { Errors.warn( this.toString() + ": unconnected Intersection" ); } } // simulation support // pick one outgoing road from this interseciton for a vehicle to take protected Road pickRoad() { int r = Math.abs( MyRandom.stream.nextInt() ); int i = r % outgoing.size(); // BUG -- costs depend on the list size, is this a problem? return outgoing.get( i ); } /** a vehicle enters this intersection * @param time that this vehicle enters * @param source tells where the vehicle came from */ public abstract void enter( float now, int source ); // every subclass of Intersection is likely to have a private exit method } /** Stoplights are a kind of Intersection * @author Douglas Jones * @version 2019-03-01 * @see Intersection */ final class StopLight extends Intersection { // simulation variables of a stoplight private int direction = 0; // current direction that stoplight is green private final float period; // how long it stays green // queues of waiting vehicles for each incoming road private int[] queues; // eventually, will be new int[ incoming.size() ] /** construct a new StopLight * @param sc the scanner used to get the attributes of this StopLight * When called, the keyword "stoplight" has already been scanned, * so we are ready to scan the additional attributes, if any. */ public StopLight( Scanner sc ) { super( sc ); period = ScanSupport.scanPositiveFloat( sc, () -> "Stoplight " + name ); // BUG -- scan attributes of this stoplight ScanSupport.finishLine( sc, () -> this + " followed by junk" ); } public String toString() { if (name != null) { return "Stoplight " + name; } else { return "Stoplight ???"; } } // check sanity and finish initialization /** check sanity and finish initializing this Stoplight */ public void check() { super.check(); queues = new int[ incoming.size() ]; // initialize each queue to empty -- trivial, default sets all zero // launch the light-change process Simulation.schedule( period, (float t) -> this.lightChange( t ) ); } // simulation methods /** a vehicle enters this stoplight intersection * @param time that this vehicle enters */ public void enter( float now, int source ) { if ((source == direction) && (!occupied)) { // green light System.out.println( now + ": vehicle zipped through " + this ); occupied = true; Simulation.schedule( now + delay, (float t) -> this.exit( t ) ); } else { // red light System.out.println( now + ": vehicle stopped at " + this ); queues[ source ] = queues[ source ] + 1; // enqueue me } } // exit this stoplight private void exit( float now ) { // push this vehicle out of the intersection this.pickRoad().enter( now ); if (queues[ direction ] != 0) { // let the next car go! queues[ direction ] = queues[ direction ] - 1; // dequeue a vehicle Simulation.schedule( now + delay, (float t) -> this.exit( t ) ); } else { occupied = false; } } // stoplight light changing process private void lightChange( float now ) { direction = (direction + 1) % incoming.size(); if ((queues[ direction ] != 0) && (!occupied)) { queues[ direction ] = queues[ direction ] - 1; // dequeue a vehicle occupied = true; Simulation.schedule( now + delay, (float t) -> this.exit( t ) ); } Simulation.schedule( now + period, (float t) -> this.lightChange( t ) ); } } /** Sources are a kind of Intersection * @author Douglas Jones * @version 2019-03-11 * @see Intersection */ final class Source extends Intersection { // super.delay // time before the first vehicle is created private int number; // number of vehicles this source will generate private float interval; // time between successive vehicles /** construct a new Source * @param sc the scanner used to get the attributes of this Source * When called, the keyword "stoplight" has already been scanned, * so we are ready to scan the additional attributes, if any. */ public Source( Scanner sc ) { super( sc ); // get the new attributes of this source number = ScanSupport.scanPositiveInt( sc, () -> "Source "+ name +" "+ delay ); interval = ScanSupport.scanPositiveFloat( sc, () -> "Source " + name + " " + delay + " " + number ); // kick off the sequence of events for this source intersection Simulation.schedule( delay, (float t) -> this.exit( t ) ); ScanSupport.finishLine( sc, () -> this + " followed by junk" ); } public String toString() { if (name != null) { return "Source " + name +" "+ delay +" "+ number +" "+ interval; } else { return "Source ???" + name +" "+ delay +" "+ number +" "+ interval; } } // simulation methods /** a vehicle enters this source intersection (should never happen) * @param time that this vehicle enters * @param source tells where the vehicle came from */ public void enter( float now, int source ) { Errors.warn( this.toString() + " illegally entered at time " + now ); } // vehicle leaves this intersection private void exit( float now ) { // push a vehicle into the rest of the model this.pickRoad().enter( now ); // Simulation.schedule( now, (float t) -> this.pickRoad().enter( t ) ); // continue the process of vehicle creation number = number - 1; if (number > 0) { Simulation.schedule( now + interval, (float t)->this.exit( t ) ); } } } /** Sinks are a kind of Intersection * @author Douglas Jones * @version 2019-03-11 * @see Intersection */ final class Sink extends Intersection { // super.delay // ignored field that is required anyway /** construct a new Sink * @param sc the scanner used to get the attributes of this Source * When called, the keyword "stoplight" has already been scanned, * so we are ready to scan the additional attributes, if any. */ public Sink( Scanner sc ) { super( sc ); // sink intersections are really simple ScanSupport.finishLine( sc, () -> this + " followed by junk" ); } public String toString() { if (name != null) { return "Sink " + name; } else { return "Sink ???"; } } // simulation methods /** a vehicle enters this sink intersection * @param time that this vehicle enters * @param source tells where the vehicle came from */ public void enter( float now, int source ) { System.out.println( now + ": vehicle crushed at " + this ); // crush and discard vehicle } } /** Main program * @author Douglas Jones * @version 2019-02-13 * @see Road * @see Interseciton */ public class RoadNetwork { // lists of all the parts of this model private static final List intersections = new LinkedList (); private static final List roads = new LinkedList (); /** look up an intersection by name * @param n the name of the intersection, possibly null (matches nothing) * @returns the Intersection with that name, or null if no match */ public static Intersection findIntersection( String n ) { // stupid code, a linear search, but does it matter? for (Intersection i: intersections) { if (i.name.equals( n )) return i; } return null; } // build the road network by scanning a source file private static void buildNetwork( Scanner sc ) { // BUG -- what if there is no next while (sc.hasNext()) { // pick off the next part of the network description String command = sc.next(); if ("stoplight".equals( command )) { intersections.add( new StopLight( sc ) ); } else if ("source".equals( command )) { intersections.add( new Source( sc ) ); } else if ("sink".equals( command )) { intersections.add( new Sink( sc ) ); } else if ("road".equals( command )) { roads.add( new Road( sc ) ); } else { Errors.warn( "invalid command " + command ); } // BUG -- it would be nice to allow some kind of comments } } // check sanity and finish initialization of network private static void checkNetwork() { for (Intersection i: intersections) { i.check(); } for (Road r: roads) { r.check(); } } // print out the entire road network private static void printNetwork() { for (Intersection i: intersections) { System.out.println( i ); } for (Road r: roads) { System.out.println( r ); } } public static void main( String[] args ) { Errors.setPrefix( "RoadNetwork" ); if (args.length < 1) { Errors.fatal( "missing argument" ); } if (args.length > 1) { Errors.warn( "extra arguments" ); } try { // args[0] is the text file holding the road network, buildNetwork( new Scanner( new FileInputStream( args[0] ) ) ); checkNetwork(); printNetwork(); // something testable! if (Errors.ok()) Simulation.run(); } catch( FileNotFoundException e ) { Errors.fatal( "can't open file" ); } } }