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 period = self._event_rect_drawn_as_period(rect) 98 direction = self._get_direction(period, up) 99 evt = self._get_overlapping_event(period, direction, selected_event, rect) 100 return (evt, direction)
101
102 - def center_text(self):
103 return self._appearance.get_center_event_texts()
104
106 for (_, rect) in self.event_data: 107 rect.Inflate(self._outer_padding, self._outer_padding)
108
109 - def _get_event_rect(self, event):
110 for (evt, rect) in self.event_data: 111 if evt == event: 112 return rect 113 return None
114
115 - def _event_rect_drawn_as_period(self, event_rect):
116 return event_rect.Y >= self.divider_y
117
118 - def _get_direction(self, period, up):
119 if up: 120 if period: 121 direction = BACKWARD 122 else: 123 direction = FORWARD 124 else: 125 if period: 126 direction = FORWARD 127 else: 128 direction = BACKWARD 129 return direction
130
131 - def _get_overlapping_event(self, period, direction, selected_event, rect):
132 event_data = self._get_overlapping_events_list(period, rect) 133 event = self._get_overlapping_event_from_list(event_data, direction, 134 selected_event) 135 return event
136
137 - def _get_overlapping_events_list(self, period, rect):
138 if period: 139 return self._get_list_with_overlapping_period_events(rect) 140 else: 141 return self._get_list_with_overlapping_point_events(rect)
142
143 - def _get_overlapping_event_from_list(self, event_data, direction, selected_event):
144 if direction == FORWARD: 145 return self._get_next_overlapping_event(event_data, selected_event) 146 else: 147 return self._get_prev_overlapping_event(event_data, selected_event)
148
149 - def _get_next_overlapping_event(self, event_data, selected_event):
150 selected_event_found = False 151 for (e, _) in event_data: 152 if not selected_event.is_subevent() and e.is_subevent(): 153 continue 154 if selected_event_found: 155 return e 156 else: 157 if e == selected_event: 158 selected_event_found = True 159 return None
160
161 - def _get_prev_overlapping_event(self, event_data, selected_event):
162 prev_event = None 163 for (e, _) in event_data: 164 if not selected_event.is_subevent() and e.is_subevent(): 165 continue 166 if e == selected_event: 167 return prev_event 168 prev_event = e
169
171 self.events_from_db = self._db.get_events(self._view_properties.displayed_period) 172 visible_events = self._view_properties.filter_events(self.events_from_db) 173 visible_events = self._place_subevents_after_container(visible_events) 174 return self._calc_event_rects(visible_events)
175
176 - def _place_subevents_after_container(self, events):
177 """ 178 All subevents belonging to a container are placed directly after 179 the container event in the events list. 180 This is necessary because the position of the subevents are 181 dependent on the position of the container. So the container metrics 182 must be calculated first. 183 """ 184 result = [] 185 for event in events: 186 if event.is_container(): 187 result.append(event) 188 result.extend(self._get_container_subevents(event, events)) 189 elif not event.is_subevent(): 190 result.append(event) 191 return result
192
193 - def _get_container_subevents(self, container, events):
194 return [ 195 evt for evt 196 in events 197 if evt.is_subevent() and evt.container is container 198 ]
199
200 - def _calc_event_rects(self, events):
201 self.event_data = self._calc_non_overlapping_event_rects(events) 202 self._deflate_rects(self.event_data) 203 return self.event_data
204
205 - def _calc_non_overlapping_event_rects(self, events):
206 self.event_data = [] 207 for event in events: 208 rect = self._create_ideal_rect_for_event(event) 209 self._prevent_overlapping_by_adjusting_rect_y(event, rect) 210 self.event_data.append((event, rect)) 211 return self.event_data
212
213 - def _deflate_rects(self, event_data):
214 for (_, rect) in event_data: 215 rect.Deflate(self._outer_padding, self._outer_padding)
216
217 - def _create_ideal_rect_for_event(self, event):
218 self._reset_ends_today_when_start_date_is_in_future(event) 219 if event.ends_today: 220 event.set_end_time(self._db.get_time_type().now()) 221 if self._display_as_period(event): 222 return self._calc_ideal_rect_for_period_event(event) 223 else: 224 return self._calc_ideal_rect_for_non_period_event(event)
225
227 if event.ends_today and self._start_date_is_in_future(event): 228 event.ends_today = False
229
230 - def _start_date_is_in_future(self, event):
231 return event.get_time_period().start_time > self._db.get_time_type().now()
232
233 - def _display_as_period(self, event):
234 return self._metrics.calc_width(event.get_time_period()) > self._period_threshold
235
236 - def _calc_ideal_rect_for_period_event(self, event):
237 rw, rh = self._calc_width_and_height_for_period_event(event) 238 rx = self._calc_x_pos_for_period_event(event) 239 ry = self._calc_y_pos_for_period_event(event) 240 return self._calc_ideal_wx_rect(rx, ry, rw, rh)
241
243 _, th = self._get_text_size(event.get_text()) 244 ew = self._metrics.calc_width(event.get_time_period()) 245 min_w = 5 * self._outer_padding 246 rw = max(ew + 2 * self._outer_padding, min_w) 247 rh = th + 2 * self._inner_padding + 2 * self._outer_padding 248 return rw, rh
249
250 - def _calc_x_pos_for_period_event(self, event):
251 return self._metrics.calc_x(event.get_time_period().start_time) - self._outer_padding
252
253 - def _calc_y_pos_for_period_event(self, event):
254 if event.is_subevent(): 255 if event.is_period(): 256 return self._get_container_ry(event) 257 else: 258 return self._metrics.half_height - self._baseline_padding 259 else: 260 return self._metrics.half_height + self._baseline_padding
261
262 - def _get_container_ry(self, subevent):
263 for (event, rect) in self.event_data: 264 if event == subevent.container: 265 return rect.y 266 return self._metrics.half_height + self._baseline_padding
267
269 if self.never_show_period_events_as_point_events() and event.is_period(): 270 return self._calc_invisible_wx_rect() 271 else: 272 rw, rh = self._calc_width_and_height_for_non_period_event(event) 273 rx = self._calc_x_pos_for_non_period_event(event, rw) 274 ry = self._calc_y_pos_for_non_period_event(event, rh) 275 if event.is_milestone(): 276 rw = rh 277 rx = self._metrics.calc_x(event.get_time_period().start_time) - rw / 2 278 return wx.Rect(rx, ry, rw, rh) 279 return self._calc_ideal_wx_rect(rx, ry, rw, rh)
280
281 - def _calc_invisible_wx_rect(self):
282 return self._calc_ideal_wx_rect(-1, -1, 0, 0)
283
285 tw, th = self._get_text_size(event.get_text()) 286 rw = tw + 2 * self._inner_padding + 2 * self._outer_padding 287 rh = th + 2 * self._inner_padding + 2 * self._outer_padding 288 if event.has_data(): 289 rw += self._data_indicator_size / 3 290 if event.get_fuzzy() or event.get_locked(): 291 rw += th + 2 * self._inner_padding 292 return rw, rh
293
294 - def _calc_x_pos_for_non_period_event(self, event, rw):
295 if self._appearance.get_draw_period_events_to_right(): 296 return self._metrics.calc_x(event.get_time_period().start_time) - self._outer_padding 297 else: 298 return self._metrics.calc_x(event.mean_time()) - rw / 2
299
300 - def _calc_y_pos_for_non_period_event(self, event, rh):
301 if event.is_milestone(): 302 return self._metrics.half_height - rh / 2 303 else: 304 return self._metrics.half_height - rh - self._baseline_padding
305
306 - def _get_text_size(self, text):
307 if len(text) > 0: 308 return self._get_text_size_fn(text) 309 else: 310 return self._get_text_size_fn(" ")
311
313 return self._appearance.get_never_show_period_events_as_point_events()
314
315 - def _calc_ideal_wx_rect(self, rx, ry, rw, rh):
316 # Drawing stuff on huge x-coordinates causes drawing to fail. 317 # MARGIN must be big enough to hide outer padding, borders, and 318 # selection markers. 319 MARGIN = 15 320 if rx < (-MARGIN): 321 move_distance = abs(rx) - MARGIN 322 rx += move_distance 323 rw -= move_distance 324 right_edge_x = rx + rw 325 if right_edge_x > self.width + MARGIN: 326 rw -= right_edge_x - self.width - MARGIN 327 return wx.Rect(rx, ry, rw, rh)
328
330 """Fill the two arrays `minor_strip_data` and `major_strip_data`.""" 331 332 def fill(strip_list, strip): 333 """Fill the given list with the given strip.""" 334 try: 335 current_start = strip.start(self._view_properties.displayed_period.start_time) 336 while current_start < self._view_properties.displayed_period.end_time: 337 next_start = strip.increment(current_start) 338 strip_list.append(TimePeriod(current_start, next_start)) 339 current_start = next_start 340 except: 341 # Exception occurs when major=century and when we are at the end of the calendar 342 pass
343 major_strip_data = [] # List of time_period 344 minor_strip_data = [] # List of time_period 345 self.major_strip, self.minor_strip = self._db.get_time_type().choose_strip(self._metrics, self._appearance) 346 if hasattr(self.major_strip, 'set_skip_s_in_decade_text'): 347 self.major_strip.set_skip_s_in_decade_text(self._view_properties.get_skip_s_in_decade_text()) 348 if hasattr(self.minor_strip, 'set_skip_s_in_decade_text'): 349 self.minor_strip.set_skip_s_in_decade_text(self._view_properties.get_skip_s_in_decade_text()) 350 fill(major_strip_data, self.major_strip) 351 fill(minor_strip_data, self.minor_strip) 352 return (minor_strip_data, major_strip_data)
353
354 - def minor_strip_is_day(self):
355 return self.minor_strip.is_day()
356
357 - def is_weekend_day(self, time):
358 return self._db.time_type.is_weekend_day(time)
359
360 - def get_hidden_event_count(self):
361 return len(self.events_from_db) - self._count_visible_events()
362
363 - def _count_visible_events(self):
364 num_visible = 0 365 for (_, rect) in self.event_data: 366 if rect.Y < self.height and (rect.Y + rect.Height) > 0: 367 num_visible += 1 368 return num_visible
369
370 - def _prevent_overlapping_by_adjusting_rect_y(self, event, event_rect):
371 if event.is_milestone(): 372 return 373 if event.is_subevent() and self._display_as_period(event): 374 self._adjust_subevent_rect(event, event_rect) 375 else: 376 if self._display_as_period(event): 377 self._adjust_period_rect(event_rect) 378 else: 379 self._adjust_point_rect(event_rect)
380
381 - def _adjust_period_rect(self, event_rect):
382 rect = self._get_overlapping_period_rect_with_largest_y(event_rect) 383 if rect is not None: 384 event_rect.Y = rect.Y + rect.height
385
386 - def _adjust_subevent_rect(self, subevent, event_rect):
387 rect = self._get_overlapping_subevent_rect_with_largest_y(subevent, event_rect) 388 if rect is not None: 389 event_rect.Y = rect.Y + rect.height 390 self._adjust_container_rect_height(subevent, event_rect)
391
392 - def _adjust_container_rect_height(self, subevent, event_rect):
393 for (evt, rect) in self.event_data: 394 if evt.is_container() and evt is subevent.container: 395 _, th = self._get_text_size(evt.get_text()) 396 rh = th + 2 * (self._inner_padding + self._outer_padding) 397 h = event_rect.Y - rect.Y + rh 398 if rect.height < h: 399 rect.Height = h 400 break
401
402 - def _get_overlapping_subevent_rect_with_largest_y(self, subevent, event_rect):
403 event_data = self._get_list_with_overlapping_subevents(subevent, event_rect) 404 rect_with_largest_y = None 405 for (_, rect) in event_data: 406 if rect_with_largest_y is None or rect.Y > rect_with_largest_y.Y: 407 rect_with_largest_y = rect 408 return rect_with_largest_y
409
410 - def _get_overlapping_period_rect_with_largest_y(self, event_rect):
411 event_data = self._get_list_with_overlapping_period_events(event_rect) 412 rect_with_largest_yh = None 413 for (_, rect) in event_data: 414 if rect_with_largest_yh is None or rect.Y + rect.Height > rect_with_largest_yh.Y + rect_with_largest_yh.Height: 415 rect_with_largest_yh = rect 416 return rect_with_largest_yh
417
418 - def _get_list_with_overlapping_period_events(self, event_rect):
419 return [(event, rect) for (event, rect) in self.event_data 420 if (self._rects_overlap(event_rect, rect) and 421 rect.Y >= self.divider_y)]
422
423 - def _get_list_with_overlapping_subevents(self, subevent, event_rect):
424 ls = [(event, rect) for (event, rect) in self.event_data 425 if (event.is_subevent() and 426 event.container is subevent.container and 427 self._rects_overlap(event_rect, rect) and 428 rect.Y >= self.divider_y)] 429 return ls
430
431 - def _adjust_point_rect(self, event_rect):
432 rect = self._get_overlapping_point_rect_with_smallest_y(event_rect) 433 if rect is not None: 434 event_rect.Y = rect.Y - event_rect.height
435
436 - def _get_overlapping_point_rect_with_smallest_y(self, event_rect):
437 event_data = self._get_list_with_overlapping_point_events(event_rect) 438 rect_with_smallest_y = None 439 for (_, rect) in event_data: 440 if rect_with_smallest_y is None or rect.Y < rect_with_smallest_y.Y: 441 rect_with_smallest_y = rect 442 return rect_with_smallest_y
443
444 - def _get_list_with_overlapping_point_events(self, event_rect):
445 return [(event, rect) for (event, rect) in self.event_data 446 if (self._rects_overlap(event_rect, rect) and 447 rect.Y < self.divider_y)]
448
449 - def _rects_overlap(self, rect1, rect2):
450 REMOVE_X_PADDING = 2 + self._outer_padding * 2 451 return (rect2.x + REMOVE_X_PADDING <= rect1.x + rect1.width and 452 rect1.x + REMOVE_X_PADDING <= rect2.x + rect2.width)
453