Package Gnumed :: Package timelinelib :: Package canvas :: Package drawing :: Module scene
[frames] | no frames]

Source Code for Module Gnumed.timelinelib.canvas.drawing.scene

  1  # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg 
  2  # 
  3  # This file is part of Timeline. 
  4  # 
  5  # Timeline is free software: you can redistribute it and/or modify 
  6  # it under the terms of the GNU General Public License as published by 
  7  # the Free Software Foundation, either version 3 of the License, or 
  8  # (at your option) any later version. 
  9  # 
 10  # Timeline is distributed in the hope that it will be useful, 
 11  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 13  # GNU General Public License for more details. 
 14  # 
 15  # You should have received a copy of the GNU General Public License 
 16  # along with Timeline.  If not, see <http://www.gnu.org/licenses/>. 
 17   
 18   
 19  import wx 
 20   
 21  from timelinelib.canvas.drawing.utils import Metrics 
 22  from timelinelib.canvas.data import TimePeriod 
 23   
 24   
 25  FORWARD = 1 
 26  BACKWARD = -1 
 27   
 28   
29 -class TimelineScene(object):
30
31 - def __init__(self, size, db, view_properties, get_text_size_fn, appearance):
32 self._db = db 33 self._view_properties = view_properties 34 self._get_text_size_fn = get_text_size_fn 35 self._appearance = appearance 36 self._outer_padding = 5 37 self._inner_padding = 3 38 self._baseline_padding = 15 39 self._period_threshold = 20 40 self._data_indicator_size = 10 41 self._metrics = Metrics(size, self._db.get_time_type(), 42 self._view_properties.displayed_period, 43 self._view_properties.divider_position) 44 self.width, self.height = size 45 self.divider_y = self._metrics.half_height 46 self.event_data = [] 47 self.major_strip = None 48 self.minor_strip = None 49 self.major_strip_data = [] 50 self.minor_strip_data = []
51
52 - def set_outer_padding(self, outer_padding):
53 self._outer_padding = outer_padding
54
55 - def set_inner_padding(self, inner_padding):
56 self._inner_padding = inner_padding
57
58 - def set_baseline_padding(self, baseline_padding):
59 self._baseline_padding = baseline_padding
60
61 - def set_period_threshold(self, period_threshold):
62 self._period_threshold = period_threshold
63
64 - def set_data_indicator_size(self, data_indicator_size):
65 self._data_indicator_size = data_indicator_size
66
67 - def create(self):
68 """ 69 Creating a scene means that pixel sizes and positions are calculated 70 for events and strips. 71 """ 72 self.event_data = self._calc_event_sizes_and_positions() 73 self.minor_strip_data, self.major_strip_data = self._calc_strips_sizes_and_positions()
74
75 - def x_pos_for_time(self, time):
76 return self._metrics.calc_x(time)
77
78 - def x_pos_for_now(self):
79 now = self._db.get_time_type().now() 80 return self._metrics.calc_x(now)
81
82 - def get_time(self, x):
83 return self._metrics.get_time(x)
84
85 - def distance_between_times(self, time1, time2):
86 time1_x = self._metrics.calc_exact_x(time1) 87 time2_x = self._metrics.calc_exact_x(time2) 88 distance = abs(time1_x - time2_x) 89 return distance
90
91 - def width_of_period(self, time_period):
92 return self._metrics.calc_width(time_period)
93
94 - def get_closest_overlapping_event(self, selected_event, up=True):
95 self._inflate_event_rects_to_get_right_dimensions_for_overlap_calculations() 96 rect = self._get_event_rect(selected_event) 97 # self._get_event_rect() returns None if the selected event isn't visible. 98 # (The selected event can be scrolled out of view). In this case the period 99 # can't be calculated and because of that the direction and event can't be 100 # calculated. Instead you get exceptions when trying to access rect attributes. 101 # To avoid this situation we return event=None when no rect is found. 102 # The result on the GUI is that the event won't be moved vertically. And 103 # that's better then an exception! 104 if rect is None: 105 return (None, 1) 106 else: 107 period = self._event_rect_drawn_as_period(rect) 108 direction = self._get_direction(period, up) 109 evt = self._get_overlapping_event(period, direction, selected_event, rect) 110 return (evt, direction)
111
112 - def center_text(self):
113 return self._appearance.get_center_event_texts()
114
116 for (_, rect) in self.event_data: 117 rect.Inflate(self._outer_padding, self._outer_padding)
118
119 - def _get_event_rect(self, event):
120 for (evt, rect) in self.event_data: 121 if evt == event: 122 return rect 123 return None
124
125 - def _event_rect_drawn_as_period(self, event_rect):
126 return event_rect.Y >= self.divider_y
127
128 - def _get_direction(self, period, up):
129 if up: 130 if period: 131 direction = BACKWARD 132 else: 133 direction = FORWARD 134 else: 135 if period: 136 direction = FORWARD 137 else: 138 direction = BACKWARD 139 return direction
140
141 - def _get_overlapping_event(self, period, direction, selected_event, rect):
142 event_data = self._get_overlapping_events_list(period, rect) 143 event = self._get_overlapping_event_from_list(event_data, direction, 144 selected_event) 145 return event
146
147 - def _get_overlapping_events_list(self, period, rect):
148 if period: 149 return self._get_list_with_overlapping_period_events(rect) 150 else: 151 return self._get_list_with_overlapping_point_events(rect)
152
153 - def _get_overlapping_event_from_list(self, event_data, direction, selected_event):
154 if direction == FORWARD: 155 return self._get_next_overlapping_event(event_data, selected_event) 156 else: 157 return self._get_prev_overlapping_event(event_data, selected_event)
158
159 - def _get_next_overlapping_event(self, event_data, selected_event):
160 selected_event_found = False 161 for (e, _) in event_data: 162 if not selected_event.is_subevent() and e.is_subevent(): 163 continue 164 if selected_event_found: 165 return e 166 else: 167 if e == selected_event: 168 selected_event_found = True 169 return None
170
171 - def _get_prev_overlapping_event(self, event_data, selected_event):
172 prev_event = None 173 for (e, _) in event_data: 174 if not selected_event.is_subevent() and e.is_subevent(): 175 continue 176 if e == selected_event: 177 return prev_event 178 prev_event = e
179
181 self.events_from_db = self._db.get_events(self._view_properties.displayed_period) 182 visible_events = self._view_properties.filter_events(self.events_from_db) 183 visible_events = self._place_subevents_after_container(visible_events) 184 return self._calc_event_rects(visible_events)
185
186 - def _place_subevents_after_container(self, events):
187 """ 188 All subevents belonging to a container are placed directly after 189 the container event in the events list. 190 This is necessary because the position of the subevents are 191 dependent on the position of the container. So the container metrics 192 must be calculated first. 193 """ 194 result = [] 195 for event in events: 196 if event.is_container(): 197 result.append(event) 198 result.extend(self._get_container_subevents(event, events)) 199 elif not event.is_subevent(): 200 result.append(event) 201 return result
202
203 - def _get_container_subevents(self, container, events):
204 return [ 205 evt for evt 206 in events 207 if evt.is_subevent() and evt.container is container 208 ]
209
210 - def _calc_event_rects(self, events):
211 self.event_data = self._calc_non_overlapping_event_rects(events) 212 self._deflate_rects(self.event_data) 213 return self.event_data
214
215 - def _calc_non_overlapping_event_rects(self, events):
216 self.event_data = [] 217 for event in events: 218 rect = self._create_ideal_rect_for_event(event) 219 self._prevent_overlapping_by_adjusting_rect_y(event, rect) 220 self.event_data.append((event, rect)) 221 return self.event_data
222
223 - def _deflate_rects(self, event_data):
224 for (_, rect) in event_data: 225 rect.Deflate(self._outer_padding, self._outer_padding)
226
227 - def _create_ideal_rect_for_event(self, event):
228 self._reset_ends_today_when_start_date_is_in_future(event) 229 if event.ends_today: 230 event.set_end_time(self._db.get_time_type().now()) 231 if self._display_as_period(event): 232 return self._calc_ideal_rect_for_period_event(event) 233 else: 234 return self._calc_ideal_rect_for_non_period_event(event)
235
237 if event.ends_today and self._start_date_is_in_future(event): 238 event.ends_today = False
239
240 - def _start_date_is_in_future(self, event):
241 return event.get_time_period().start_time > self._db.get_time_type().now()
242
243 - def _display_as_period(self, event):
244 return self._metrics.calc_width(event.get_time_period()) > self._period_threshold
245
246 - def _calc_ideal_rect_for_period_event(self, event):
247 rw, rh = self._calc_width_and_height_for_period_event(event) 248 rx = self._calc_x_pos_for_period_event(event) 249 ry = self._calc_y_pos_for_period_event(event) 250 return self._calc_ideal_wx_rect(rx, ry, rw, rh)
251
253 _, th = self._get_text_size(event.get_text()) 254 ew = self._metrics.calc_width(event.get_time_period()) 255 min_w = 5 * self._outer_padding 256 rw = max(ew + 2 * self._outer_padding, min_w) 257 rh = th + 2 * self._inner_padding + 2 * self._outer_padding 258 return rw, rh
259
260 - def _calc_x_pos_for_period_event(self, event):
261 return self._metrics.calc_x(event.get_time_period().start_time) - self._outer_padding
262
263 - def _calc_y_pos_for_period_event(self, event):
264 if event.is_subevent(): 265 if event.is_period(): 266 return self._get_container_ry(event) 267 else: 268 return self._metrics.half_height - self._baseline_padding 269 else: 270 return self._metrics.half_height + self._baseline_padding
271
272 - def _get_container_ry(self, subevent):
273 for (event, rect) in self.event_data: 274 if event == subevent.container: 275 return rect.y 276 return self._metrics.half_height + self._baseline_padding
277
279 if self.never_show_period_events_as_point_events() and event.is_period(): 280 return self._calc_invisible_wx_rect() 281 else: 282 rw, rh = self._calc_width_and_height_for_non_period_event(event) 283 rx = self._calc_x_pos_for_non_period_event(event, rw) 284 ry = self._calc_y_pos_for_non_period_event(event, rh) 285 if event.is_milestone(): 286 rw = rh 287 rx = self._metrics.calc_x(event.get_time_period().start_time) - rw / 2 288 return wx.Rect(rx, ry, rw, rh) 289 return self._calc_ideal_wx_rect(rx, ry, rw, rh)
290
291 - def _calc_invisible_wx_rect(self):
292 return self._calc_ideal_wx_rect(-1, -1, 0, 0)
293
295 tw, th = self._get_text_size(event.get_text()) 296 rw = tw + 2 * self._inner_padding + 2 * self._outer_padding 297 rh = th + 2 * self._inner_padding + 2 * self._outer_padding 298 if event.has_data(): 299 rw += self._data_indicator_size / 3 300 if event.get_fuzzy() or event.get_locked(): 301 rw += th + 2 * self._inner_padding 302 return rw, rh
303
304 - def _calc_x_pos_for_non_period_event(self, event, rw):
305 if self._appearance.get_draw_period_events_to_right(): 306 return self._metrics.calc_x(event.get_time_period().start_time) - self._outer_padding 307 else: 308 return self._metrics.calc_x(event.mean_time()) - rw / 2
309
310 - def _calc_y_pos_for_non_period_event(self, event, rh):
311 if event.is_milestone(): 312 return self._metrics.half_height - rh / 2 313 else: 314 return self._metrics.half_height - rh - self._baseline_padding
315
316 - def _get_text_size(self, text):
317 if len(text) > 0: 318 return self._get_text_size_fn(text) 319 else: 320 return self._get_text_size_fn(" ")
321
323 return self._appearance.get_never_show_period_events_as_point_events()
324
325 - def _calc_ideal_wx_rect(self, rx, ry, rw, rh):
326 # Drawing stuff on huge x-coordinates causes drawing to fail. 327 # MARGIN must be big enough to hide outer padding, borders, and 328 # selection markers. 329 MARGIN = 15 330 if rx < (-MARGIN): 331 move_distance = abs(rx) - MARGIN 332 rx += move_distance 333 rw -= move_distance 334 right_edge_x = rx + rw 335 if right_edge_x > self.width + MARGIN: 336 rw -= right_edge_x - self.width - MARGIN 337 return wx.Rect(rx, ry, rw, rh)
338
340 """Fill the two arrays `minor_strip_data` and `major_strip_data`.""" 341 342 def fill(strip_list, strip): 343 """Fill the given list with the given strip.""" 344 try: 345 current_start = strip.start(self._view_properties.displayed_period.start_time) 346 while current_start < self._view_properties.displayed_period.end_time: 347 next_start = strip.increment(current_start) 348 strip_list.append(TimePeriod(current_start, next_start)) 349 current_start = next_start 350 except: 351 # Exception occurs when major=century and when we are at the end of the calendar 352 pass
353 major_strip_data = [] # List of time_period 354 minor_strip_data = [] # List of time_period 355 self.major_strip, self.minor_strip = self._db.get_time_type().choose_strip(self._metrics, self._appearance) 356 if hasattr(self.major_strip, 'set_skip_s_in_decade_text'): 357 self.major_strip.set_skip_s_in_decade_text(self._view_properties.get_skip_s_in_decade_text()) 358 if hasattr(self.minor_strip, 'set_skip_s_in_decade_text'): 359 self.minor_strip.set_skip_s_in_decade_text(self._view_properties.get_skip_s_in_decade_text()) 360 fill(major_strip_data, self.major_strip) 361 fill(minor_strip_data, self.minor_strip) 362 return (minor_strip_data, major_strip_data)
363
364 - def minor_strip_is_day(self):
365 return self.minor_strip.is_day()
366
367 - def is_weekend_day(self, time):
368 return self._db.time_type.is_weekend_day(time)
369
370 - def get_hidden_event_count(self):
371 return len(self.events_from_db) - self._count_visible_events()
372
373 - def _count_visible_events(self):
374 num_visible = 0 375 for (_, rect) in self.event_data: 376 if rect.Y < self.height and (rect.Y + rect.Height) > 0: 377 num_visible += 1 378 return num_visible
379
380 - def _prevent_overlapping_by_adjusting_rect_y(self, event, event_rect):
381 if event.is_milestone(): 382 return 383 if event.is_subevent() and self._display_as_period(event): 384 self._adjust_subevent_rect(event, event_rect) 385 else: 386 if self._display_as_period(event): 387 self._adjust_period_rect(event_rect) 388 else: 389 self._adjust_point_rect(event_rect)
390
391 - def _adjust_period_rect(self, event_rect):
392 rect = self._get_overlapping_period_rect_with_largest_y(event_rect) 393 if rect is not None: 394 event_rect.Y = rect.Y + rect.height
395
396 - def _adjust_subevent_rect(self, subevent, event_rect):
397 rect = self._get_overlapping_subevent_rect_with_largest_y(subevent, event_rect) 398 if rect is not None: 399 event_rect.Y = rect.Y + rect.height 400 self._adjust_container_rect_height(subevent, event_rect)
401
402 - def _adjust_container_rect_height(self, subevent, event_rect):
403 for (evt, rect) in self.event_data: 404 if evt.is_container() and evt is subevent.container: 405 _, th = self._get_text_size(evt.get_text()) 406 rh = th + 2 * (self._inner_padding + self._outer_padding) 407 h = event_rect.Y - rect.Y + rh 408 if rect.height < h: 409 rect.Height = h 410 break
411
412 - def _get_overlapping_subevent_rect_with_largest_y(self, subevent, event_rect):
413 event_data = self._get_list_with_overlapping_subevents(subevent, event_rect) 414 rect_with_largest_y = None 415 for (_, rect) in event_data: 416 if rect_with_largest_y is None or rect.Y > rect_with_largest_y.Y: 417 rect_with_largest_y = rect 418 return rect_with_largest_y
419
420 - def _get_overlapping_period_rect_with_largest_y(self, event_rect):
421 event_data = self._get_list_with_overlapping_period_events(event_rect) 422 rect_with_largest_yh = None 423 for (_, rect) in event_data: 424 if rect_with_largest_yh is None or rect.Y + rect.Height > rect_with_largest_yh.Y + rect_with_largest_yh.Height: 425 rect_with_largest_yh = rect 426 return rect_with_largest_yh
427
428 - def _get_list_with_overlapping_period_events(self, event_rect):
429 return [(event, rect) for (event, rect) in self.event_data 430 if (self._rects_overlap(event_rect, rect) and 431 rect.Y >= self.divider_y)]
432
433 - def _get_list_with_overlapping_subevents(self, subevent, event_rect):
434 ls = [(event, rect) for (event, rect) in self.event_data 435 if (event.is_subevent() and 436 event.container is subevent.container and 437 self._rects_overlap(event_rect, rect) and 438 rect.Y >= self.divider_y)] 439 return ls
440
441 - def _adjust_point_rect(self, event_rect):
442 rect = self._get_overlapping_point_rect_with_smallest_y(event_rect) 443 if rect is not None: 444 event_rect.Y = rect.Y - event_rect.height
445
446 - def _get_overlapping_point_rect_with_smallest_y(self, event_rect):
447 event_data = self._get_list_with_overlapping_point_events(event_rect) 448 rect_with_smallest_y = None 449 for (_, rect) in event_data: 450 if rect_with_smallest_y is None or rect.Y < rect_with_smallest_y.Y: 451 rect_with_smallest_y = rect 452 return rect_with_smallest_y
453
454 - def _get_list_with_overlapping_point_events(self, event_rect):
455 return [(event, rect) for (event, rect) in self.event_data 456 if (self._rects_overlap(event_rect, rect) and 457 rect.Y < self.divider_y)]
458
459 - def _rects_overlap(self, rect1, rect2):
460 REMOVE_X_PADDING = 2 + self._outer_padding * 2 461 return (rect2.x + REMOVE_X_PADDING <= rect1.x + rect1.width and 462 rect1.x + REMOVE_X_PADDING <= rect2.x + rect2.width)
463