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