/* Copyright (C) 2002 J. M. Spivey */ import java.io.*; import java.util.*; import java.awt.Color; import java.net.*; /** A class representing static information about the map, * plus dynamic colours for each town and road. */ class Map { private Dictionary towns = new Hashtable(50); // The towns, each stored under its name private Vector roads = new Vector(100); // The (undirected) roads private Vector outlines = new Vector(10); // The coastlines /** Add a town */ public void addTown(String name, Coords location) { towns.put(name, new Town(name, location)); } /** Add a coast outline */ public void addOutline(Outline o) { outlines.addElement(o); } /** Add a road */ public void addRoad(String name1, String name2, float length) { /* Three objects are created: a Road that stores the length colour, and two Arc objects, one from each town to the other */ Town t1 = (Town) towns.get(name1); Town t2 = (Town) towns.get(name2); Road road = new Road(t1, t2, length); roads.addElement(road); t1.addArc(t2, road); t2.addArc(t1, road); } /** Create an enumeration that runs over the towns */ public Enumeration getTowns() { return towns.elements(); } /** Create an enumeration that runs over the roads */ public Enumeration getRoads() { return roads.elements(); } /** Count the outlines */ public int numOutlines() { return outlines.size(); } /** Create an enumeration that runs over the outlines */ public Enumeration getOutlines() { return outlines.elements(); } /** Reset all colours to black */ public void reset() { // Set roads back to black for (int i = 0; i < roads.size(); i++) { Road road = (Road) roads.elementAt(i); road.setColor(Color.black); } // Set towns to black too for (Enumeration e = towns.elements(); e.hasMoreElements();) { Town t = (Town) e.nextElement(); t.setColor(Color.black); } } // Here are some static methods for reading maps from files and URLs. /** Read map from a file */ public static Map fromFile(String fname) { try { return fromReader(new FileReader(fname)); } catch (IOException e) { return null; } } /** Read map from an InputStream */ public static Map fromStream(InputStream stream) { try { return fromReader(new InputStreamReader(stream)); } catch (IOException e) { return null; } } /** Read map from a URL */ public static Map fromURL(URL url) { try { InputStream s = url.openStream(); return fromReader(new InputStreamReader(s)); } catch (IOException e) { return null; } } /** Read a map file and build a map object */ private static Map fromReader(Reader rawin) throws IOException { Map map = new Map(); float outScale = 0.0f, outOffX = 0.0f, outOffY = 0.0f; Outline outline = new Outline(); // The file consists of a sequence of lines. // Each line is a sequence of fields separated by "/". // The first field of each line is a command that identifies // the format of the rest of the line. BufferedReader in = new BufferedReader(rawin); for (int lnum = 1;; lnum ++) { String line = in.readLine(); if (line == null) break; // Trim spaces. Ignore empty lines and comments starting with "#" String trimmed = line.trim(); if (trimmed.length() == 0 || trimmed.charAt(0) == '#') continue; // Use a StringTokenizer to split the line into fields StringTokenizer scan = new StringTokenizer(trimmed, "/", false); String fields[] = new String[scan.countTokens()]; for (int i = 0; i < fields.length; i++) fields[i] = scan.nextToken(); // Select action based on first field String cmd = fields[0]; if (cmd.equals("town") && fields.length == 4) { // Add a town (name, x-coord, y-coord) map.addTown(fields[1], new Coords(new Float(fields[2]).floatValue(), new Float(fields[3]).floatValue())); } else if (cmd.equals("road") && fields.length == 4) { // Add a road (town1, town2, length) map.addRoad(fields[1], fields[2], new Float(fields[3]).floatValue()); } else if (cmd.equals("mapscale") && fields.length == 4) { // Set scale and offset for subsequent outlines outScale = new Float(fields[1]).floatValue(); outOffX = new Float(fields[2]).floatValue(); outOffY = new Float(fields[3]).floatValue(); } else if (cmd.equals("point") && fields.length == 3) { // Add a point to the current outline if (outScale == 0.0) { System.out.println("Use 'mapscale' before outlines"); } else { float x = outScale * new Float(fields[1]).floatValue() + outOffX; float y = -outScale * new Float(fields[2]).floatValue() + outOffY; outline.addPoint(new Coords(x, y)); } } else if (cmd.equals("makeoutline") && fields.length == 1) { // Wrap up the current outline, and begin a new one map.addOutline(outline); outline = new Outline(); } else if (cmd.equals("end") && fields.length == 1) { // That's all, folks! break; } else { // We don't expect the map to contain errors. System.out.println("Unknown command '" + cmd + "' (" + fields.length + ") on line " + lnum); } } in.close(); return map; } /** A polygonal coastline. * * This is just a dressed-up vector of points. */ static class Outline { private Vector points = new Vector(10); /** Return the i'th point in the outline */ public Coords pointAt(int i) { return (Coords) points.elementAt(i); } /** Return the number of points */ public int length() { return points.size(); } /** Add a point */ public void addPoint(Coords p) { points.addElement(p); } } /** All permanent data about a town. * * This class represents towns with a name, a location, and a list * of outgoing arcs. In addition, the town has a colour that is * visible when it is displayed, and an object 'data' associated * with it. In Dijkstra's algorithm, the data includes the * status, estimated distance and backlink. */ static class Town { private String name; private Coords location; private Vector arcs = new Vector(5); private Color color = Color.black; private Object data; public Town(String name, Coords location) { this.name = name; this.location = location; } public void addArc(Town dest, Road road) { arcs.addElement(new Arc(dest, road)); } public String getName() { return name; } public Coords getLocation() { return location; } public Enumeration getArcs() { return arcs.elements(); } public Color getColor() { return color; } public void setColor(Color color) { this.color = color; } public float distance(Town other) { return this.location.distance(other.location); } public void setData(Object data) { this.data = data; } public Object getData() { return data; } } /** A symmetrical road between two towns. * * The road has a length, and a colour that is used when it is * displayed. Each road is associated with two directed Arcs that * are used in Dijkstra's algorithm. */ static class Road { public Town t1, t2; public float length; private Color color = Color.black; public Road(Town t1, Town t2, float length) { this.t1 = t1; this.t2 = t2; this.length = length; } public Color getColor() { return color; } public void setColor(Color color) { this.color = color; } } /** A directed link from one town to another. * * Each arc is associated with a road, but has a direction, so * that one of the road's two ends is identified as its * destination. */ static class Arc { private Town dest; private Road road; public Arc(Town dest, Road road) { this.dest = dest; this.road = road; } public Town getDest() { return dest; } public Road getRoad() { return road; } } /** Coordinates for a point on the map. * * The coordinates are floating-point values expressed in miles. * They must be transformed to integer coordinates expressed in * pixels before the point can be displayed. */ static class Coords { private float x, y; public Coords(float x, float y) { this.x = x; this.y = y; } public Coords() { this(0.0f, 0.0f); } public float getX() { return x; } public float getY() { return y; } public float distance(Coords other) { float dx = this.x - other.x, dy = this.y - other.y; return (float) Math.sqrt(dx*dx + dy*dy); } } }