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
    185 -    def _create_rectangle_for_period_subevent(self, event):
    186          return self._create_ideal_rect_for_event(event)
    187
    188 -    def _create_rectangle_for_possibly_overlapping_event(self, event):
    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
    254 -    def _create_ideal_rect_for_non_period_event(self, event):
    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
    267 -    def _ensure_rect_is_not_far_outisde_screen(self, rect):
    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