// RoadNetwork.java /** Road network simulator (or it will become one once it's done) * @author Douglas W. Jones * @version Feb. 26, 2021 */ import java.io.File; import java.io.FileNotFoundException; import java.util.Collection; import java.util.LinkedList; import java.util.Random; import java.util.Scanner; import java.util.regex.Pattern; /** Centralized error reporting tools * error reports are output to System.err (standard error, aka stderr) * WARNING: Never create any instances or subclasses of Error. */ abstract class Error { private static int errorCount = 0; /** Fatal error * @param msg -- the warning message * this never returns, the application is terminated abnormally */ public static void fatal( String msg ) { System.err.println( "Error: " + msg ); System.exit( 1 ); // report failure to the OS (or to the shell) } /** Non-fatal warning * @param msg -- the warning message */ public static void warn( String msg ) { System.err.println( "Warning: " + msg ); errorCount = errorCount + 1; } /** If there have been any warnings prior to this call, exit */ public static void exitIfWarnings() { if (errorCount > 0) { System.exit( 1 ); // report failure to the OS (or to the shell) } } } /** Support for scanning input files with error reporting * @see Error * @see java.util.Scanner * Ideally, this would be extend class Scanner, but class Scanner is final * Therefore, this is a wrapper class around class Scanner */ class MyScanner { Scanner sc; // the scanner we are wrapping public MyScanner( File f ) throws FileNotFoundException { sc = new Scanner( f ); } // methods that we wish we could inhereit from Scanner public boolean hasNext() { return sc.hasNext(); } public boolean hasNextFloat() { return sc.hasNextFloat(); } public String next() { return sc.next(); } // patterns that matter here private static final Pattern delimPat = Pattern.compile( "[ \t\n\r]*" ); private static final Pattern namePat = Pattern.compile( "([A-Za-z][0-9A-Za-z]*)|" ); private static final Pattern floatPat = Pattern.compile( "([0-9][0-9]*\\.[0-9]*)|([0-9]*\\.[0-9][0-9]*)|([0-9]*)" ); // new methods added to class Scanner /** get the next name from the scanner or complain if missing * Names start with letter then letters or digits up to anything else. * @param defaut -- return value if there is no next item * @param errorMesage -- the message to complain with * @return the next item or the default */ public String getNextName( String defalt, String errorMessage ) { sc.skip( delimPat ); // this will not throw uncheckedException because delimPat can be empty String name = sc.skip( namePat ).match().group(); if (name.isEmpty()) { Error.warn( errorMessage ); return defalt; } else { return name; } } /** get the next float from the scanner or complain if missing * only support 0.0 0. .0 . (no exponent field allowed) * @param defalt -- return value if there is no next item * @param errorMesage -- the message to complain with * @return the next item or the defalt */ public float getNextFloat( float defalt, String errorMessage ) { sc.skip( delimPat ); // this will not throw uncheckedException because delimPat can be empty String text = sc.skip( floatPat ).match().group(); if ( text.isEmpty() ) { Error.warn( errorMessage ); return defalt; } else { return Float.parseFloat( text ); // this will not throw uncheckedException because text is valid } } /** try to scan over the given literal and complain if missing * @param literal -- the string to scan over * @param errorMesage -- the message to complain with */ public void getNextLiteral( String literal, String errorMessage ) { if ( sc.hasNext( literal ) ) { sc.next( literal ); } else { Error.warn( errorMessage ); } } } /** Wrapper extending class Random, turning it into a singleton class * @see Random * Ideally, no user should ever create an instance of Random, all use this! * Users can call MyRandom.stream.anyMethodOfRandom() (or of MyRandom) * or MyRandom.stream().anyMethodOfRandom() * Users can allocate MyRandom myStream = MyRandom.stream; * or MyRandom myStream = MyRandom.stream(); * No matter how they do it, they get the same stream */ class MyRandom extends Random { public static final MyRandom stream = new MyRandom(); // the only stream; // nobody can construct a MyRandom except the above line of code private MyRandom() { super(); } /* alternative access to stream * Returns the only stream */ public static MyRandom stream() { return stream; } // add distributions that weren't built in /** exponential distribution * @param mean -- the mean value of the distribution * @return a positive exponentially distributed random value */ public double nextExponential( double mean ) { return mean * -Math.log( this.nextDouble() ); } } /** Roads connect intersections */ class Road { // instance variables private Intersection source; // road from here private Intersection destination; // road to there private double travelTime; // time in seconds // static information about all roads // BUG: only public so test code can iterate over it -- alternatives? public final static LinkedList all = new LinkedList<>(); /** Read a road description and construct it * @param in -- the Scanner from which the road description is read */ public Road( MyScanner in ) { String srcname; // name of source intersection String dstname; // name of destination intersection String msg = "road"; srcname = in.getNextName( "???", msg + ": source missing" ); msg = msg + " " + srcname; dstname = in.getNextName( "???", msg + ": destination missing" ); msg = msg + " " + dstname; travelTime = in.getNextFloat( 99999F, msg + ": travel time missing" ); msg = msg + " " + travelTime; in.getNextLiteral( ";", msg + ": semicolon missing"); // number of concatenations above was O(n) = 3(n-1) for n getNext calls // BUG: What about time units? if (travelTime <= 0.0F) { Error.warn( msg + ": negative travel time?" ); travelTime = 99999F; } // look up the source and destination intersections source = Intersection.lookup( srcname ); if (source == null) { Error.warn( msg + ": undefined source intersection?" ); } destination = Intersection.lookup( dstname ); if (destination == null) { Error.warn( msg + ": undefined destination intersection?" ); } all.add( this ); } /** Reconstruct a string approximation of the text used to create this road * WARNING: Never call this until after a call to Error.exitIfWarnings() */ public String toString() { return "road " + source.name + " " + destination.name + " " + travelTime + " ;"; } // BUG: need to add behavior for roads } /** Intersections are connected by roads */ class Intersection { // instance variables of each intersection public final String name; // the name of the intersection private double traversalTime; // time in seconds // BUG: is collection the right class? Should I pick a subclass? private Collection incoming; // the roads leading here private Collection outgoing; // the roads that come from here // static data about all the intersections // BUG: only public so test code can iterate over it -- alternatives? public final static LinkedList all = new LinkedList<>(); /** Read an intersection description and construct it * @param in -- the Scanner from which the intersection description is read */ public Intersection( MyScanner in ) { name = in.getNextName( "???", "intersection: name missing" ); traversalTime = in.getNextFloat( 99999F, "intersection " + name + ": traversal time missing" ); in.getNextLiteral( ";", "intersection " + name + " " + traversalTime + ": semicolon missing" ); // BUG: What about time units? if (lookup( name ) != null) { Error.warn( "intersection " + name + " " + traversalTime + ": name redefined?" ); // BUG: anything else to do in this case? } all.add( this ); } public String toString() { return "intersection " + name + " " + traversalTime + " ;"; } // BUG: need to add behavior for intersections /** look up an interseciton by name * @param name -- the textual name of the intersetion * @return the Intersection object or null if there is none */ public static Intersection lookup( String name ) { for (Intersection i: all) if (i.name.equals( name )) return i; return null; } } public class RoadNetwork { /** build a road network model by scanning an input file * @param in -- the scanner used to read the file * Input file contains keywords road and intersection, * details beyond that are handled by the appropriate classes */ private static void buildModel( MyScanner in ) { while (in.hasNext()) { String keyword = in.next(); if ("intersection".equals( keyword )) { new Intersection( in ); } else if ("road".equals( keyword )) { new Road( in ); } else { Error.warn( "not a keyword in the input: " + keyword ); } } Error.exitIfWarnings(); // if there were warnings, don't try to go on } /** Print out the road network. * This is intended to help with debugging buildmodel */ private static void printModel() { for (Intersection i: Intersection.all) { System.out.println( i.toString() ); } for (Road r: Road.all) { System.out.println( "" + r ); } } /** main method mostly concerned with command line arguments * @param args -- the command line arguments * Want just one argument, the file name for model description */ public static void main( String[] args ) { // arg[0] should be the name of a file describing a road network if (args.length < 1) { Error.fatal( "missing command line argument, a file name" ); } if (args.length > 1) { Error.warn( "extra command line argument: " + args[1] ); } try { buildModel( new MyScanner( new File( args[0] ) ) ); printModel(); // BUG: this is only for debugging // BUG: simulate the network that was built } catch ( FileNotFoundException e ) { Error.fatal( "could not open file: " + args[0] ); } } }