Chris@0: # Redmine - project management software Chris@0: # Copyright (C) 2006-2008 Jean-Philippe Lang Chris@0: # Chris@0: # This program is free software; you can redistribute it and/or Chris@0: # modify it under the terms of the GNU General Public License Chris@0: # as published by the Free Software Foundation; either version 2 Chris@0: # of the License, or (at your option) any later version. Chris@0: # Chris@0: # This program is distributed in the hope that it will be useful, Chris@0: # but WITHOUT ANY WARRANTY; without even the implied warranty of Chris@0: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Chris@0: # GNU General Public License for more details. Chris@0: # Chris@0: # You should have received a copy of the GNU General Public License Chris@0: # along with this program; if not, write to the Free Software Chris@0: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Chris@0: Chris@0: module Redmine Chris@0: module Helpers Chris@0: # Simple class to handle gantt chart data Chris@0: class Gantt Chris@0: attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :events Chris@0: Chris@0: def initialize(options={}) Chris@0: options = options.dup Chris@0: @events = [] Chris@0: Chris@0: if options[:year] && options[:year].to_i >0 Chris@0: @year_from = options[:year].to_i Chris@0: if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12 Chris@0: @month_from = options[:month].to_i Chris@0: else Chris@0: @month_from = 1 Chris@0: end Chris@0: else Chris@0: @month_from ||= Date.today.month Chris@0: @year_from ||= Date.today.year Chris@0: end Chris@0: Chris@0: zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i Chris@0: @zoom = (zoom > 0 && zoom < 5) ? zoom : 2 Chris@0: months = (options[:months] || User.current.pref[:gantt_months]).to_i Chris@0: @months = (months > 0 && months < 25) ? months : 6 Chris@0: Chris@0: # Save gantt parameters as user preference (zoom and months count) Chris@0: if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months])) Chris@0: User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months Chris@0: User.current.preference.save Chris@0: end Chris@0: Chris@0: @date_from = Date.civil(@year_from, @month_from, 1) Chris@0: @date_to = (@date_from >> @months) - 1 Chris@0: end Chris@0: Chris@0: Chris@0: def events=(e) Chris@0: @events = e Chris@0: # Adds all ancestors Chris@0: root_ids = e.select {|i| i.is_a?(Issue) && i.parent_id? }.collect(&:root_id).uniq Chris@0: if root_ids.any? Chris@0: # Retrieves all nodes Chris@0: parents = Issue.find_all_by_root_id(root_ids, :conditions => ["rgt - lft > 1"]) Chris@0: # Only add ancestors Chris@0: @events += parents.select {|p| @events.detect {|i| i.is_a?(Issue) && p.is_ancestor_of?(i)}} Chris@0: end Chris@0: @events.uniq! Chris@0: # Sort issues by hierarchy and start dates Chris@0: @events.sort! {|x,y| Chris@0: if x.is_a?(Issue) && y.is_a?(Issue) Chris@0: gantt_issue_compare(x, y, @events) Chris@0: else Chris@0: gantt_start_compare(x, y) Chris@0: end Chris@0: } Chris@0: # Removes issues that have no start or end date Chris@0: @events.reject! {|i| i.is_a?(Issue) && (i.start_date.nil? || i.due_before.nil?) } Chris@0: @events Chris@0: end Chris@0: Chris@0: def params Chris@0: { :zoom => zoom, :year => year_from, :month => month_from, :months => months } Chris@0: end Chris@0: Chris@0: def params_previous Chris@0: { :year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months } Chris@0: end Chris@0: Chris@0: def params_next Chris@0: { :year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months } Chris@0: end Chris@0: Chris@0: # Generates a gantt image Chris@0: # Only defined if RMagick is avalaible Chris@1: def to_image(project, format='PNG') Chris@0: date_to = (@date_from >> @months)-1 Chris@0: show_weeks = @zoom > 1 Chris@0: show_days = @zoom > 2 Chris@0: Chris@1: subject_width = 400 Chris@0: header_heigth = 18 Chris@0: # width of one day in pixels Chris@0: zoom = @zoom*2 Chris@0: g_width = (@date_to - @date_from + 1)*zoom Chris@0: g_height = 20 * events.length + 20 Chris@0: headers_heigth = (show_weeks ? 2*header_heigth : header_heigth) Chris@0: height = g_height + headers_heigth Chris@0: Chris@0: imgl = Magick::ImageList.new Chris@0: imgl.new_image(subject_width+g_width+1, height) Chris@0: gc = Magick::Draw.new Chris@0: Chris@0: # Subjects Chris@0: top = headers_heigth + 20 Chris@0: gc.fill('black') Chris@0: gc.stroke('transparent') Chris@0: gc.stroke_width(1) Chris@0: events.each do |i| Chris@1: text = "" Chris@1: if i.is_a? Issue Chris@1: text = "#{i.tracker} #{i.id}: #{i.subject}" Chris@1: else Chris@1: text = i.name Chris@1: end Chris@1: text = "#{i.project} - #{text}" unless project && project == i.project Chris@1: gc.text(4, top + 2, text) Chris@0: top = top + 20 Chris@0: end Chris@0: Chris@0: # Months headers Chris@0: month_f = @date_from Chris@0: left = subject_width Chris@0: @months.times do Chris@0: width = ((month_f >> 1) - month_f) * zoom Chris@0: gc.fill('white') Chris@0: gc.stroke('grey') Chris@0: gc.stroke_width(1) Chris@0: gc.rectangle(left, 0, left + width, height) Chris@0: gc.fill('black') Chris@0: gc.stroke('transparent') Chris@0: gc.stroke_width(1) Chris@0: gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}") Chris@0: left = left + width Chris@0: month_f = month_f >> 1 Chris@0: end Chris@0: Chris@0: # Weeks headers Chris@0: if show_weeks Chris@0: left = subject_width Chris@0: height = header_heigth Chris@0: if @date_from.cwday == 1 Chris@0: # date_from is monday Chris@0: week_f = date_from Chris@0: else Chris@0: # find next monday after date_from Chris@0: week_f = @date_from + (7 - @date_from.cwday + 1) Chris@0: width = (7 - @date_from.cwday + 1) * zoom Chris@0: gc.fill('white') Chris@0: gc.stroke('grey') Chris@0: gc.stroke_width(1) Chris@0: gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1) Chris@0: left = left + width Chris@0: end Chris@0: while week_f <= date_to Chris@0: width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom Chris@0: gc.fill('white') Chris@0: gc.stroke('grey') Chris@0: gc.stroke_width(1) Chris@0: gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1) Chris@0: gc.fill('black') Chris@0: gc.stroke('transparent') Chris@0: gc.stroke_width(1) Chris@0: gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s) Chris@0: left = left + width Chris@0: week_f = week_f+7 Chris@0: end Chris@0: end Chris@0: Chris@0: # Days details (week-end in grey) Chris@0: if show_days Chris@0: left = subject_width Chris@0: height = g_height + header_heigth - 1 Chris@0: wday = @date_from.cwday Chris@0: (date_to - @date_from + 1).to_i.times do Chris@0: width = zoom Chris@0: gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white') Chris@0: gc.stroke('grey') Chris@0: gc.stroke_width(1) Chris@0: gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1) Chris@0: left = left + width Chris@0: wday = wday + 1 Chris@0: wday = 1 if wday > 7 Chris@0: end Chris@0: end Chris@0: Chris@0: # border Chris@0: gc.fill('transparent') Chris@0: gc.stroke('grey') Chris@0: gc.stroke_width(1) Chris@0: gc.rectangle(0, 0, subject_width+g_width, headers_heigth) Chris@0: gc.stroke('black') Chris@0: gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1) Chris@0: Chris@0: # content Chris@0: top = headers_heigth + 20 Chris@0: gc.stroke('transparent') Chris@0: events.each do |i| Chris@0: if i.is_a?(Issue) Chris@0: i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from ) Chris@0: i_end_date = (i.due_before <= date_to ? i.due_before : date_to ) Chris@0: i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor Chris@0: i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date ) Chris@0: i_done_date = (i_done_date >= date_to ? date_to : i_done_date ) Chris@0: i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today Chris@0: Chris@0: i_left = subject_width + ((i_start_date - @date_from)*zoom).floor Chris@0: i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue Chris@0: d_width = ((i_done_date - i_start_date)*zoom).floor # done width Chris@0: l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width Chris@0: Chris@0: gc.fill('grey') Chris@0: gc.rectangle(i_left, top, i_left + i_width, top - 6) Chris@0: gc.fill('red') Chris@0: gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0 Chris@0: gc.fill('blue') Chris@0: gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0 Chris@0: gc.fill('black') Chris@0: gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%") Chris@0: else Chris@0: i_left = subject_width + ((i.start_date - @date_from)*zoom).floor Chris@0: gc.fill('green') Chris@0: gc.rectangle(i_left, top, i_left + 6, top - 6) Chris@0: gc.fill('black') Chris@0: gc.text(i_left + 11, top + 1, i.name) Chris@0: end Chris@0: top = top + 20 Chris@0: end Chris@0: Chris@0: # today red line Chris@0: if Date.today >= @date_from and Date.today <= date_to Chris@0: gc.stroke('red') Chris@0: x = (Date.today-@date_from+1)*zoom + subject_width Chris@0: gc.line(x, headers_heigth, x, headers_heigth + g_height-1) Chris@0: end Chris@0: Chris@0: gc.draw(imgl) Chris@0: imgl.format = format Chris@0: imgl.to_blob Chris@0: end if Object.const_defined?(:Magick) Chris@0: Chris@0: private Chris@0: Chris@0: def gantt_issue_compare(x, y, issues) Chris@0: if x.parent_id == y.parent_id Chris@0: gantt_start_compare(x, y) Chris@0: elsif x.is_ancestor_of?(y) Chris@0: -1 Chris@0: elsif y.is_ancestor_of?(x) Chris@0: 1 Chris@0: else Chris@0: ax = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(x) && !i.is_ancestor_of?(y) }.sort_by(&:lft).first Chris@0: ay = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(y) && !i.is_ancestor_of?(x) }.sort_by(&:lft).first Chris@0: if ax.nil? && ay.nil? Chris@0: gantt_start_compare(x, y) Chris@0: else Chris@0: gantt_issue_compare(ax || x, ay || y, issues) Chris@0: end Chris@0: end Chris@0: end Chris@0: Chris@0: def gantt_start_compare(x, y) Chris@0: if x.start_date.nil? Chris@0: -1 Chris@0: elsif y.start_date.nil? Chris@0: 1 Chris@0: else Chris@0: x.start_date <=> y.start_date Chris@0: end Chris@0: end Chris@0: end Chris@0: end Chris@0: end