package com.opendoorlogistics.components.heatmap; import gnu.trove.map.hash.TIntObjectHashMap; import gnu.trove.set.hash.TIntHashSet; import java.awt.Point; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import com.opendoorlogistics.components.heatmap.HeatmapGenerator.TraceCoord; class TraceGraph { private static class Edge { Node node1; Node node2; ArrayList<TraceRecord> traceRecords = new ArrayList<TraceGraph.TraceRecord>(2); Node other(Node node) { if (node1 == node) { return node2; } else if (node2 == node) { return node1; } throw new IllegalArgumentException(); } Orientation orientation(){ if(node1.pnt.y == node2.pnt.y){ return Orientation.HORIZONTAL; } if(node1.pnt.x == node2.pnt.x){ return Orientation.VERTICAL; } return Orientation.UNDEFINED; } } private static enum Orientation{ HORIZONTAL, VERTICAL, UNDEFINED, } private static class Node { Point pnt; ArrayList<Edge> edges = new ArrayList<TraceGraph.Edge>(2); boolean isMarkedForRemoval; Edge getEdgeTo(Node node) { for (Edge edge : edges) { if (edge.node1 == node || edge.node2 == node) { return edge; } } return null; } HashSet<Node> neighbours() { HashSet<Node> ret = new HashSet<TraceGraph.Node>(); for (Edge edge : edges) { ret.add(edge.other(this)); } return ret; } } private static class TraceRecord { int traceRingNb; Point tracedFrom; } private HashMap<Point, Node> nodes = new HashMap<Point, TraceGraph.Node>(); private TIntObjectHashMap<TraceRingHeader> traceHeaders = new TIntObjectHashMap<TraceRingHeader>(); private static class TraceRingHeader{ int id; int level; Point firstTraceCell; ArrayList<Node> nodes =new ArrayList<TraceGraph.Node>(); } /** * Create the first edge in a trace * @param coord * @param level * @param traceRingNb * @param traceEdgeNb * @return The trace number (id for each trace) */ int createTraceFirstEdge(TraceCoord coord, int level) { TraceRingHeader header = new TraceRingHeader(); header.level = level; header.id = traceHeaders.size(); header.firstTraceCell = new Point(coord.cell); traceHeaders.put(header.id, header); createEdge(coord, header,true); return header.id; } /** * Create a later edge in a trace * @param coord * @param level * @param traceRingNb * @param traceEdgeNb * @return */ void createTraceLaterEdge(TraceCoord coord, int level, int traceRingNb) { createEdge(coord, traceHeaders.get(traceRingNb),false); } private Edge createEdge(TraceCoord coord, TraceRingHeader header,boolean isFirst) { Node node1 = createNode(coord.edgeStart); Node node2 = createNode(coord.edgeEnd); Edge edge = node1.getEdgeTo(node2); if (edge == null) { edge = new Edge(); edge.node1 = node1; edge.node2 = node2; node1.edges.add(edge); node2.edges.add(edge); } TraceRecord record = new TraceRecord(); record.traceRingNb = header.id; record.tracedFrom = new Point(coord.cell); edge.traceRecords.add(record); if(isFirst){ header.nodes.add(node1); } header.nodes.add(node2); return edge; } boolean isEdgeTracedFromCellAlready(TraceCoord coord) { Node node1 = nodes.get(coord.edgeStart); Node node2 = nodes.get(coord.edgeEnd); if (node1 != null && node2 != null) { Edge edge = node1.getEdgeTo(node2); if (edge != null) { for (TraceRecord record : edge.traceRecords) { if (record.tracedFrom.equals(coord.cell)) { return true; } } } } return false; } private Node createNode(Point pnt) { pnt = new Point(pnt); Node ret = nodes.get(pnt); if (ret == null) { ret = new Node(); ret.pnt = pnt; nodes.put(pnt, ret); } return ret; } void calculateDiagonals() { for (Node node :nodes.values()) { class IsOk { boolean ok = true; } IsOk isOk = new IsOk(); // Fill out along the trace from the node checking (a) node has 2 edges only, // (b) node or no neighbouring node is diagonalised already and (c) all neighbouring // nodes have 2 edges only. floodFill(node, new FloodFillNodeCallback() { @Override public FloodFillOption floodFillCallback(Node otherNode, int distance) { if (distance <= 1) { if (otherNode.edges.size() != 2 || otherNode.isMarkedForRemoval) { isOk.ok = false; } return isOk.ok ? FloodFillOption.ContinueFill : FloodFillOption.QuitFill; } return FloodFillOption.QuitFill; } }); // Check for horizontal then vertical (or vice versa) if(isOk.ok){ // Already know it has 2 edges from earlier test Orientation o1 = node.edges.get(0).orientation(); Orientation o2 = node.edges.get(1).orientation(); isOk.ok = (o1==Orientation.HORIZONTAL && o2==Orientation.VERTICAL) ||(o1==Orientation.VERTICAL && o2==Orientation.HORIZONTAL); } // Check no loop smaller than X returns to the node. if(isOk.ok){ int minLoop = 4; HashSet<Node> neighbours = new HashSet<TraceGraph.Node>(); for (Edge edge : node.edges) { Node ngb =edge.other(node); floodFill(ngb, new FloodFillNodeCallback() { @Override public FloodFillOption floodFillCallback(Node otherNode, int distance) { if(otherNode == node){ // don't allow filling through the central node return FloodFillOption.SkipElement; } // if we're not at the starting neighbour but we are at another neighbour then we've // looped, don't allow loops smaller than minimum if(otherNode!=ngb && neighbours.contains(otherNode) && distance< minLoop){ isOk.ok = false; } return isOk.ok && distance <= minLoop? FloodFillOption.ContinueFill: FloodFillOption.QuitFill; } }); } } // Check we don't have another trace within X cells if(isOk.ok){ TIntHashSet traces = new TIntHashSet(); int myTraceRingNb = node.edges.get(0).traceRecords.get(0).traceRingNb; traces.add(myTraceRingNb); Point searchPoint = new Point(); int bf=1; for(searchPoint.x = node.pnt.x - bf ; searchPoint.x <= node.pnt.x + bf && isOk.ok; searchPoint.x++){ for(searchPoint.y = node.pnt.y - bf ; searchPoint.y <= node.pnt.y + bf && isOk.ok; searchPoint.y++){ Node other = nodes.get(searchPoint); if(other!=null){ for(Edge edge : other.edges){ for(TraceRecord record : edge.traceRecords){ traces.add(record.traceRingNb); } } } isOk.ok = traces.size()<=2; } } } if(isOk.ok){ node.isMarkedForRemoval = true; } } } List<Point> getPoints(int ringNb, boolean createDiagonals){ TraceRingHeader header=traceHeaders.get(ringNb); ArrayList<Point> points = new ArrayList<Point>(header.nodes.size()); for(Node node : header.nodes){ if(!createDiagonals || !node.isMarkedForRemoval){ points.add(node.pnt); } } return points; } private enum FloodFillOption { ContinueFill, QuitFill, SkipElement } private interface FloodFillNodeCallback { FloodFillOption floodFillCallback(Node node, int distance); } // interface FloodFillEdgeCallback { // FloodFillOption floodFillEdgeCallback(Edge edge, int distance); // } /** * Flood fill from this node along the trace, getting a callback with distance (number of * connections), starting with the input node * * @param startNode * @param cb */ private void floodFill(Node startNode, FloodFillNodeCallback cb) { HashSet<Node> open = new HashSet<TraceGraph.Node>(); HashSet<Node> closed = new HashSet<TraceGraph.Node>(); open.add(startNode); int distance = 0; while (open.size() > 0) { HashSet<Node> newOpen = new HashSet<TraceGraph.Node>(); for (Node node : open) { if (!closed.contains(node)) { if (cb != null) { switch (cb.floodFillCallback(node, distance)) { case QuitFill: return; case SkipElement: continue; default: break; } } closed.add(node); for (Edge edge : node.edges) { // if (ecb != null) { // switch (ecb.floodFillEdgeCallback(edge, distance)) { // case QuitFill: // return; // // case SkipElement: // continue; // // default: // break; // } // // } Node ngb = edge.other(node); if (!open.contains(ngb) && !closed.contains(ngb)) { newOpen.add(ngb); } } } } open.clear(); open.addAll(newOpen); distance++; } } int getLevel(int traceNb){ return traceHeaders.get(traceNb).level; } Point getFirstCell(int traceNb){ return traceHeaders.get(traceNb).firstTraceCell; } }