package ca.cmput301f13t03.adventure_datetime.view.treeView; import android.graphics.Path; import android.graphics.Path.Direction; import android.util.Log; import java.util.*; public final class ConnectionPlacer { private static final int DEPTH_LIMIT = 5000; private static final String TAG = "ConnectionPlacer"; private int m_horizontalOffset = 0; private int m_verticalOffset = 0; private int m_mapWidth = 0; private int m_mapHeight = 0; private int m_actualHeight = 0; private int m_actualWidth = 0; private int m_gridSize = 0; private boolean[][] m_map = null; public ConnectionPlacer(ArrayList<GridSegment> gridSegments, int gridSize) { // expand the gridSegments into a large square region. Fill in // any holes as just empty regions int leftMost = Integer.MAX_VALUE; int rightMost = Integer.MIN_VALUE; int topMost = Integer.MAX_VALUE; int bottomMost = Integer.MIN_VALUE; m_gridSize = gridSize; for(GridSegment seg : gridSegments) { int top = seg.y; int bottom = seg.y + seg.height; int left = seg.x; int right = seg.x + seg.width; leftMost = Math.min(left, leftMost); rightMost = Math.max(right, rightMost); topMost = Math.min(top, topMost); bottomMost = Math.max(bottom, bottomMost); } m_actualWidth = rightMost - leftMost; m_actualHeight = bottomMost - topMost; m_mapWidth = m_actualWidth / gridSize; m_mapHeight = m_actualHeight / gridSize; m_horizontalOffset = leftMost; m_verticalOffset = topMost; m_map = new boolean[m_mapWidth][m_mapHeight]; FillMap(gridSegments); } private void FillMap(ArrayList<GridSegment> segments) { ClearMap(); for(GridSegment seg : segments) { ApplySegmentToMap(seg); } } private void ApplySegmentToMap(GridSegment seg) { int xStart = seg.x; int yStart = seg.y; int increment = m_gridSize; int xEnd = seg.x + seg.width; int yEnd = seg.y + seg.height; for(int x = xStart ; x < xEnd ; x += increment) { for(int y = yStart ; y < yEnd ; y += increment) { if(seg.IsEmpty(x, y)) { int localX = (x - m_horizontalOffset) / m_gridSize; int localY = (y - m_verticalOffset) / m_gridSize; m_map[localX][localY] = true; } } } } private void ApplyFragmentToMap(FragmentNode node, boolean valueToApply) { int xStart = node.x - m_horizontalOffset; int yStart = node.y - m_verticalOffset; int increment = m_gridSize; int xEnd = xStart + node.width; int yEnd = yStart + node.height; for(int x = xStart ; x < xEnd ; x += increment) { for(int y = yStart ; y < yEnd ; y += increment) { int localX = (x) / m_gridSize; int localY = (y) / m_gridSize; m_map[localX][localY] = valueToApply; } } } private void ClearMap() { for(int x = 0 ; x < m_mapWidth ; ++x) { for(int y = 0 ; y < m_mapHeight ; ++y) { m_map[x][y] = false; } } } public void PlaceConnection(FragmentConnection connection, FragmentNode originFrag, FragmentNode targetFrag) { if(originFrag.GetFragment().getFragmentID().equals(targetFrag.GetFragment().getFragmentID())) { Path newPath = new Path(); newPath.addCircle( originFrag.x + originFrag.width / 8, originFrag.y + originFrag.height / 4, originFrag.width / 5, Direction.CW); connection.SetPath(newPath); return; } // Transform the x and y cords to local map space Location start = new Location( originFrag.x - m_horizontalOffset + originFrag.width / 2, originFrag.y - m_verticalOffset + originFrag.height / 2); Location end = new Location( targetFrag.x - m_horizontalOffset + targetFrag.width / 2, targetFrag.y - m_verticalOffset + targetFrag.height / 2); // first remove the origin and target fragments from the collision map ApplyFragmentToMap(originFrag, false); ApplyFragmentToMap(targetFrag, false); // Path values Path finalPath = GetPath(start, end, new FullPathBuilder(start, end, m_gridSize, m_map)); // now restore the collision map ApplyFragmentToMap(originFrag, true); ApplyFragmentToMap(targetFrag, true); // transform them to global space and assign it to the connection finalPath.offset(m_horizontalOffset, m_verticalOffset); connection.SetPath(finalPath); } // helper function, why does Java not have this??? private int Clamp(int val, int min, int max) { return Math.min(Math.max(val, min), max); } private Path GetPath(Location start, Location target, FullPathBuilder builder) { SortedLocationList openList = new SortedLocationList(m_map, m_gridSize); Set<Location> closedList = new HashSet<Location>(); int currentDepth = 0; int approxDistanceFromStart = 0; LocationNode startPoint = new LocationNode(start, target, currentDepth, null); openList.add(startPoint); while(openList.size() > 0 && currentDepth < DEPTH_LIMIT) { List<LocationNode> nextBatch = openList.GetAndRemoveNextLowestWeights(); for(LocationNode currentLocation : nextBatch) { // only check it if we haven't checked it already if(!closedList.contains(currentLocation.location)) { if(builder.TestForDestination(currentLocation.location)) { return builder.BuildPath(currentLocation); } else { // no luck, this node is empty. // add it to the closedList closedList.add(currentLocation.location); int baseX = currentLocation.location.x; int baseY = currentLocation.location.y; // get adjacent locations int yUp = baseY - m_gridSize; int yDown = baseY + m_gridSize; int xRight = baseX + m_gridSize; int xLeft = baseX - m_gridSize; // clamp all values yUp = Clamp(yUp, 0, m_actualHeight - m_gridSize); yDown = Clamp(yDown, 0, m_actualHeight - m_gridSize); xRight = Clamp(xRight, 0, m_actualWidth - m_gridSize); xLeft = Clamp(xLeft, 0, m_actualWidth - m_gridSize); // construct adjacent nodes LocationNode up = new LocationNode(new Location(baseX, yUp), target, approxDistanceFromStart, currentLocation); LocationNode down = new LocationNode(new Location(baseX, yDown), target, approxDistanceFromStart, currentLocation); LocationNode left = new LocationNode(new Location(xLeft, baseY), target, approxDistanceFromStart, currentLocation); LocationNode right = new LocationNode(new Location(xRight, baseY), target, approxDistanceFromStart, currentLocation); // add all adjacent nodes to this list AddToSortedList(up, openList, closedList); AddToSortedList(down, openList, closedList); AddToSortedList(left, openList, closedList); AddToSortedList(right, openList, closedList); // now lets keep searching! } } } currentDepth++; approxDistanceFromStart = currentDepth * m_gridSize; } if(currentDepth >= DEPTH_LIMIT) { Log.w(TAG, "Failed to find a path before hitting the depth limit!"); } // if we've made it this far then no path could be found as one does not exist // so just return a default path return builder.BuildDefaultPath(); } private void AddToSortedList(LocationNode node, SortedLocationList openList, Set<Location> closedList) { if(!closedList.contains(node.location)) { if(!openList.add(node)) { closedList.add(node.location); } } } private final class Location implements Comparable<Location> { public Location(int x, int y) { this.x = x; this.y = y; } public int x = 0; public int y = 0; public int DistanceSquared(Location other) { return (this.x - other.x)*(this.x - other.x) + (this.y - other.y)*(this.y - other.y); } public int compareTo(Location other) { // bleh, I don't like this if(this.x > other.x) { return 1; } else if(this.x < other.x) { return -1; } else { if(this.y > other.y) { return 1; } else if(this.y < other.y) { return -1; } else { // they are equal! return 0; } } } public boolean equals(Object other) { if(other instanceof Location) { Location otherLocation = (Location)(other); return otherLocation.x == this.x && otherLocation.y == this.y; } else { return false; } } public int hashCode() { return this.x ^ this.y; } } // no I am not using tuple for this. // Item1 Item2 is a terrible interface private final class LocationNode { public LocationNode(Location src, Location target, int expansionLevel, LocationNode previousNode) { location = src; weight = (int) Math.round(Math.sqrt(src.DistanceSquared(target))); weight += expansionLevel; this.prev = previousNode; } public Location location; public int weight; public LocationNode prev; // used in path creation public float dx; public float dy; } private final class SortedLocationList extends LinkedList<LocationNode> { private static final long serialVersionUID = -1181516011560588762L; private boolean[][] m_map = null; private int m_gridSize = 0; public SortedLocationList(boolean[][] map, int gridSize) { this.m_gridSize = gridSize; this.m_map = map; } public boolean add(LocationNode object) { // only add it if it doesn't collide with anything int x = object.location.x / this.m_gridSize; int y = object.location.y / this.m_gridSize; if(m_map[x][y]) // if there is something at this position { return false; } Iterator<LocationNode> itr = this.listIterator(); int insertLocation = 0; while(itr.hasNext()) { if(itr.next().weight > object.weight) { // found it! break; } ++insertLocation; } super.add(insertLocation, object); return true; } public List<LocationNode> GetAndRemoveNextLowestWeights() { List<LocationNode> lowest = new ArrayList<LocationNode>(); LocationNode absoluteLowest = this.getFirst(); Iterator<LocationNode> itr = this.listIterator(); while(itr.hasNext()) { LocationNode current = itr.next(); // sorted list so no need to use less than if(current.weight == absoluteLowest.weight) { lowest.add(current); } else { // then we have all we need //break; } } // now remove those we have collected this.removeAll(lowest); return lowest; } } private final class FullPathBuilder { Location m_target = null; Location m_start = null; int m_variance = 0; boolean[][] m_map = null; public FullPathBuilder(Location start, Location target, int gridWidth, boolean[][] map) { m_start = start; m_target = target; m_variance = (int) (gridWidth * gridWidth * (2.30f)); m_map = map; } public boolean TestForDestination(Location loc) { return loc.DistanceSquared(m_target) <= m_variance; } public Path BuildPath(LocationNode endNode) { List<LocationNode> pathNodes = new ArrayList<LocationNode>(); // Backtrack through the nodes until we are back at the beginning (ie prev == null) LocationNode current = endNode; while(current.prev != null) { pathNodes.add(current); current = current.prev; } return ConstructFromNodes(pathNodes); } private Path ConstructFromNodes(List<LocationNode> nodes) { Path result = new Path(); // first simplify the node list nodes = SimplifyPath(nodes); CenterNodes(nodes); if(nodes.size() > 1) { for(int i = nodes.size() - 2; i < nodes.size(); i++) { if(i >= 0) { LocationNode point = nodes.get(i); if(i == 0) { LocationNode next = nodes.get(i + 1); point.dx = ((next.location.x - point.location.x) / 3); point.dy = ((next.location.y - point.location.y) / 3); } else if(i == nodes.size() - 1) { LocationNode prev = nodes.get(i - 1); point.dx = ((point.location.x - prev.location.x) / 3); point.dy = ((point.location.y - prev.location.y) / 3); } else { LocationNode next = nodes.get(i + 1); LocationNode prev = nodes.get(i - 1); point.dx = ((next.location.x - prev.location.x) / 3); point.dy = ((next.location.y - prev.location.y) / 3); } } } } if(nodes.size() > 0) { LocationNode first = nodes.get(0); result.moveTo(first.location.x, first.location.y); } for(int i = 1; i < nodes.size(); i++) { LocationNode point = nodes.get(i); LocationNode prev = null; prev = nodes.get(i - 1); result.cubicTo( prev.location.x + prev.dx, prev.location.y + prev.dy, point.location.x - point.dx, point.location.y - point.dy, point.location.x, point.location.y); } return result; } private void CenterNodes(List<LocationNode> nodes) { for(LocationNode node : nodes) { node.location.x += m_gridSize / 2; node.location.y += m_gridSize / 2; } } public Path BuildDefaultPath() { Path result = new Path(); result.moveTo(m_start.x, m_start.y); result.lineTo(m_target.x, m_target.y); return result; } private List<LocationNode> SimplifyPath(List<LocationNode> baseNodes) { List<LocationNode> simplifiedList = new ArrayList<LocationNode>(); if(baseNodes.size() > 1) { LocationNode currentBase = baseNodes.get(1); LocationNode prev = baseNodes.get(0); Iterator<LocationNode> itr = baseNodes.listIterator(2); simplifiedList.add(prev); prev = currentBase; while(itr.hasNext()) { LocationNode nextNode = itr.next(); if(IsStraightLineReachable(currentBase.location, nextNode.location)) { // then we'll just ignore it and continue with the next node prev = nextNode; continue; } else { // the current node cannnot be reached from the base, therefore // we need to rebase from prev and continue simplifiedList.add(prev); currentBase = nextNode; } } // make sure the last node is in the list simplifiedList.add(baseNodes.get(baseNodes.size() - 1)); } else { // well crap, only 1 or zero nodes, just give 'em that back. // doesn't get much simpler than a path with only a point A or // no point at all! simplifiedList = baseNodes; } return simplifiedList; } /** * Test if a straight line can be drawn from node 1 to node 2 without anything * getting in the way. Returns true if this is possible and false if it is not. */ private boolean IsStraightLineReachable(Location p1, Location p2) { final float VAR = 0.01f; boolean result = true; // assuming the Titanic will not sink until we hit the iceberg // (or Buckingham palace, as the case may be) float rise = (float)(p2.y) - (float)(p1.y); float run = (float)(p2.x) - (float)(p1.x); float angle = (float) Math.atan(rise / run); float h_step = (float) ((float)(m_gridSize) * Math.cos(angle)); float v_step = (float) ((float)(m_gridSize) * Math.sin(angle)); // technically speaking rise/v_step and run/hstep should be equal // but I figured it current hurt to average them in case the floating // point values are off by a hair, also easier to debug this way! float steps = 0.0f; if(Math.abs(v_step) <= VAR) { steps = (float)(run / h_step); } else if(Math.abs(h_step) <= VAR) { steps = (float)(rise / v_step); } else { steps = (float) Math.ceil(((rise / v_step) + (run / h_step)) / 2.0f); } for(float step = 0 ; step < steps ; ++step) { int testPointX = (int) (((step * h_step) + p1.x) / m_gridSize); int testPointY = (int) (((step * v_step) + p1.y) / m_gridSize); if(this.m_map[testPointX][testPointY]) { // then we hit something! result = false; break; } } return result; } } }