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
    105 -    def _inflate_event_rects_to_get_right_dimensions_for_overlap_calculations(self):
    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
    170 -    def _calc_event_sizes_and_positions(self):
    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
    226 -    def _reset_ends_today_when_start_date_is_in_future(self, event):
    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
    242 -    def _calc_width_and_height_for_period_event(self, event):
    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
    268 -    def _calc_ideal_rect_for_non_period_event(self, event):
    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
    284 -    def _calc_width_and_height_for_non_period_event(self, event):
    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
    312 -    def never_show_period_events_as_point_events(self):
    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
    329 -    def _calc_strips_sizes_and_positions(self):
    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