Parent

Class Index [+]

Quicksearch

TaskJuggler::GanttRouter

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.

Constants

MinStartGap

Minimum distance between the starting point and the first turning point.

MinEndGap

Minimum distance between the last turning point and the tip of the arrow.

Public Class Methods

new(width, height) click to toggle source

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

Public Instance Methods

addZone(x, y, w, h, horiz, vert) click to toggle source
    # 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
route(startX, startY, endX, endY) click to toggle source

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
routeLines(fromToPoints) click to toggle source
    # 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
to_html() click to toggle source

This function is only intended for debugging purposes. It marks either the vertical or horizontal zones in the chart.

     # File lib/taskjuggler/reports/GanttRouter.rb, line 165
165:     def to_html
166:       @detector.to_html
167:     end

Private Instance Methods

addLineTo(points, x2, y2) click to toggle source

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
justify(x, y, w, h) click to toggle source

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
placeLine(segment, horizontal, start, delta) click to toggle source

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.

[Validate]

Generated with the Darkfish Rdoc Generator 1.1.6.