lib/reports/GanttChart.rb in taskjuggler-0.0.2 vs lib/reports/GanttChart.rb in taskjuggler-0.0.3
- old
+ new
@@ -25,21 +25,25 @@
# the user. In addition the width _or_ the scale can be provided. The
# non-provided value will then be calculated. So after the object has been
# created, the user must call generateByWidth or generateByResolution.
class GanttChart
+ # The height in pixels of a horizontal scrollbar on an HTML page. This
+ # value should be large enough to work for all browsers.
+ SCROLLBARHEIGHT = 20
+
include HTMLGraphics
attr_reader :start, :end, :now, :weekStartsMonday, :header, :width,
:scale, :scales, :table
attr_writer :viewWidth
# Create the GanttChart object, but don't do much right now. We still need
# more information about the chart before we can actually generate it. _now_
# is the date that should be used as current date. _weekStartsMonday_ is
# true if the weeks should start on Mondays instead of Sundays. _table_ is a
- # reference to the ReportTableElement that the chart is part of.
+ # reference to the TableReport that the chart is part of.
def initialize(now, weekStartsMonday, table = nil)
# The start and end dates of the reported interval.
@start = nil
@end = nil
@now = now
@@ -144,19 +148,21 @@
td = XMLElement.new('td',
'rowspan' => "#{2 + @lines.length + (hasScrollbar? ? 1 : 0)}",
'style' => 'padding:0px; vertical-align:top;')
# Now we generate two 'div's nested into each other. The first div is the
# view. It may contain a scrollbar if the second div is wider than the
- # first one. In case we need a scrollbar The outer div is 18 pixels
- # heigher to hold the scrollbar. Unfortunately this must be a hardcoded
- # value even though the height of the scrollbar varies from system to
- # system. This value should be good enough for most systems.
- td << (scrollDiv = XMLElement.new('div',
+ # first one. In case we need a scrollbar The outer div is
+ # SCROLLBARHEIGHT pixels heigher to hold the scrollbar. Unfortunately
+ # this must be a hardcoded value even though the height of the scrollbar
+ # varies from system to system. This value should be good enough for
+ # most systems.
+ td << (scrollDiv = XMLElement.new('div', 'class' => 'tabback',
'style' => 'position:relative; ' +
"overflow:auto; " +
"width:#{hasScrollbar? ? @viewWidth : @width}px; " +
- "height:#{@height + (hasScrollbar? ? 18 : 0)}px;"))
+ "height:#{@height +
+ (hasScrollbar? ? SCROLLBARHEIGHT : 0)}px;"))
scrollDiv << (div = XMLElement.new('div',
'style' => "margin:0px; padding:0px; " +
"position:absolute; " +
"top:0px; left:0px; " +
"width:#{@width.to_i}px; " +
@@ -242,52 +248,72 @@
end
@router.addZone(@header.nowLineX - 1, 0, 3, @height - 1, false, true)
@tasks.each do |task, lines|
- generateDepArrow(task, lines)
+ generateDepArrows(task, lines)
end
end
# Generate an output format independent description of the dependency lines
# for a specific _task_. _lines_ is a list of GanttLines that the tasks are
# displayed on. Reports with multiple scenarios have multiple lines per
# task.
- def generateDepArrow(task, lines)
+ def generateDepArrows(task, lines)
# Since we need the line and the index we use an index iterator.
lines.length.times do |lineIndex|
line = lines[lineIndex]
scenarioIdx = line.scenarioIdx
# Generate the dependencies on the start of the task.
- startX, startY = line.getTask.startDepLineStart
- task['startsuccs', scenarioIdx].each do |t, onEnd|
- # Skip inherited dependencies and tasks that are not included in the
- # chart.
- if (t.parent &&
- task.hasDependency?(scenarioIdx, 'startsuccs', t.parent, onEnd)) ||
- !@tasks.include?(t)
- next
- end
- endX, endY = @tasks[t][lineIndex].getTask.send(
- onEnd ? :endDepLineEnd : :startDepLineEnd)
- routeArrow(startX, startY, endX, endY)
- end
+ collectAndSortArrows('startsuccs', task, scenarioIdx, lineIndex,
+ *line.getTask.startDepLineStart)
# Generate the dependencies on the end of the task.
- startX, startY = line.getTask.endDepLineStart
- task['endsuccs', scenarioIdx].each do |t, onEnd|
- # Skip inherited dependencies and tasks that are not included in the
- # chart.
- if (t.parent &&
- task.hasDependency?(scenarioIdx, 'endsuccs', t.parent, onEnd)) ||
- !@tasks.include?(t)
- next
- end
- endX, endY = @tasks[t][lineIndex].getTask.send(
- onEnd ? :endDepLineEnd : :startDepLineEnd)
- routeArrow(startX, startY, endX, endY)
+ collectAndSortArrows('endsuccs', task, scenarioIdx, lineIndex,
+ *line.getTask.endDepLineStart)
+ end
+ end
+
+ # Generate the dependencies on the start or end of the task depending on
+ # _kind_. Use 'startsuccs' for the start and 'endsuccs' for end. _startX_
+ # and _startY_ are the graphic coordinates for the begin of the arrow
+ # line. _task_ references the Task in question and _scenarioIdx_ the
+ # scenario. _lineIndex_ specifies the line number in the chart.
+ def collectAndSortArrows(kind, task, scenarioIdx, lineIndex, startX, startY)
+ # We need to sort the arrows. This is an Array that holds 6 values for
+ # each entry: The x and y coordinates for start and end points, the
+ # sinus value of the angle between a vertical and the line specified by
+ # the points and the length of the line.
+ touples = []
+ task[kind, scenarioIdx].each do |t, onEnd|
+ # Skip inherited dependencies and tasks that are not included in the
+ # chart.
+ if (t.parent &&
+ task.hasDependency?(scenarioIdx, kind, t.parent, onEnd)) ||
+ !@tasks.include?(t)
+ next
end
+ endX, endY = @tasks[t][lineIndex].getTask.send(
+ onEnd ? :endDepLineEnd : :startDepLineEnd)
+ # To make sure that we minimize the crossings of arrows that
+ # originate from the same position, we sort the arrows by the
+ # smallest angle between the vertical line through the task end
+ # and the line between the start and end of the arrow.
+ oppLeg = endX - startX
+ adjLeg = (startY - endY).abs
+ hypothenuse = Math.sqrt(adjLeg ** 2 + oppLeg ** 2)
+ # We can now calculate the sinus values of the angle between the
+ # vertical and a line through the coordinates.
+ touples << [ startX, startY, endX, endY,
+ (oppLeg / hypothenuse), hypothenuse ]
+ end
+ # We sort the arrows from small to a large angle. In case the angle is
+ # identical, we use the length of the line as second criteria.
+ touples.sort! { |t1, t2| t1[4] == t2[4] ? t1[5] <=> t2[5] :
+ t1[4] <=> t2[4] }
+ touples.each do |t|
+ routeArrow(*t[0, 4])
end
end
# Route the dependency lines from the start to the end point.
def routeArrow(startX, startY, endX, endY)