| 1 | # Redmine - project management software
 | 
  
    | 2 | # Copyright (C) 2006-2011  Jean-Philippe Lang
 | 
  
    | 3 | #
 | 
  
    | 4 | # This program is free software; you can redistribute it and/or
 | 
  
    | 5 | # modify it under the terms of the GNU General Public License
 | 
  
    | 6 | # as published by the Free Software Foundation; either version 2
 | 
  
    | 7 | # of the License, or (at your option) any later version.
 | 
  
    | 8 | # 
 | 
  
    | 9 | # This program is distributed in the hope that it will be useful,
 | 
  
    | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
  
    | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
  
    | 12 | # GNU General Public License for more details.
 | 
  
    | 13 | # 
 | 
  
    | 14 | # You should have received a copy of the GNU General Public License
 | 
  
    | 15 | # along with this program; if not, write to the Free Software
 | 
  
    | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 | 
  
    | 17 | 
 | 
  
    | 18 | module Redmine
 | 
  
    | 19 |   module Helpers
 | 
  
    | 20 |     # Simple class to handle gantt chart data
 | 
  
    | 21 |     class Gantt
 | 
  
    | 22 |       include ERB::Util
 | 
  
    | 23 |       include Redmine::I18n
 | 
  
    | 24 | 
 | 
  
    | 25 |       # :nodoc:
 | 
  
    | 26 |       # Some utility methods for the PDF export
 | 
  
    | 27 |       class PDF
 | 
  
    | 28 |         MaxCharactorsForSubject = 45
 | 
  
    | 29 |         TotalWidth = 280
 | 
  
    | 30 |         LeftPaneWidth = 100
 | 
  
    | 31 | 
 | 
  
    | 32 |         def self.right_pane_width
 | 
  
    | 33 |           TotalWidth - LeftPaneWidth
 | 
  
    | 34 |         end
 | 
  
    | 35 |       end
 | 
  
    | 36 | 
 | 
  
    | 37 |       attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows
 | 
  
    | 38 |       attr_accessor :query
 | 
  
    | 39 |       attr_accessor :project
 | 
  
    | 40 |       attr_accessor :view
 | 
  
    | 41 |       
 | 
  
    | 42 |       def initialize(options={})
 | 
  
    | 43 |         options = options.dup
 | 
  
    | 44 |         
 | 
  
    | 45 |         if options[:year] && options[:year].to_i >0
 | 
  
    | 46 |           @year_from = options[:year].to_i
 | 
  
    | 47 |           if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
 | 
  
    | 48 |             @month_from = options[:month].to_i
 | 
  
    | 49 |           else
 | 
  
    | 50 |             @month_from = 1
 | 
  
    | 51 |           end
 | 
  
    | 52 |         else
 | 
  
    | 53 |           @month_from ||= Date.today.month
 | 
  
    | 54 |           @year_from ||= Date.today.year
 | 
  
    | 55 |         end
 | 
  
    | 56 |         
 | 
  
    | 57 |         zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
 | 
  
    | 58 |         @zoom = (zoom > 0 && zoom < 5) ? zoom : 2    
 | 
  
    | 59 |         months = (options[:months] || User.current.pref[:gantt_months]).to_i
 | 
  
    | 60 |         @months = (months > 0 && months < 25) ? months : 6
 | 
  
    | 61 |         
 | 
  
    | 62 |         # Save gantt parameters as user preference (zoom and months count)
 | 
  
    | 63 |         if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
 | 
  
    | 64 |           User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
 | 
  
    | 65 |           User.current.preference.save
 | 
  
    | 66 |         end
 | 
  
    | 67 |         
 | 
  
    | 68 |         @date_from = Date.civil(@year_from, @month_from, 1)
 | 
  
    | 69 |         @date_to = (@date_from >> @months) - 1
 | 
  
    | 70 |         
 | 
  
    | 71 |         @subjects = ''
 | 
  
    | 72 |         @lines = ''
 | 
  
    | 73 |         @number_of_rows = nil
 | 
  
    | 74 |         
 | 
  
    | 75 |         @issue_ancestors = []
 | 
  
    | 76 |         
 | 
  
    | 77 |         @truncated = false
 | 
  
    | 78 |         if options.has_key?(:max_rows)
 | 
  
    | 79 |           @max_rows = options[:max_rows]
 | 
  
    | 80 |         else
 | 
  
    | 81 |           @max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i
 | 
  
    | 82 |         end
 | 
  
    | 83 |       end
 | 
  
    | 84 | 
 | 
  
    | 85 |       def common_params
 | 
  
    | 86 |         { :controller => 'gantts', :action => 'show', :project_id => @project }
 | 
  
    | 87 |       end
 | 
  
    | 88 |       
 | 
  
    | 89 |       def params
 | 
  
    | 90 |         common_params.merge({  :zoom => zoom, :year => year_from, :month => month_from, :months => months })
 | 
  
    | 91 |       end
 | 
  
    | 92 |       
 | 
  
    | 93 |       def params_previous
 | 
  
    | 94 |         common_params.merge({:year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months })
 | 
  
    | 95 |       end
 | 
  
    | 96 |       
 | 
  
    | 97 |       def params_next
 | 
  
    | 98 |         common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months })
 | 
  
    | 99 |       end
 | 
  
    | 100 | 
 | 
  
    | 101 |       # Returns the number of rows that will be rendered on the Gantt chart
 | 
  
    | 102 |       def number_of_rows
 | 
  
    | 103 |         return @number_of_rows if @number_of_rows
 | 
  
    | 104 |         
 | 
  
    | 105 |         rows = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)}
 | 
  
    | 106 |         rows > @max_rows ? @max_rows : rows
 | 
  
    | 107 |       end
 | 
  
    | 108 |       
 | 
  
    | 109 |       # Returns the number of rows that will be used to list a project on
 | 
  
    | 110 |       # the Gantt chart.  This will recurse for each subproject.
 | 
  
    | 111 |       def number_of_rows_on_project(project)
 | 
  
    | 112 |         return 0 unless projects.include?(project)
 | 
  
    | 113 |         
 | 
  
    | 114 |         count = 1
 | 
  
    | 115 |         count += project_issues(project).size
 | 
  
    | 116 |         count += project_versions(project).size
 | 
  
    | 117 |         count
 | 
  
    | 118 |       end
 | 
  
    | 119 |       
 | 
  
    | 120 |       # Renders the subjects of the Gantt chart, the left side.
 | 
  
    | 121 |       def subjects(options={})
 | 
  
    | 122 |         render(options.merge(:only => :subjects)) unless @subjects_rendered
 | 
  
    | 123 |         @subjects
 | 
  
    | 124 |       end
 | 
  
    | 125 | 
 | 
  
    | 126 |       # Renders the lines of the Gantt chart, the right side
 | 
  
    | 127 |       def lines(options={})
 | 
  
    | 128 |         render(options.merge(:only => :lines)) unless @lines_rendered
 | 
  
    | 129 |         @lines
 | 
  
    | 130 |       end
 | 
  
    | 131 |       
 | 
  
    | 132 |       # Returns issues that will be rendered
 | 
  
    | 133 |       def issues
 | 
  
    | 134 |         @issues ||= @query.issues(
 | 
  
    | 135 |           :include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
 | 
  
    | 136 |           :order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC", 
 | 
  
    | 137 |           :limit => @max_rows
 | 
  
    | 138 |         )
 | 
  
    | 139 |       end
 | 
  
    | 140 |       
 | 
  
    | 141 |       # Return all the project nodes that will be displayed
 | 
  
    | 142 |       def projects
 | 
  
    | 143 |         return @projects if @projects
 | 
  
    | 144 |         
 | 
  
    | 145 |         ids = issues.collect(&:project).uniq.collect(&:id)
 | 
  
    | 146 |         if ids.any?
 | 
  
    | 147 |           # All issues projects and their visible ancestors
 | 
  
    | 148 |           @projects = Project.visible.all(
 | 
  
    | 149 |             :joins => "LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt",
 | 
  
    | 150 |             :conditions => ["child.id IN (?)", ids],
 | 
  
    | 151 |             :order => "#{Project.table_name}.lft ASC"
 | 
  
    | 152 |           ).uniq
 | 
  
    | 153 |         else
 | 
  
    | 154 |           @projects = []
 | 
  
    | 155 |         end
 | 
  
    | 156 |       end
 | 
  
    | 157 |       
 | 
  
    | 158 |       # Returns the issues that belong to +project+
 | 
  
    | 159 |       def project_issues(project)
 | 
  
    | 160 |         @issues_by_project ||= issues.group_by(&:project)
 | 
  
    | 161 |         @issues_by_project[project] || []
 | 
  
    | 162 |       end
 | 
  
    | 163 |       
 | 
  
    | 164 |       # Returns the distinct versions of the issues that belong to +project+
 | 
  
    | 165 |       def project_versions(project)
 | 
  
    | 166 |         project_issues(project).collect(&:fixed_version).compact.uniq
 | 
  
    | 167 |       end
 | 
  
    | 168 |       
 | 
  
    | 169 |       # Returns the issues that belong to +project+ and are assigned to +version+
 | 
  
    | 170 |       def version_issues(project, version)
 | 
  
    | 171 |         project_issues(project).select {|issue| issue.fixed_version == version}
 | 
  
    | 172 |       end
 | 
  
    | 173 |       
 | 
  
    | 174 |       def render(options={})
 | 
  
    | 175 |         options = {:top => 0, :top_increment => 20, :indent_increment => 20, :render => :subject, :format => :html}.merge(options)
 | 
  
    | 176 |         indent = options[:indent] || 4
 | 
  
    | 177 |         
 | 
  
    | 178 |         @subjects = '' unless options[:only] == :lines
 | 
  
    | 179 |         @lines = '' unless options[:only] == :subjects
 | 
  
    | 180 |         @number_of_rows = 0
 | 
  
    | 181 |         
 | 
  
    | 182 |         Project.project_tree(projects) do |project, level|
 | 
  
    | 183 |           options[:indent] = indent + level * options[:indent_increment]
 | 
  
    | 184 |           render_project(project, options)
 | 
  
    | 185 |           break if abort?
 | 
  
    | 186 |         end
 | 
  
    | 187 |         
 | 
  
    | 188 |         @subjects_rendered = true unless options[:only] == :lines
 | 
  
    | 189 |         @lines_rendered = true unless options[:only] == :subjects
 | 
  
    | 190 |         
 | 
  
    | 191 |         render_end(options)
 | 
  
    | 192 |       end
 | 
  
    | 193 | 
 | 
  
    | 194 |       def render_project(project, options={})
 | 
  
    | 195 |         subject_for_project(project, options) unless options[:only] == :lines
 | 
  
    | 196 |         line_for_project(project, options) unless options[:only] == :subjects
 | 
  
    | 197 |         
 | 
  
    | 198 |         options[:top] += options[:top_increment]
 | 
  
    | 199 |         options[:indent] += options[:indent_increment]
 | 
  
    | 200 |         @number_of_rows += 1
 | 
  
    | 201 |         return if abort?
 | 
  
    | 202 |         
 | 
  
    | 203 |         issues = project_issues(project).select {|i| i.fixed_version.nil?}
 | 
  
    | 204 |         sort_issues!(issues)
 | 
  
    | 205 |         if issues
 | 
  
    | 206 |           render_issues(issues, options)
 | 
  
    | 207 |           return if abort?
 | 
  
    | 208 |         end
 | 
  
    | 209 |         
 | 
  
    | 210 |         versions = project_versions(project)
 | 
  
    | 211 |         versions.each do |version|
 | 
  
    | 212 |           render_version(project, version, options)
 | 
  
    | 213 |         end
 | 
  
    | 214 | 
 | 
  
    | 215 |         # Remove indent to hit the next sibling
 | 
  
    | 216 |         options[:indent] -= options[:indent_increment]
 | 
  
    | 217 |       end
 | 
  
    | 218 | 
 | 
  
    | 219 |       def render_issues(issues, options={})
 | 
  
    | 220 |         @issue_ancestors = []
 | 
  
    | 221 |         
 | 
  
    | 222 |         issues.each do |i|
 | 
  
    | 223 |           subject_for_issue(i, options) unless options[:only] == :lines
 | 
  
    | 224 |           line_for_issue(i, options) unless options[:only] == :subjects
 | 
  
    | 225 |           
 | 
  
    | 226 |           options[:top] += options[:top_increment]
 | 
  
    | 227 |           @number_of_rows += 1
 | 
  
    | 228 |           break if abort?
 | 
  
    | 229 |         end
 | 
  
    | 230 |         
 | 
  
    | 231 |         options[:indent] -= (options[:indent_increment] * @issue_ancestors.size)
 | 
  
    | 232 |       end
 | 
  
    | 233 | 
 | 
  
    | 234 |       def render_version(project, version, options={})
 | 
  
    | 235 |         # Version header
 | 
  
    | 236 |         subject_for_version(version, options) unless options[:only] == :lines
 | 
  
    | 237 |         line_for_version(version, options) unless options[:only] == :subjects
 | 
  
    | 238 |         
 | 
  
    | 239 |         options[:top] += options[:top_increment]
 | 
  
    | 240 |         @number_of_rows += 1
 | 
  
    | 241 |         return if abort?
 | 
  
    | 242 |         
 | 
  
    | 243 |         issues = version_issues(project, version)
 | 
  
    | 244 |         if issues
 | 
  
    | 245 |           sort_issues!(issues)
 | 
  
    | 246 |           # Indent issues
 | 
  
    | 247 |           options[:indent] += options[:indent_increment]
 | 
  
    | 248 |           render_issues(issues, options)
 | 
  
    | 249 |           options[:indent] -= options[:indent_increment]
 | 
  
    | 250 |         end
 | 
  
    | 251 |       end
 | 
  
    | 252 |       
 | 
  
    | 253 |       def render_end(options={})
 | 
  
    | 254 |         case options[:format]
 | 
  
    | 255 |         when :pdf        
 | 
  
    | 256 |           options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
 | 
  
    | 257 |         end
 | 
  
    | 258 |       end
 | 
  
    | 259 | 
 | 
  
    | 260 |       def subject_for_project(project, options)
 | 
  
    | 261 |         case options[:format]
 | 
  
    | 262 |         when :html
 | 
  
    | 263 |           subject = "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>"
 | 
  
    | 264 |           subject << view.link_to_project(project)
 | 
  
    | 265 |           subject << '</span>'
 | 
  
    | 266 |           html_subject(options, subject, :css => "project-name")
 | 
  
    | 267 |         when :image
 | 
  
    | 268 |           image_subject(options, project.name)
 | 
  
    | 269 |         when :pdf
 | 
  
    | 270 |           pdf_new_page?(options)
 | 
  
    | 271 |           pdf_subject(options, project.name)
 | 
  
    | 272 |         end
 | 
  
    | 273 |       end
 | 
  
    | 274 | 
 | 
  
    | 275 |       def line_for_project(project, options)
 | 
  
    | 276 |         # Skip versions that don't have a start_date or due date
 | 
  
    | 277 |         if project.is_a?(Project) && project.start_date && project.due_date
 | 
  
    | 278 |           options[:zoom] ||= 1
 | 
  
    | 279 |           options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
 | 
  
    | 280 |             
 | 
  
    | 281 |           coords = coordinates(project.start_date, project.due_date, nil, options[:zoom])
 | 
  
    | 282 |           label = h(project)
 | 
  
    | 283 |           
 | 
  
    | 284 |           case options[:format]
 | 
  
    | 285 |           when :html
 | 
  
    | 286 |             html_task(options, coords, :css => "project task", :label => label, :markers => true)
 | 
  
    | 287 |           when :image
 | 
  
    | 288 |             image_task(options, coords, :label => label, :markers => true, :height => 3)
 | 
  
    | 289 |           when :pdf
 | 
  
    | 290 |             pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
 | 
  
    | 291 |           end
 | 
  
    | 292 |         else
 | 
  
    | 293 |           ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
 | 
  
    | 294 |           ''
 | 
  
    | 295 |         end
 | 
  
    | 296 |       end
 | 
  
    | 297 | 
 | 
  
    | 298 |       def subject_for_version(version, options)
 | 
  
    | 299 |         case options[:format]
 | 
  
    | 300 |         when :html
 | 
  
    | 301 |           subject = "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
 | 
  
    | 302 |           subject << view.link_to_version(version)
 | 
  
    | 303 |           subject << '</span>'
 | 
  
    | 304 |           html_subject(options, subject, :css => "version-name")
 | 
  
    | 305 |         when :image
 | 
  
    | 306 |           image_subject(options, version.to_s_with_project)
 | 
  
    | 307 |         when :pdf
 | 
  
    | 308 |           pdf_new_page?(options)
 | 
  
    | 309 |           pdf_subject(options, version.to_s_with_project)
 | 
  
    | 310 |         end
 | 
  
    | 311 |       end
 | 
  
    | 312 | 
 | 
  
    | 313 |       def line_for_version(version, options)
 | 
  
    | 314 |         # Skip versions that don't have a start_date
 | 
  
    | 315 |         if version.is_a?(Version) && version.start_date && version.due_date
 | 
  
    | 316 |           options[:zoom] ||= 1
 | 
  
    | 317 |           options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
 | 
  
    | 318 |           
 | 
  
    | 319 |           coords = coordinates(version.start_date, version.due_date, version.completed_pourcent, options[:zoom])
 | 
  
    | 320 |           label = "#{h version } #{h version.completed_pourcent.to_i.to_s}%"
 | 
  
    | 321 |           label = h("#{version.project} -") + label unless @project && @project == version.project
 | 
  
    | 322 | 
 | 
  
    | 323 |           case options[:format]
 | 
  
    | 324 |           when :html
 | 
  
    | 325 |             html_task(options, coords, :css => "version task", :label => label, :markers => true)
 | 
  
    | 326 |           when :image
 | 
  
    | 327 |             image_task(options, coords, :label => label, :markers => true, :height => 3)
 | 
  
    | 328 |           when :pdf
 | 
  
    | 329 |             pdf_task(options, coords, :label => label, :markers => true, :height => 0.8)
 | 
  
    | 330 |           end
 | 
  
    | 331 |         else
 | 
  
    | 332 |           ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
 | 
  
    | 333 |           ''
 | 
  
    | 334 |         end
 | 
  
    | 335 |       end
 | 
  
    | 336 | 
 | 
  
    | 337 |       def subject_for_issue(issue, options)
 | 
  
    | 338 |         while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last)
 | 
  
    | 339 |           @issue_ancestors.pop
 | 
  
    | 340 |           options[:indent] -= options[:indent_increment]
 | 
  
    | 341 |         end
 | 
  
    | 342 |           
 | 
  
    | 343 |         output = case options[:format]
 | 
  
    | 344 |         when :html
 | 
  
    | 345 |           css_classes = ''
 | 
  
    | 346 |           css_classes << ' issue-overdue' if issue.overdue?
 | 
  
    | 347 |           css_classes << ' issue-behind-schedule' if issue.behind_schedule?
 | 
  
    | 348 |           css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
 | 
  
    | 349 |           
 | 
  
    | 350 |           subject = "<span class='#{css_classes}'>"
 | 
  
    | 351 |           if issue.assigned_to.present?
 | 
  
    | 352 |             assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
 | 
  
    | 353 |             subject << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string).to_s
 | 
  
    | 354 |           end
 | 
  
    | 355 |           subject << view.link_to_issue(issue)
 | 
  
    | 356 |           subject << '</span>'
 | 
  
    | 357 |           html_subject(options, subject, :css => "issue-subject", :title => issue.subject) + "\n"
 | 
  
    | 358 |         when :image
 | 
  
    | 359 |           image_subject(options, issue.subject)
 | 
  
    | 360 |         when :pdf
 | 
  
    | 361 |           pdf_new_page?(options)
 | 
  
    | 362 |           pdf_subject(options, issue.subject)
 | 
  
    | 363 |         end
 | 
  
    | 364 | 
 | 
  
    | 365 |         unless issue.leaf?
 | 
  
    | 366 |           @issue_ancestors << issue
 | 
  
    | 367 |           options[:indent] += options[:indent_increment]
 | 
  
    | 368 |         end
 | 
  
    | 369 |         
 | 
  
    | 370 |         output
 | 
  
    | 371 |       end
 | 
  
    | 372 | 
 | 
  
    | 373 |       def line_for_issue(issue, options)
 | 
  
    | 374 |         # Skip issues that don't have a due_before (due_date or version's due_date)
 | 
  
    | 375 |         if issue.is_a?(Issue) && issue.due_before
 | 
  
    | 376 |           coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
 | 
  
    | 377 |           label = "#{ issue.status.name } #{ issue.done_ratio }%"
 | 
  
    | 378 |           
 | 
  
    | 379 |           case options[:format]
 | 
  
    | 380 |           when :html
 | 
  
    | 381 |             html_task(options, coords, :css => "task " + (issue.leaf? ? 'leaf' : 'parent'), :label => label, :issue => issue, :markers => !issue.leaf?)
 | 
  
    | 382 |           when :image
 | 
  
    | 383 |             image_task(options, coords, :label => label)
 | 
  
    | 384 |           when :pdf
 | 
  
    | 385 |             pdf_task(options, coords, :label => label)
 | 
  
    | 386 |         end
 | 
  
    | 387 |         else
 | 
  
    | 388 |           ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
 | 
  
    | 389 |           ''
 | 
  
    | 390 |         end
 | 
  
    | 391 |       end
 | 
  
    | 392 | 
 | 
  
    | 393 |       # Generates a gantt image
 | 
  
    | 394 |       # Only defined if RMagick is avalaible
 | 
  
    | 395 |       def to_image(format='PNG')
 | 
  
    | 396 |         date_to = (@date_from >> @months)-1    
 | 
  
    | 397 |         show_weeks = @zoom > 1
 | 
  
    | 398 |         show_days = @zoom > 2
 | 
  
    | 399 |         
 | 
  
    | 400 |         subject_width = 400
 | 
  
    | 401 |         header_heigth = 18
 | 
  
    | 402 |         # width of one day in pixels
 | 
  
    | 403 |         zoom = @zoom*2
 | 
  
    | 404 |         g_width = (@date_to - @date_from + 1)*zoom
 | 
  
    | 405 |         g_height = 20 * number_of_rows + 30
 | 
  
    | 406 |         headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
 | 
  
    | 407 |         height = g_height + headers_heigth
 | 
  
    | 408 |             
 | 
  
    | 409 |         imgl = Magick::ImageList.new
 | 
  
    | 410 |         imgl.new_image(subject_width+g_width+1, height)
 | 
  
    | 411 |         gc = Magick::Draw.new
 | 
  
    | 412 |         
 | 
  
    | 413 |         # Subjects
 | 
  
    | 414 |         gc.stroke('transparent')
 | 
  
    | 415 |         subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
 | 
  
    | 416 |     
 | 
  
    | 417 |         # Months headers
 | 
  
    | 418 |         month_f = @date_from
 | 
  
    | 419 |         left = subject_width
 | 
  
    | 420 |         @months.times do 
 | 
  
    | 421 |           width = ((month_f >> 1) - month_f) * zoom
 | 
  
    | 422 |           gc.fill('white')
 | 
  
    | 423 |           gc.stroke('grey')
 | 
  
    | 424 |           gc.stroke_width(1)
 | 
  
    | 425 |           gc.rectangle(left, 0, left + width, height)
 | 
  
    | 426 |           gc.fill('black')
 | 
  
    | 427 |           gc.stroke('transparent')
 | 
  
    | 428 |           gc.stroke_width(1)
 | 
  
    | 429 |           gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
 | 
  
    | 430 |           left = left + width
 | 
  
    | 431 |           month_f = month_f >> 1
 | 
  
    | 432 |         end
 | 
  
    | 433 |         
 | 
  
    | 434 |         # Weeks headers
 | 
  
    | 435 |         if show_weeks
 | 
  
    | 436 |         	left = subject_width
 | 
  
    | 437 |         	height = header_heigth
 | 
  
    | 438 |         	if @date_from.cwday == 1
 | 
  
    | 439 |         	    # date_from is monday
 | 
  
    | 440 |                 week_f = date_from
 | 
  
    | 441 |         	else
 | 
  
    | 442 |         	    # find next monday after date_from
 | 
  
    | 443 |         		week_f = @date_from + (7 - @date_from.cwday + 1)
 | 
  
    | 444 |         		width = (7 - @date_from.cwday + 1) * zoom
 | 
  
    | 445 |                 gc.fill('white')
 | 
  
    | 446 |                 gc.stroke('grey')
 | 
  
    | 447 |                 gc.stroke_width(1)
 | 
  
    | 448 |                 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
 | 
  
    | 449 |         		left = left + width
 | 
  
    | 450 |         	end
 | 
  
    | 451 |         	while week_f <= date_to
 | 
  
    | 452 |         		width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
 | 
  
    | 453 |                 gc.fill('white')
 | 
  
    | 454 |                 gc.stroke('grey')
 | 
  
    | 455 |                 gc.stroke_width(1)
 | 
  
    | 456 |                 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
 | 
  
    | 457 |                 gc.fill('black')
 | 
  
    | 458 |                 gc.stroke('transparent')
 | 
  
    | 459 |                 gc.stroke_width(1)
 | 
  
    | 460 |                 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
 | 
  
    | 461 |         		left = left + width
 | 
  
    | 462 |         		week_f = week_f+7
 | 
  
    | 463 |         	end
 | 
  
    | 464 |         end
 | 
  
    | 465 |         
 | 
  
    | 466 |         # Days details (week-end in grey)
 | 
  
    | 467 |         if show_days
 | 
  
    | 468 |         	left = subject_width
 | 
  
    | 469 |         	height = g_height + header_heigth - 1
 | 
  
    | 470 |         	wday = @date_from.cwday
 | 
  
    | 471 |         	(date_to - @date_from + 1).to_i.times do 
 | 
  
    | 472 |               width =  zoom
 | 
  
    | 473 |               gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
 | 
  
    | 474 |               gc.stroke('#ddd')
 | 
  
    | 475 |               gc.stroke_width(1)
 | 
  
    | 476 |               gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
 | 
  
    | 477 |               left = left + width
 | 
  
    | 478 |               wday = wday + 1
 | 
  
    | 479 |               wday = 1 if wday > 7
 | 
  
    | 480 |         	end
 | 
  
    | 481 |         end
 | 
  
    | 482 |     
 | 
  
    | 483 |         # border
 | 
  
    | 484 |         gc.fill('transparent')
 | 
  
    | 485 |         gc.stroke('grey')
 | 
  
    | 486 |         gc.stroke_width(1)
 | 
  
    | 487 |         gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
 | 
  
    | 488 |         gc.stroke('black')
 | 
  
    | 489 |         gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
 | 
  
    | 490 |             
 | 
  
    | 491 |         # content
 | 
  
    | 492 |         top = headers_heigth + 20
 | 
  
    | 493 | 
 | 
  
    | 494 |         gc.stroke('transparent')
 | 
  
    | 495 |         lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
 | 
  
    | 496 |         
 | 
  
    | 497 |         # today red line
 | 
  
    | 498 |         if Date.today >= @date_from and Date.today <= date_to
 | 
  
    | 499 |           gc.stroke('red')
 | 
  
    | 500 |           x = (Date.today-@date_from+1)*zoom + subject_width
 | 
  
    | 501 |           gc.line(x, headers_heigth, x, headers_heigth + g_height-1)      
 | 
  
    | 502 |         end    
 | 
  
    | 503 |         
 | 
  
    | 504 |         gc.draw(imgl)
 | 
  
    | 505 |         imgl.format = format
 | 
  
    | 506 |         imgl.to_blob
 | 
  
    | 507 |       end if Object.const_defined?(:Magick)
 | 
  
    | 508 | 
 | 
  
    | 509 |       def to_pdf
 | 
  
    | 510 |         if l(:general_pdf_encoding).upcase == 'UTF-8'
 | 
  
    | 511 |           pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
 | 
  
    | 512 |         else
 | 
  
    | 513 |           pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
 | 
  
    | 514 |         end
 | 
  
    | 515 |         pdf.SetTitle("#{l(:label_gantt)} #{project}")
 | 
  
    | 516 |         pdf.alias_nb_pages
 | 
  
    | 517 |         pdf.footer_date = format_date(Date.today)
 | 
  
    | 518 |         pdf.AddPage("L")
 | 
  
    | 519 |         pdf.SetFontStyle('B',12)
 | 
  
    | 520 |         pdf.SetX(15)
 | 
  
    | 521 |         pdf.UTF8Cell(PDF::LeftPaneWidth, 20, project.to_s)
 | 
  
    | 522 |         pdf.Ln
 | 
  
    | 523 |         pdf.SetFontStyle('B',9)
 | 
  
    | 524 |         
 | 
  
    | 525 |         subject_width = PDF::LeftPaneWidth
 | 
  
    | 526 |         header_heigth = 5
 | 
  
    | 527 |         
 | 
  
    | 528 |         headers_heigth = header_heigth
 | 
  
    | 529 |         show_weeks = false
 | 
  
    | 530 |         show_days = false
 | 
  
    | 531 |         
 | 
  
    | 532 |         if self.months < 7
 | 
  
    | 533 |           show_weeks = true
 | 
  
    | 534 |           headers_heigth = 2*header_heigth
 | 
  
    | 535 |           if self.months < 3
 | 
  
    | 536 |             show_days = true
 | 
  
    | 537 |             headers_heigth = 3*header_heigth
 | 
  
    | 538 |           end
 | 
  
    | 539 |         end
 | 
  
    | 540 |         
 | 
  
    | 541 |         g_width = PDF.right_pane_width
 | 
  
    | 542 |         zoom = (g_width) / (self.date_to - self.date_from + 1)
 | 
  
    | 543 |         g_height = 120
 | 
  
    | 544 |         t_height = g_height + headers_heigth
 | 
  
    | 545 |         
 | 
  
    | 546 |         y_start = pdf.GetY
 | 
  
    | 547 |         
 | 
  
    | 548 |         # Months headers
 | 
  
    | 549 |         month_f = self.date_from
 | 
  
    | 550 |         left = subject_width
 | 
  
    | 551 |         height = header_heigth
 | 
  
    | 552 |         self.months.times do 
 | 
  
    | 553 |           width = ((month_f >> 1) - month_f) * zoom 
 | 
  
    | 554 |           pdf.SetY(y_start)
 | 
  
    | 555 |           pdf.SetX(left)
 | 
  
    | 556 |           pdf.UTF8Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
 | 
  
    | 557 |           left = left + width
 | 
  
    | 558 |           month_f = month_f >> 1
 | 
  
    | 559 |         end  
 | 
  
    | 560 |         
 | 
  
    | 561 |         # Weeks headers
 | 
  
    | 562 |         if show_weeks
 | 
  
    | 563 |           left = subject_width
 | 
  
    | 564 |           height = header_heigth
 | 
  
    | 565 |           if self.date_from.cwday == 1
 | 
  
    | 566 |             # self.date_from is monday
 | 
  
    | 567 |             week_f = self.date_from
 | 
  
    | 568 |           else
 | 
  
    | 569 |             # find next monday after self.date_from
 | 
  
    | 570 |             week_f = self.date_from + (7 - self.date_from.cwday + 1)
 | 
  
    | 571 |             width = (7 - self.date_from.cwday + 1) * zoom-1
 | 
  
    | 572 |             pdf.SetY(y_start + header_heigth)
 | 
  
    | 573 |             pdf.SetX(left)
 | 
  
    | 574 |             pdf.UTF8Cell(width + 1, height, "", "LTR")
 | 
  
    | 575 |             left = left + width+1
 | 
  
    | 576 |           end
 | 
  
    | 577 |           while week_f <= self.date_to
 | 
  
    | 578 |             width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
 | 
  
    | 579 |             pdf.SetY(y_start + header_heigth)
 | 
  
    | 580 |             pdf.SetX(left)
 | 
  
    | 581 |             pdf.UTF8Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
 | 
  
    | 582 |             left = left + width
 | 
  
    | 583 |             week_f = week_f+7
 | 
  
    | 584 |           end
 | 
  
    | 585 |         end
 | 
  
    | 586 |         
 | 
  
    | 587 |         # Days headers
 | 
  
    | 588 |         if show_days
 | 
  
    | 589 |           left = subject_width
 | 
  
    | 590 |           height = header_heigth
 | 
  
    | 591 |           wday = self.date_from.cwday
 | 
  
    | 592 |           pdf.SetFontStyle('B',7)
 | 
  
    | 593 |           (self.date_to - self.date_from + 1).to_i.times do 
 | 
  
    | 594 |             width = zoom
 | 
  
    | 595 |             pdf.SetY(y_start + 2 * header_heigth)
 | 
  
    | 596 |             pdf.SetX(left)
 | 
  
    | 597 |             pdf.UTF8Cell(width, height, day_name(wday).first, "LTR", 0, "C")
 | 
  
    | 598 |             left = left + width
 | 
  
    | 599 |             wday = wday + 1
 | 
  
    | 600 |             wday = 1 if wday > 7
 | 
  
    | 601 |           end
 | 
  
    | 602 |         end
 | 
  
    | 603 |         
 | 
  
    | 604 |         pdf.SetY(y_start)
 | 
  
    | 605 |         pdf.SetX(15)
 | 
  
    | 606 |         pdf.UTF8Cell(subject_width+g_width-15, headers_heigth, "", 1)
 | 
  
    | 607 |         
 | 
  
    | 608 |         # Tasks
 | 
  
    | 609 |         top = headers_heigth + y_start
 | 
  
    | 610 |         options = {
 | 
  
    | 611 |           :top => top,
 | 
  
    | 612 |           :zoom => zoom,
 | 
  
    | 613 |           :subject_width => subject_width,
 | 
  
    | 614 |           :g_width => g_width,
 | 
  
    | 615 |           :indent => 0,
 | 
  
    | 616 |           :indent_increment => 5,
 | 
  
    | 617 |           :top_increment => 5,
 | 
  
    | 618 |           :format => :pdf,
 | 
  
    | 619 |           :pdf => pdf
 | 
  
    | 620 |         }
 | 
  
    | 621 |         render(options)
 | 
  
    | 622 |         pdf.Output
 | 
  
    | 623 |       end
 | 
  
    | 624 |       
 | 
  
    | 625 |       private
 | 
  
    | 626 |       
 | 
  
    | 627 |       def coordinates(start_date, end_date, progress, zoom=nil)
 | 
  
    | 628 |         zoom ||= @zoom
 | 
  
    | 629 |         
 | 
  
    | 630 |         coords = {}
 | 
  
    | 631 |         if start_date && end_date && start_date < self.date_to && end_date > self.date_from
 | 
  
    | 632 |           if start_date > self.date_from
 | 
  
    | 633 |             coords[:start] = start_date - self.date_from
 | 
  
    | 634 |             coords[:bar_start] = start_date - self.date_from
 | 
  
    | 635 |           else
 | 
  
    | 636 |             coords[:bar_start] = 0
 | 
  
    | 637 |           end
 | 
  
    | 638 |           if end_date < self.date_to
 | 
  
    | 639 |             coords[:end] = end_date - self.date_from
 | 
  
    | 640 |             coords[:bar_end] = end_date - self.date_from + 1
 | 
  
    | 641 |           else
 | 
  
    | 642 |             coords[:bar_end] = self.date_to - self.date_from + 1
 | 
  
    | 643 |           end
 | 
  
    | 644 |         
 | 
  
    | 645 |           if progress
 | 
  
    | 646 |             progress_date = start_date + (end_date - start_date) * (progress / 100.0)
 | 
  
    | 647 |             if progress_date > self.date_from && progress_date > start_date
 | 
  
    | 648 |               if progress_date < self.date_to
 | 
  
    | 649 |                 coords[:bar_progress_end] = progress_date - self.date_from + 1
 | 
  
    | 650 |               else
 | 
  
    | 651 |                 coords[:bar_progress_end] = self.date_to - self.date_from + 1
 | 
  
    | 652 |               end
 | 
  
    | 653 |             end
 | 
  
    | 654 |             
 | 
  
    | 655 |             if progress_date < Date.today
 | 
  
    | 656 |               late_date = [Date.today, end_date].min
 | 
  
    | 657 |               if late_date > self.date_from && late_date > start_date
 | 
  
    | 658 |                 if late_date < self.date_to
 | 
  
    | 659 |                   coords[:bar_late_end] = late_date - self.date_from + 1
 | 
  
    | 660 |                 else
 | 
  
    | 661 |                   coords[:bar_late_end] = self.date_to - self.date_from + 1
 | 
  
    | 662 |                 end
 | 
  
    | 663 |               end
 | 
  
    | 664 |             end
 | 
  
    | 665 |           end
 | 
  
    | 666 |         end
 | 
  
    | 667 |         
 | 
  
    | 668 |         # Transforms dates into pixels witdh
 | 
  
    | 669 |         coords.keys.each do |key|
 | 
  
    | 670 |           coords[key] = (coords[key] * zoom).floor
 | 
  
    | 671 |         end
 | 
  
    | 672 |         coords
 | 
  
    | 673 |       end
 | 
  
    | 674 | 
 | 
  
    | 675 |       # Sorts a collection of issues by start_date, due_date, id for gantt rendering
 | 
  
    | 676 |       def sort_issues!(issues)
 | 
  
    | 677 |         issues.sort! { |a, b| gantt_issue_compare(a, b, issues) }
 | 
  
    | 678 |       end
 | 
  
    | 679 |   
 | 
  
    | 680 |       # TODO: top level issues should be sorted by start date
 | 
  
    | 681 |       def gantt_issue_compare(x, y, issues)
 | 
  
    | 682 |         if x.root_id == y.root_id
 | 
  
    | 683 |           x.lft <=> y.lft
 | 
  
    | 684 |         else
 | 
  
    | 685 |           x.root_id <=> y.root_id
 | 
  
    | 686 |         end
 | 
  
    | 687 |       end
 | 
  
    | 688 |       
 | 
  
    | 689 |       def current_limit
 | 
  
    | 690 |         if @max_rows
 | 
  
    | 691 |           @max_rows - @number_of_rows
 | 
  
    | 692 |         else
 | 
  
    | 693 |           nil
 | 
  
    | 694 |         end
 | 
  
    | 695 |       end
 | 
  
    | 696 |       
 | 
  
    | 697 |       def abort?
 | 
  
    | 698 |         if @max_rows && @number_of_rows >= @max_rows
 | 
  
    | 699 |           @truncated = true
 | 
  
    | 700 |         end
 | 
  
    | 701 |       end
 | 
  
    | 702 |       
 | 
  
    | 703 |       def pdf_new_page?(options)
 | 
  
    | 704 |         if options[:top] > 180
 | 
  
    | 705 |           options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
 | 
  
    | 706 |           options[:pdf].AddPage("L")
 | 
  
    | 707 |           options[:top] = 15
 | 
  
    | 708 |           options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
 | 
  
    | 709 |         end
 | 
  
    | 710 |       end
 | 
  
    | 711 |       
 | 
  
    | 712 |       def html_subject(params, subject, options={})
 | 
  
    | 713 |         style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
 | 
  
    | 714 |         style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
 | 
  
    | 715 |         
 | 
  
    | 716 |         output = view.content_tag 'div', subject, :class => options[:css], :style => style, :title => options[:title]
 | 
  
    | 717 |         @subjects << output
 | 
  
    | 718 |         output
 | 
  
    | 719 |       end
 | 
  
    | 720 |       
 | 
  
    | 721 |       def pdf_subject(params, subject, options={})
 | 
  
    | 722 |         params[:pdf].SetY(params[:top])
 | 
  
    | 723 |         params[:pdf].SetX(15)
 | 
  
    | 724 |         
 | 
  
    | 725 |         char_limit = PDF::MaxCharactorsForSubject - params[:indent]
 | 
  
    | 726 |         params[:pdf].UTF8Cell(params[:subject_width]-15, 5, (" " * params[:indent]) +  subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
 | 
  
    | 727 |       
 | 
  
    | 728 |         params[:pdf].SetY(params[:top])
 | 
  
    | 729 |         params[:pdf].SetX(params[:subject_width])
 | 
  
    | 730 |         params[:pdf].UTF8Cell(params[:g_width], 5, "", "LR")
 | 
  
    | 731 |       end
 | 
  
    | 732 |       
 | 
  
    | 733 |       def image_subject(params, subject, options={})
 | 
  
    | 734 |         params[:image].fill('black')
 | 
  
    | 735 |         params[:image].stroke('transparent')
 | 
  
    | 736 |         params[:image].stroke_width(1)
 | 
  
    | 737 |         params[:image].text(params[:indent], params[:top] + 2, subject)
 | 
  
    | 738 |       end
 | 
  
    | 739 |       
 | 
  
    | 740 |       def html_task(params, coords, options={})
 | 
  
    | 741 |         output = ''
 | 
  
    | 742 |         # Renders the task bar, with progress and late
 | 
  
    | 743 |         if coords[:bar_start] && coords[:bar_end]
 | 
  
    | 744 |           output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_todo'> </div>"
 | 
  
    | 745 |           
 | 
  
    | 746 |           if coords[:bar_late_end]
 | 
  
    | 747 |             output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_late_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_late'> </div>"
 | 
  
    | 748 |           end
 | 
  
    | 749 |           if coords[:bar_progress_end]
 | 
  
    | 750 |             output << "<div style='top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_progress_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_done'> </div>"
 | 
  
    | 751 |           end
 | 
  
    | 752 |         end
 | 
  
    | 753 |         # Renders the markers
 | 
  
    | 754 |         if options[:markers]
 | 
  
    | 755 |           if coords[:start]
 | 
  
    | 756 |             output << "<div style='top:#{ params[:top] }px;left:#{ coords[:start] }px;width:15px;' class='#{options[:css]} marker starting'> </div>"
 | 
  
    | 757 |           end
 | 
  
    | 758 |           if coords[:end]
 | 
  
    | 759 |             output << "<div style='top:#{ params[:top] }px;left:#{ coords[:end] + params[:zoom] }px;width:15px;' class='#{options[:css]} marker ending'> </div>"
 | 
  
    | 760 |           end
 | 
  
    | 761 |         end
 | 
  
    | 762 |         # Renders the label on the right
 | 
  
    | 763 |         if options[:label]
 | 
  
    | 764 |           output << "<div style='top:#{ params[:top] }px;left:#{ (coords[:bar_end] || 0) + 8 }px;' class='#{options[:css]} label'>"
 | 
  
    | 765 |           output << options[:label]
 | 
  
    | 766 |           output << "</div>"
 | 
  
    | 767 |         end
 | 
  
    | 768 |         # Renders the tooltip
 | 
  
    | 769 |         if options[:issue] && coords[:bar_start] && coords[:bar_end]
 | 
  
    | 770 |           output << "<div class='tooltip' style='position: absolute;top:#{ params[:top] }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_end] - coords[:bar_start] }px;height:12px;'>"
 | 
  
    | 771 |           output << '<span class="tip">'
 | 
  
    | 772 |           output << view.render_issue_tooltip(options[:issue])
 | 
  
    | 773 |           output << "</span></div>"
 | 
  
    | 774 |         end
 | 
  
    | 775 |         @lines << output
 | 
  
    | 776 |         output
 | 
  
    | 777 |       end
 | 
  
    | 778 |       
 | 
  
    | 779 |       def pdf_task(params, coords, options={})
 | 
  
    | 780 |         height = options[:height] || 2
 | 
  
    | 781 |         
 | 
  
    | 782 |         # Renders the task bar, with progress and late
 | 
  
    | 783 |         if coords[:bar_start] && coords[:bar_end]
 | 
  
    | 784 |           params[:pdf].SetY(params[:top]+1.5)
 | 
  
    | 785 |           params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
 | 
  
    | 786 |           params[:pdf].SetFillColor(200,200,200)
 | 
  
    | 787 |           params[:pdf].UTF8Cell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1)
 | 
  
    | 788 |             
 | 
  
    | 789 |           if coords[:bar_late_end]
 | 
  
    | 790 |             params[:pdf].SetY(params[:top]+1.5)
 | 
  
    | 791 |             params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
 | 
  
    | 792 |             params[:pdf].SetFillColor(255,100,100)
 | 
  
    | 793 |             params[:pdf].UTF8Cell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1)
 | 
  
    | 794 |           end
 | 
  
    | 795 |           if coords[:bar_progress_end]
 | 
  
    | 796 |             params[:pdf].SetY(params[:top]+1.5)
 | 
  
    | 797 |             params[:pdf].SetX(params[:subject_width] + coords[:bar_start])
 | 
  
    | 798 |             params[:pdf].SetFillColor(90,200,90)
 | 
  
    | 799 |             params[:pdf].UTF8Cell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1)
 | 
  
    | 800 |           end
 | 
  
    | 801 |         end
 | 
  
    | 802 |         # Renders the markers
 | 
  
    | 803 |         if options[:markers]
 | 
  
    | 804 |           if coords[:start]
 | 
  
    | 805 |             params[:pdf].SetY(params[:top] + 1)
 | 
  
    | 806 |             params[:pdf].SetX(params[:subject_width] + coords[:start] - 1)
 | 
  
    | 807 |             params[:pdf].SetFillColor(50,50,200)
 | 
  
    | 808 |             params[:pdf].UTF8Cell(2, 2, "", 0, 0, "", 1) 
 | 
  
    | 809 |           end
 | 
  
    | 810 |           if coords[:end]
 | 
  
    | 811 |             params[:pdf].SetY(params[:top] + 1)
 | 
  
    | 812 |             params[:pdf].SetX(params[:subject_width] + coords[:end] - 1)
 | 
  
    | 813 |             params[:pdf].SetFillColor(50,50,200)
 | 
  
    | 814 |             params[:pdf].UTF8Cell(2, 2, "", 0, 0, "", 1) 
 | 
  
    | 815 |           end
 | 
  
    | 816 |         end
 | 
  
    | 817 |         # Renders the label on the right
 | 
  
    | 818 |         if options[:label]
 | 
  
    | 819 |           params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5)
 | 
  
    | 820 |           params[:pdf].UTF8Cell(30, 2, options[:label])
 | 
  
    | 821 |         end
 | 
  
    | 822 |       end
 | 
  
    | 823 | 
 | 
  
    | 824 |       def image_task(params, coords, options={})
 | 
  
    | 825 |         height = options[:height] || 6
 | 
  
    | 826 |         
 | 
  
    | 827 |         # Renders the task bar, with progress and late
 | 
  
    | 828 |         if coords[:bar_start] && coords[:bar_end]
 | 
  
    | 829 |           params[:image].fill('#aaa')
 | 
  
    | 830 |           params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_end], params[:top] - height)
 | 
  
    | 831 |  
 | 
  
    | 832 |           if coords[:bar_late_end]
 | 
  
    | 833 |             params[:image].fill('#f66')
 | 
  
    | 834 |             params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_late_end], params[:top] - height)
 | 
  
    | 835 |           end
 | 
  
    | 836 |           if coords[:bar_progress_end]
 | 
  
    | 837 |             params[:image].fill('#00c600')
 | 
  
    | 838 |             params[:image].rectangle(params[:subject_width] + coords[:bar_start], params[:top], params[:subject_width] + coords[:bar_progress_end], params[:top] - height)
 | 
  
    | 839 |           end
 | 
  
    | 840 |         end
 | 
  
    | 841 |         # Renders the markers
 | 
  
    | 842 |         if options[:markers]
 | 
  
    | 843 |           if coords[:start]
 | 
  
    | 844 |             x = params[:subject_width] + coords[:start]
 | 
  
    | 845 |             y = params[:top] - height / 2
 | 
  
    | 846 |             params[:image].fill('blue')
 | 
  
    | 847 |             params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
 | 
  
    | 848 |           end
 | 
  
    | 849 |           if coords[:end]
 | 
  
    | 850 |             x = params[:subject_width] + coords[:end] + params[:zoom]
 | 
  
    | 851 |             y = params[:top] - height / 2
 | 
  
    | 852 |             params[:image].fill('blue')
 | 
  
    | 853 |             params[:image].polygon(x-4, y, x, y-4, x+4, y, x, y+4)
 | 
  
    | 854 |           end
 | 
  
    | 855 |         end
 | 
  
    | 856 |         # Renders the label on the right
 | 
  
    | 857 |         if options[:label]
 | 
  
    | 858 |           params[:image].fill('black')
 | 
  
    | 859 |           params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5,params[:top] + 1, options[:label])
 | 
  
    | 860 |         end
 | 
  
    | 861 |       end
 | 
  
    | 862 |     end
 | 
  
    | 863 |   end
 | 
  
    | 864 | end
 |