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

Source Code for Module Gnumed.timelinelib.drawing.scene

  1  # Copyright (C) 2009, 2010, 2011  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.drawing.utils import Metrics 
 22  from timelinelib.db.objects import TimePeriod 
 23   
 24   
 25  FORWARD  = 1 
 26  BACKWARD = -1 
 27   
28 -class TimelineScene(object):
29
30 - def __init__(self, size, db, view_properties, get_text_size_fn, config):
31 self._db = db 32 self._view_properties = view_properties 33 self._get_text_size = get_text_size_fn 34 self._config = config 35 self._outer_padding = 5 36 self._inner_padding = 3 37 self._baseline_padding = 15 38 self._period_threshold = 20 39 self._data_indicator_size = 10 40 self._metrics = Metrics(size, self._db.get_time_type(), 41 self._view_properties.displayed_period, 42 self._view_properties.divider_position) 43 self.width, self.height = size 44 self.divider_y = self._metrics.half_height 45 self.event_data = [] 46 self.major_strip = None 47 self.minor_strip = None 48 self.major_strip_data = [] 49 self.minor_strip_data = []
50
51 - def set_outer_padding(self, outer_padding):
52 self._outer_padding = outer_padding
53
54 - def set_inner_padding(self, inner_padding):
55 self._inner_padding = inner_padding
56
57 - def set_baseline_padding(self, baseline_padding):
58 self._baseline_padding = baseline_padding
59
60 - def set_period_threshold(self, period_threshold):
61 self._period_threshold = period_threshold
62
63 - def set_data_indicator_size(self, data_indicator_size):
64 self._data_indicator_size = data_indicator_size
65
66 - def create(self):
67 self._calc_event_positions() 68 self._calc_strips()
69
70 - def x_pos_for_time(self, time):
71 return self._metrics.calc_x(time)
72
73 - def x_pos_for_now(self):
74 now = self._db.get_time_type().now() 75 return self._metrics.calc_x(now)
76
77 - def get_time(self, x):
78 return self._metrics.get_time(x)
79
80 - def distance_between_times(self, time1, time2):
81 time1_x = self._metrics.calc_exact_x(time1) 82 time2_x = self._metrics.calc_exact_x(time2) 83 distance = abs(time1_x - time2_x) 84 return distance
85
86 - def width_of_period(self, time_period):
87 return self._metrics.calc_width(time_period)
88
89 - def get_closest_overlapping_event(self, selected_event, up=True):
90 rect = self._get_event_rect(selected_event) 91 period = self._event_rect_drawn_as_period(rect) 92 direction = self._get_direction(period, up) 93 evt = self._get_overlapping_event(period, direction, selected_event, rect) 94 return (evt, direction)
95
96 - def _get_event_rect(self, event):
97 for (evt, rect) in self.event_data: 98 if evt == event: 99 return rect 100 return None
101
102 - def _event_rect_drawn_as_period(self, event_rect):
103 return event_rect.Y >= self.divider_y
104
105 - def _get_direction(self, period, up):
106 if up: 107 if period: 108 direction = BACKWARD 109 else: 110 direction = FORWARD 111 else: 112 if period: 113 direction = FORWARD 114 else: 115 direction = BACKWARD 116 return direction
117
118 - def _get_overlapping_event(self, period, direction, selected_event, rect):
119 list = self._get_overlapping_events_list(period, rect) 120 event = self._get_overlapping_event_from_list(list, direction, 121 selected_event) 122 return event
123
124 - def _get_overlapping_events_list(self, period, rect):
125 if period: 126 list = self._get_list_with_overlapping_period_events(rect) 127 else: 128 list = self._get_list_with_overlapping_point_events(rect) 129 return list
130
131 - def _get_overlapping_event_from_list(self, list, direction, selected_event):
132 if direction == FORWARD: 133 return self._get_next_overlapping_event(list, selected_event) 134 else: 135 return self._get_prev_overlapping_event(list, selected_event)
136
137 - def _get_next_overlapping_event(self, list, selected_event):
138 selected_event_found = False 139 for (e,r) in list: 140 if selected_event_found: 141 return e 142 else: 143 if e == selected_event: 144 selected_event_found = True 145 return None
146
147 - def _get_prev_overlapping_event(self, list, selected_event):
148 prev_event = None 149 for (e,r) in list: 150 if e == selected_event: 151 return prev_event 152 prev_event = e
153
154 - def _calc_event_positions(self):
155 self.events_from_db = self._db.get_events(self._view_properties.displayed_period) 156 visible_events = self._view_properties.filter_events(self.events_from_db) 157 visible_events = self._place_subevents_last(visible_events) 158 self._calc_rects(visible_events)
159
160 - def _place_subevents_last(self, events):
161 reordered_events = [event for event in events 162 if not event.is_subevent()] 163 subevents = [event for event in events 164 if event.is_subevent()] 165 reordered_events.extend(subevents) 166 return reordered_events
167
168 - def _calc_rects(self, events):
169 self.event_data = [] 170 for event in events: 171 rect = self._create_rectangle_for_event(event) 172 self.event_data.append((event, rect)) 173 for (event, rect) in self.event_data: 174 rect.Deflate(self._outer_padding, self._outer_padding)
175
176 - def _create_rectangle_for_event(self, event):
177 if self._period_subevent(event): 178 return self._create_rectangle_for_period_subevent(event) 179 else: 180 return self._create_rectangle_for_possibly_overlapping_event(event)
181
182 - def _period_subevent(self, event):
183 return event.is_subevent() and event.is_period()
184
186 return self._create_ideal_rect_for_event(event)
187
189 rect = self._create_ideal_rect_for_event(event) 190 self._ensure_rect_is_not_far_outisde_screen(rect) 191 self._prevent_overlapping_by_adjusting_rect_y(event, rect) 192 return rect
193
194 - def _create_ideal_rect_for_event(self, event):
195 if event.ends_today: 196 event.time_period.end_time = self._db.get_time_type().now() 197 if self._display_as_period(event) or event.is_subevent(): 198 if self._display_as_period(event): 199 return self._create_ideal_rect_for_period_event(event) 200 else: 201 return self._create_ideal_rect_for_non_period_event(event) 202 else: 203 return self._create_ideal_rect_for_non_period_event(event)
204
205 - def _display_as_period(self, event):
206 if event.is_container(): 207 event_width = self._calc_min_subevent_threshold_width(event) 208 else: 209 event_width = self._metrics.calc_width(event.time_period) 210 return event_width > self._period_threshold
211
212 - def _calc_min_subevent_threshold_width(self, container):
213 min_width = self._metrics.calc_width(container.time_period) 214 for event in container.events: 215 if event.is_period(): 216 width = self._calc_subevent_threshold_width(event) 217 if width > 0 and width < min_width: 218 min_width = width 219 return min_width
220
221 - def _calc_subevent_threshold_width(self, event):
222 # The enlarging factor allows sub-events to be smaller than a normal 223 # event before the container becomes a point event. 224 enlarging_factor = 2 225 return enlarging_factor * self._metrics.calc_width(event.time_period)
226
227 - def _create_ideal_rect_for_period_event(self, event):
228 tw, th = self._get_text_size(event.text) 229 ew = self._metrics.calc_width(event.time_period) 230 min_w = 5 * self._outer_padding 231 rw = max(ew + 2 * self._outer_padding, min_w) 232 rh = th + 2 * self._inner_padding + 2 * self._outer_padding 233 rx = (self._metrics.calc_x(event.time_period.start_time) - 234 self._outer_padding) 235 ry = self._get_ry(event) 236 rect = wx.Rect(rx, ry, rw, rh) 237 return rect
238
239 - def _get_ry(self, event):
240 if event.is_subevent(): 241 if event.is_period(): 242 return self._get_container_ry(event) 243 else: 244 return self._metrics.half_height - self._baseline_padding 245 else: 246 return self._metrics.half_height + self._baseline_padding
247
248 - def _get_container_ry(self, subevent):
249 for (event, rect) in self.event_data: 250 if event == subevent.container: 251 return rect.y 252 return self._metrics.half_height + self._baseline_padding
253
255 tw, th = self._get_text_size(event.text) 256 rw = tw + 2 * self._inner_padding + 2 * self._outer_padding 257 rh = th + 2 * self._inner_padding + 2 * self._outer_padding 258 if event.has_data(): 259 rw += self._data_indicator_size / 3 260 if event.fuzzy or event.locked: 261 rw += th + 2 * self._inner_padding 262 rx = self._metrics.calc_x(event.mean_time()) - rw / 2 263 ry = self._metrics.half_height - rh - self._baseline_padding 264 rect = wx.Rect(rx, ry, rw, rh) 265 return rect
266
268 # Drawing stuff on huge x-coordinates causes drawing to fail. 269 # MARGIN must be big enough to hide outer padding, borders, and 270 # selection markers. 271 rx = rect.GetX() 272 rw = rect.GetWidth() 273 MARGIN = 50 274 if rx < -MARGIN: 275 distance_beyond_left_margin = -rx - MARGIN 276 rx += distance_beyond_left_margin 277 rw -= distance_beyond_left_margin 278 right_edge_x = rx + rw 279 if right_edge_x > self._metrics.width + MARGIN: 280 rw -= right_edge_x - self._metrics.width - MARGIN 281 rect.SetX(rx) 282 rect.SetWidth(rw)
283
284 - def _calc_strips(self):
285 """Fill the two arrays `minor_strip_data` and `major_strip_data`.""" 286 def fill(list, strip): 287 """Fill the given list with the given strip.""" 288 current_start = strip.start(self._view_properties.displayed_period.start_time) 289 try: 290 while current_start < self._view_properties.displayed_period.end_time: 291 next_start = strip.increment(current_start) 292 list.append(TimePeriod(self._db.get_time_type(), current_start, next_start)) 293 current_start = next_start 294 except: 295 #Exception occurs when major=century and when we are at the end of the calendar 296 pass
297 self.major_strip_data = [] # List of time_period 298 self.minor_strip_data = [] # List of time_period 299 self.major_strip, self.minor_strip = self._db.get_time_type().choose_strip(self._metrics, self._config) 300 fill(self.major_strip_data, self.major_strip) 301 fill(self.minor_strip_data, self.minor_strip)
302
303 - def get_hidden_event_count(self):
304 return len(self.events_from_db) - self._count_visible_events()
305
306 - def _count_visible_events(self):
307 num_visible = 0 308 for (event, rect) in self.event_data: 309 if rect.Y < self.height and (rect.Y + rect.Height) > 0: 310 num_visible += 1 311 return num_visible
312
313 - def _prevent_overlapping_by_adjusting_rect_y(self, event, event_rect):
314 if self._display_as_period(event): 315 self._adjust_period_rect(event_rect) 316 else: 317 self._adjust_point_rect(event_rect)
318
319 - def _adjust_period_rect(self, event_rect):
320 rect = self._get_overlapping_period_rect_with_largest_y(event_rect) 321 if rect is not None: 322 event_rect.Y = rect.Y + event_rect.height
323
324 - def _get_overlapping_period_rect_with_largest_y(self, event_rect):
325 list = self._get_list_with_overlapping_period_events(event_rect) 326 rect_with_largest_y = None 327 for (event, rect) in list: 328 if rect_with_largest_y is None or rect.Y > rect_with_largest_y.Y: 329 rect_with_largest_y = rect 330 return rect_with_largest_y
331
332 - def _get_list_with_overlapping_period_events(self, event_rect):
333 return [(event, rect) for (event, rect) in self.event_data 334 if (self._rects_overlap(event_rect, rect) and 335 rect.Y >= self.divider_y )]
336
337 - def _adjust_point_rect(self, event_rect):
338 rect = self._get_overlapping_point_rect_with_smallest_y(event_rect) 339 if rect is not None: 340 event_rect.Y = rect.Y - event_rect.height
341
342 - def _get_overlapping_point_rect_with_smallest_y(self, event_rect):
343 list = self._get_list_with_overlapping_point_events(event_rect) 344 rect_with_smallest_y = None 345 for (event, rect) in list: 346 if rect_with_smallest_y is None or rect.Y < rect_with_smallest_y.Y: 347 rect_with_smallest_y = rect 348 return rect_with_smallest_y
349
350 - def _get_list_with_overlapping_point_events(self, event_rect):
351 return [(event, rect) for (event, rect) in self.event_data 352 if (self._rects_overlap(event_rect, rect) and 353 rect.Y < self.divider_y )]
354
355 - def _rects_overlap(self, rect1, rect2):
356 return (rect2.x <= rect1.x + rect1.width and 357 rect1.x <= rect2.x + rect2.width)
358