Gnumed.timelinelib.canvas.drawing.scene

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
    115 -    def _inflate_event_rects_to_get_right_dimensions_for_overlap_calculations(self):
    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
    180 -    def _calc_event_sizes_and_positions(self):
    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
    236 -    def _reset_ends_today_when_start_date_is_in_future(self, event):
    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
    252 -    def _calc_width_and_height_for_period_event(self, event):
    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
    278 -    def _calc_ideal_rect_for_non_period_event(self, event):
    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
    294 -    def _calc_width_and_height_for_non_period_event(self, event):
    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
    322 -    def never_show_period_events_as_point_events(self):
    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
    339 -    def _calc_strips_sizes_and_positions(self):
    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