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)