package edu.kit.pse.ws2013.routekit.map; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel.MapMode; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.swing.JOptionPane; import edu.kit.pse.ws2013.routekit.models.ProgressReporter; import edu.kit.pse.ws2013.routekit.util.Coordinates; /** * A street map graph. * * <p> * Note that this graph data structure is profile-independent and, like * {@link EdgeBasedGraph}, is created when a new map is imported. * * @see StreetMap */ public class Graph { int[] nodes; int[] edges; int[] edgesReverse; Map<Integer, NodeProperties> nodeProps; EdgeProperties[] edgeProps; float[] lat; float[] lon; ArrayGraphIndex[] indices; int[] correspondingEdges; /** * Creates a new {@code Graph} from the given adjacency field. * * @param nodes * the node array of the adjacency field * @param edges * the edge array of the adjacency field * @param nodeProps * the properties of the nodes (A {@code Map} of * {@code NodeProperties} is used here as most nodes do not have * any special properties and an array would be empty for the * most part.) * @param edgeProps * the properties of the edges * @param lat * the latitudes of the nodes * @param lon * the longitudes of the nodes * @throws IllegalArgumentException * if {@code edgeProps} does not have as many elements as * {@code edges} */ public Graph(int[] nodes, int[] edges, Map<Integer, NodeProperties> nodeProps, EdgeProperties[] edgeProps, float[] lat, float[] lon) { this(nodes, edges, nodeProps, edgeProps, lat, lon, null); } public Graph(int[] nodes, int[] edges, Map<Integer, NodeProperties> nodeProps, EdgeProperties[] edgeProps, float[] lat, float[] lon, ArrayGraphIndex[] indices) { if (edgeProps.length != edges.length) { throw new IllegalArgumentException( "Must have as many EdgeProperties as Edges!"); } this.nodes = nodes; this.edges = edges; this.nodeProps = nodeProps; this.edgeProps = edgeProps; this.lat = lat; this.lon = lon; this.edgesReverse = new int[edges.length]; int currentNode = 0; for (int i = 0; i < edges.length; i++) { while (currentNode + 1 < nodes.length && nodes[currentNode + 1] <= i) { currentNode++; } edgesReverse[i] = currentNode; } correspondingEdges = new int[edges.length]; for (int i = 0; i < edges.length; i++) { int target = edges[i]; int limit = (target == nodes.length - 1 ? edges.length : nodes[target + 1]); correspondingEdges[i] = -1; for (int j = nodes[target]; j < limit; j++) { if (getTargetNode(j) == getStartNode(i)) { correspondingEdges[i] = j; break; } } } if (indices == null || indices.length != 4) { initIndices(); } else { this.indices = indices; } } private void initIndices() { Thread[] threads = new Thread[4]; indices = new ArrayGraphIndex[4]; threads[0] = new Thread("L3 Graph-Index") { { setDaemon(true); } @Override public void run() { indices[0] = new GraphIndexConverter(new TreeGraphIndex( Graph.this, HighwayType.Residential, new ReducedGraphView(Graph.this, HighwayType.Primary, 10))).getIndex(); } }; threads[1] = new Thread("L2 Graph-Index") { { setDaemon(true); } @Override public void run() { indices[1] = new GraphIndexConverter(new TreeGraphIndex( Graph.this, HighwayType.Residential, new ReducedGraphView(Graph.this, HighwayType.Secondary, 12))).getIndex(); } }; threads[2] = new Thread("L1 Graph-Index") { { setDaemon(true); } @Override public void run() { indices[2] = new GraphIndexConverter(new TreeGraphIndex( Graph.this, HighwayType.Residential, new ReducedGraphView(Graph.this, HighwayType.Tertiary, 14))).getIndex(); } }; threads[3] = new Thread("L0 Graph-Index") { { setDaemon(true); } @Override public void run() { indices[3] = new GraphIndexConverter(new TreeGraphIndex( Graph.this, HighwayType.Residential, new IdentityGraphView(Graph.this))).getIndex(); } }; for (Thread t : threads) { t.start(); } for (Thread t : threads) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } // check if we got all the indices for (int i = 0; i < indices.length; i++) { if (indices[i] == null) { // this thread crashed (e. g. OOM), let’s try again // this time, synchronously – only one index at a time try { threads[i].run(); } catch (OutOfMemoryError e) { JOptionPane.showMessageDialog(null, "Nicht genug Arbeitsspeicher!"); System.exit(1); } } } } /** * Returns the {@code NodeProperties} of the given node. * * @param node * a node * @return the properties of the node, or {@code null} if none */ public NodeProperties getNodeProperties(int node) { return nodeProps.get(node); } /** * Returns the coordinates of the given node. * * @param node * a node * @return the {@link Coordinates} of the node */ public Coordinates getCoordinates(int node) { return new Coordinates(lat[node], lon[node]); } /** * Returns the start node of the given edge. * * @param edge * an edge * @return the start node of the edge */ public int getStartNode(int edge) { return edgesReverse[edge]; } /** * Returns a set of all outgoing edges of the given node. * * @param node * a node * @return all edges going out from this node */ public Set<Integer> getOutgoingEdges(int node) { return new IntIntervalSet(nodes[node], (node == nodes.length - 1 ? edges.length : nodes[node + 1]) - nodes[node]); } /** * Returns a geometric data structure for fast edge search in the given zoom * level. * * @param zoom * the zoom level * @return the {@link GraphIndex} data structure */ public GraphIndex getIndex(int zoom) { if (zoom > 14) { return indices[3]; } if (zoom > 12) { return indices[2]; } if (zoom > 10) { return indices[1]; } return indices[0]; } /** * Returns a set of all incoming edges of the given node. * * @param node * a node * @return all edges coming in at that node */ public Set<Integer> getIncomingEdges(int node) { throw new Error("Unimplemented"); } /** * Returns the edge in the opposite direction, or -1 if it doesn't exist. * * @param edge * to search for * @return the corresponding edge */ public int getCorrespondingEdge(int edge) { return correspondingEdges[edge]; } /** * Returns the {@link EdgeProperties} of the given edge. * * @param edge * an edge * @return the properties of the edge */ public EdgeProperties getEdgeProperties(int edge) { return edgeProps[edge]; } /** * Returns the end node of the given edge. * * @param edge * an edge * @return the end node of the edge */ public int getTargetNode(int edge) { return edges[edge]; } /** * Returns the number of edges in this graph. * * @return the number of edges */ public int getNumberOfEdges() { return edges.length; } /** * Returns the number of nodes in this graph. * * @return the number of nodes */ public int getNumberOfNodes() { return nodes.length; } @Override public boolean equals(Object obj) { if (obj instanceof Graph) { Graph other = (Graph) obj; return Arrays.equals(nodes, other.nodes) && Arrays.equals(edges, other.edges) && Arrays.equals(lat, other.lat) && Arrays.equals(lon, other.lon) && nodeProps.equals(other.nodeProps) && Arrays.equals(edgeProps, other.edgeProps); } return false; } @Override public int hashCode() { return nodes[0] ^ edges[0]; } private void saveIndexes(File dir) throws IOException { for (int i = 0; i < indices.length; i++) { final ArrayGraphIndex index = indices[i]; final File file = new File(dir, "L" + (3 - i) + ".index"); final File viewFile = new File(dir, file.getName().replace("index", "view")); index.save(file, viewFile); } } private static ArrayGraphIndex[] loadIndexes(Graph graph, File dir) { try { ArrayGraphIndex[] indices = new ArrayGraphIndex[4]; for (int i = 0; i < indices.length; i++) { final File file = new File(dir, "L" + (3 - i) + ".index"); final File viewFile = new File(dir, file.getName().replace( "index", "view")); indices[i] = ArrayGraphIndex.load(graph, file, viewFile); } return indices; } catch (IOException e) { return null; } } /** * Save this graph to the specified file. * <p> * The only thing you need to know about the format of the file is that * {@link #load(File)} can parse it; the rest of this documentation is * purely informational, as the format may change in the future (but we’ll * try and maintain backwards compatibility). * <h4>The file format</h4> * <h5>Header</h5> * <ul> * <li>Magic: bytes {@code 0x00}, {@code 0x02}, ASCII {@code routeKIT}</li> * <li>ASCII {@code g} (for "graph")</li> * <li>byte {@code 0x01} (for version 1, but future versions won’t * necessarily use the same header format)</li> * <li>ASCII {@code End of Transmission}</li> * </ul> * <h5>Nodes, Edges</h5> * <ul> * <li>int number of nodes, int number of edges (both four bytes, high byte * first)</li> * <li>ints nodes (all four bytes, high byte first)</li> * <li>ints edges (all four bytes, high byte first)</li> * <li>floats latitudes (all four bytes, IEEE 754 single)</li> * <li>floats longitudes (all four bytes, IEEE 754 single)</li> * </ul> * <h5>NodeProperties, EdgeProperties</h5> * <ul> * <li>For each {@link NodeProperties}, int node (four bytes, high byte * first), then the {@link NodeProperties} (see * {@link NodeProperties#save(java.io.DataOutput)})</li> * <li>int {@code -1} (four bytes, high byte first)</li> * <li>For each {@link EdgeProperties}, int number of edges with these * properties (four bytes, high byte first), then the {@link EdgeProperties} * (see {@link EdgeProperties#save(java.io.DataOutput)}), then ints edges * (all four bytes, high byte first)</li> * <li>int {@code 0} (four bytes, high byte first)</li> * </ul> * * @param file * The file. * @throws IOException * If an I/O error occurs. */ public void save(File file) throws IOException { try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { raf.writeUTF("routeKIT"); raf.writeByte(0x67); // 'g', for "graph" raf.writeByte(1); // version 1 raf.writeByte(0x04); // End of Transmission int nNodes = nodes.length; int nEdges = edges.length; raf.writeInt(nNodes); raf.writeInt(nEdges); final int headerLength = 2 + "routeKIT".length() + 3 + 8; assert (raf.getFilePointer() == headerLength); final int intsLength = nNodes * 4 + nEdges * 4; final int floatsLength = nNodes * 4 + nNodes * 4; final int dataLength = intsLength + floatsLength; MappedByteBuffer b = raf.getChannel().map(MapMode.READ_WRITE, headerLength, dataLength); IntBuffer ints = b.asIntBuffer(); ints.put(nodes, 0, nNodes); ints.put(edges, 0, nEdges); b.position(intsLength); FloatBuffer floats = b.asFloatBuffer(); floats.put(lat, 0, nNodes); floats.put(lon, 0, nNodes); raf.seek(headerLength + dataLength); for (Entry<Integer, NodeProperties> entry : nodeProps.entrySet()) { raf.writeInt(entry.getKey()); entry.getValue().save(raf); } raf.writeInt(-1); Map<EdgeProperties, Set<Integer>> reverseEdgeProperties = new HashMap<>(); for (int i = 0; i < nEdges; i++) { EdgeProperties props = edgeProps[i]; Set<Integer> current = reverseEdgeProperties.get(props); if (current == null) { current = new HashSet<>(); } current.add(i); reverseEdgeProperties.put(props, current); } for (Entry<EdgeProperties, Set<Integer>> entry : reverseEdgeProperties .entrySet()) { raf.writeInt(entry.getValue().size()); entry.getKey().save(raf); for (int i : entry.getValue()) { raf.writeInt(i); } } raf.writeInt(0); try { saveIndexes(file.getAbsoluteFile().getParentFile()); } catch (IOException e) { // ignore } } } /** * Loads a graph from the specified file. * <p> * This method can parse files generated by the current version of * {@link #save(File)}, and it will also attempt to parse files generated by * earlier versions of that method. For the file format, see there. * * @param file * The file. * @param reporter * @return A {@link Graph} loaded from {@code file}. * @throws IOException * If an I/O error occurs. */ public static Graph load(File file, ProgressReporter reporter) throws IOException { reporter.setSubTasks(new float[] { 0.7f, 0.3f }); try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { reporter.pushTask("Lese Datei"); if (!raf.readUTF().equals("routeKIT")) { throw new IOException("Wrong magic!"); } if (raf.readByte() != 0x67) { // 'g', for "graph" throw new IOException("Wrong file type!"); } if (raf.readByte() != 1) { // version 1 throw new IOException("Unsupported version!"); } if (raf.readByte() != 0x04) { // End of Transmission throw new IOException("Wrong magic!"); } int nNodes = raf.readInt(); int nEdges = raf.readInt(); final int headerLength = 2 + "routeKIT".length() + 3 + 8; assert (raf.getFilePointer() == headerLength); final int dataLength = nNodes * 4 + nEdges * 4 + nNodes * 4 + nNodes * 4; int[] nodes = new int[nNodes]; int[] edges = new int[nEdges]; float[] lats = new float[nNodes]; float[] lons = new float[nNodes]; MappedByteBuffer b = raf.getChannel().map(MapMode.READ_ONLY, 2 + "routeKIT".length() + 3 + 8, nNodes * 4 + nEdges * 4 + nNodes * 4 + nNodes * 4); IntBuffer ints = b.asIntBuffer(); ints.get(nodes, 0, nNodes); ints.get(edges, 0, nEdges); b.position(b.position() + 4 * nNodes + 4 * nEdges); FloatBuffer floats = b.asFloatBuffer(); floats.get(lats, 0, nNodes); floats.get(lons, 0, nNodes); raf.seek(headerLength + dataLength); Map<Integer, NodeProperties> nodeProps = new HashMap<>(); int node; while ((node = raf.readInt()) != -1) { NodeProperties props = NodeProperties.load(raf); nodeProps.put(node, props); } EdgeProperties[] edgeProps = new EdgeProperties[nEdges]; int length; int totalRead = 0; while ((length = raf.readInt()) != 0) { EdgeProperties props = EdgeProperties.load(raf); totalRead += length; reporter.setProgress(((float) totalRead) / nEdges); while (length-- > 0) { edgeProps[raf.readInt()] = props; } } reporter.popTask("Lese Datei"); reporter.pushTask("Baue Graphstruktur"); // loading the indices requires a Graph object, but the Graph takes // the indices as constructor arguments, so we get a little tricksy ArrayGraphIndex[] indices = new ArrayGraphIndex[4]; Graph graph = new Graph(nodes, edges, nodeProps, edgeProps, lats, lons, indices); ArrayGraphIndex[] loadedIndices = loadIndexes(graph, file.getParentFile()); if (loadedIndices == null) { // no indices saved, build the indices from scratch graph.initIndices(); // then save them for next time graph.saveIndexes(file.getParentFile()); } else { // inject indices into graph System.arraycopy(loadedIndices, 0, indices, 0, loadedIndices.length); } reporter.popTask(); return graph; } } }