The GanttRouter is used by the GanttChart to route the dependency lines from the start to the end point. The chart is a rectangular area with a certain width and height. The graphical elements of the Gantt chart can be registered as don’t-cross-zones. These zones block the either horizontal or vertical lines (or both) from crossing the zone. Zones can be registered by calling addZone(). The route() method returns routed path from start to end point.
Minimum distance between the starting point and the first turning point.
Minimum distance between the last turning point and the tip of the arrow.
Create a GanttRouter object. width and height describe the size of the rectangular area this router is operating on.
# File lib/taskjuggler/reports/GanttRouter.rb, line 35 35: def initialize(width, height) 36: @width = width.to_i 37: @height = height.to_i 38: 39: @detector = CollisionDetector.new(@width, @height) 40: end
# File lib/taskjuggler/reports/GanttRouter.rb, line 42 42: def addZone(x, y, w, h, horiz, vert) 43: @detector.addBlockedZone(x, y, w, h, horiz, vert) 44: end
Find a non-blocked route from the startPoint [ x, y ] to the endPoint [ x, y ]. The route always starts from the start point towards the right side of the chart and reaches the end point from the left side of the chart. All lines are always strictly horizontal or vertical. There are no diagonal lines. The result is an Array of [ x, y ] points that include the startPoint as first and endPoint as last element.
# File lib/taskjuggler/reports/GanttRouter.rb, line 95 95: def route(startX, startY, endX, endY) 96: points = [ [ startX, startY ] ] 97: startGap = MinStartGap 98: endGap = MinEndGap 99: 100: if endX - startX > startGap + endGap + 2 101: # If the horizontal distance between start and end point is large enough 102: # we can try a direct route. 103: # 104: # xSeg 105: # |startGap| 106: # startX/endX X--------1 107: # | 108: # | 109: # 2------X endX/endY 110: # |endGap| 111: # 112: xSeg = placeLine([ startY + (startY < endY ? 1 : 1), endY ], 113: false, startX + startGap, 1) 114: if xSeg && xSeg < endX - endGap 115: # The simple version works. Add the lines. 116: addLineTo(points, xSeg, startY) # Point 1 117: addLineTo(points, xSeg, endY) # Point 2 118: addLineTo(points, endX, endY) 119: return points 120: end 121: end 122: 123: # If the simple approach above fails, the try a more complex routing 124: # strategy. 125: # 126: # x1 127: # |startGap| 128: # startX/startY X--------1 yLS 129: # | 130: # 3---------------2 ySeg 131: # | 132: # 4------X endX/endY 133: # |endGap| 134: # x2 135: 136: # Place horizontal segue. We don't know the width yet, so we have to 137: # assume full width. That's acceptable for horizontal lines. 138: deltaY = startY < endY ? 1 : 1 139: ySeg = placeLine([ 0, @width - 1 ], true, startY + 2 * deltaY, deltaY) 140: raise "Routing failed" unless ySeg 141: 142: # Place 1st vertical 143: x1 = placeLine([ startY + deltaY, ySeg ], false, startX + startGap, 1) 144: raise "Routing failed" unless x1 145: 146: # Place 2nd vertical 147: x2 = placeLine([ ySeg + deltaY, endY ], false, endX - endGap, 1) 148: raise "Routing failed" unless x2 149: 150: # Now add the points 1 - 4 to the list and mark the zones around them. For 151: # vertical lines, we only mark vertical zones and vice versa. 152: addLineTo(points, x1, startY) # Point 1 153: if x1 != x2 154: addLineTo(points, x1, ySeg) # Point 2 155: addLineTo(points, x2, ySeg) # Point 3 156: end 157: addLineTo(points, x2, endY) # Point 4 158: addLineTo(points, endX, endY) 159: 160: points 161: end
# File lib/taskjuggler/reports/GanttRouter.rb, line 46 46: def routeLines(fromToPoints) 47: # We first convert the fromToPoints list into a more readable list of 48: # Hash objects. 49: routes = [] 50: fromToPoints.each do |touple| 51: routes << { 52: :startX => touple[0], 53: :startY => touple[1], 54: :endX => touple[2], 55: :endY => touple[3], 56: :id => touple[4] 57: } 58: end 59: 60: # To make sure that we minimize the crossings of arrows that 61: # originate from the same position, we sort the arrows by the 62: # smallest angle between the vertical line through the task end 63: # and the line between the start and end of the arrow. 64: routes.each do |r| 65: adjLeg = (r[:endX] - MinEndGap) - (r[:startX] + MinStartGap) 66: oppLeg = (r[:startY] - r[:endY]).abs 67: r[:distance] = Math.sqrt(adjLeg ** 2 + oppLeg ** 2) 68: # We can now calculate the sinus values of the angle between the 69: # vertical and a line through the coordinates. 70: sinus = oppLeg.abs / r[:distance] 71: r[:angle] = (adjLeg < 0 ? Math::PI / 2 + Math.asin(Math::PI/2 - sinus) : 72: Math.asin(sinus)) / (Math::PI / (2 * 90)) 73: end 74: # We sort the arrows from small to a large angle. In case the angle is 75: # identical, we use the length of the line as second criteria. 76: routes.sort! { |r1, r2| (r1[:angle] / 5).to_i == (r2[:angle] / 5).to_i ? 77: -(r1[:distance] <=> r2[:distance]) : 78: -(r1[:angle] <=> r2[:angle]) } 79: 80: # Now that the routes are in proper order, we can actually lay the 81: # routes. 82: routePoints = [] 83: routes.each do |r| 84: routePoints << route(r[:startX], r[:startY], r[:endX], r[:endY]) 85: end 86: routePoints 87: end
This function adds another waypoint to an existing line. In addition it adds a zone that is 2 pixel wide on each side of the line and runs in the direction of the line. This avoids too closely aligned parallel lines in the chart.
# File lib/taskjuggler/reports/GanttRouter.rb, line 209 209: def addLineTo(points, x2, y2) 210: raise "Point list may not be empty" if points.empty? 211: 212: x1, y1 = points[1] 213: points << [ x2, y2 ] 214: 215: if x1 == x2 216: # vertical line 217: return if x1 < 0 || x1 >= @width 218: x, y, w, h = justify(x1 - 2, y1, 5, y2 - y1 + 1) 219: addZone(x, y, w, h, false, true) 220: else 221: # horizontal line 222: return if y1 < 0 || x1 >= @height 223: x, y, w, h = justify(x1, y1 - 2, x2 - x1 + 1, 5) 224: addZone(x, y, w, h, true, false) 225: end 226: end
This function makes sure that the rectangle described by x, y, w and h is properly justfified. If the width or height are negative, x and y are adjusted to describe the same rectangle with all positive coordinates.
# File lib/taskjuggler/reports/GanttRouter.rb, line 232 232: def justify(x, y, w, h) 233: if w < 0 234: w = -w 235: x = x - w + 1 236: end 237: if h < 0 238: h = -h 239: y = y - h + 1 240: end 241: # Return the potentially adjusted rectangle coordinates. 242: return x, y, w, h 243: end
This function is at the heart of the routing algorithm. It tries to find a place for the line described by segment without overlapping with the defined zones. horizontal determines whether the line is running horizontally or vertically. start is the first coordinate that is looked at. In case of collisions, start is moved by delta and the check is repeated. The function returns the first collision free coordinate or the outside edge of the routing area.
# File lib/taskjuggler/reports/GanttRouter.rb, line 178 178: def placeLine(segment, horizontal, start, delta) 179: raise "delta may not be 0" if delta == 0 180: # Start must be an integer and lie within the routing area. 181: pos = start.to_i 182: pos = 0 if pos < 0 183: max = (horizontal ? @height: @width) - 1 184: pos = max if pos > max 185: 186: # Make sure that the segment coordinates are in ascending order. 187: segment.sort! 188: # TODO: Remove this check once the code becomes stable. 189: #checkLines(lines) 190: while @detector.collision?(pos, segment, horizontal) 191: pos += delta 192: # Check if we have exceded the chart area towards top/left. 193: if delta < 0 194: if pos < 0 195: break 196: end 197: else 198: # And towards right/bottom. 199: break if pos >= (horizontal ? @height : @width) 200: end 201: end 202: pos 203: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.