Gnumed.wxpython.gmEMRTimelineWidgets

Package Gnumed :: Package wxpython :: Module gmEMRTimelineWidgets |
[frames] | no frames]

# Source Code for Module Gnumed.wxpython.gmEMRTimelineWidgets

      1  """GNUmed patient EMR timeline browser.
      2
      3  Uses the excellent TheTimlineProject.
      4  """
      5  #================================================================
      6  __author__ = "Karsten.Hilbert@gmx.net"
      7  __license__ = "GPL v2 or later"
      8
      9  # std lib
     10  import sys
     11  import logging
     12  import os.path
     13
     14
     15  # 3rd party
     16  import wx
     17  import lxml.etree as lxml_etree
     18
     19
     20  # GNUmed libs
     21  if __name__ == '__main__':
     22          sys.path.insert(0, '../../')
     23  from Gnumed.pycommon import gmDispatcher
     24  from Gnumed.pycommon import gmTools
     25  from Gnumed.pycommon import gmMimeLib
     26  from Gnumed.pycommon import gmDateTime
     27
     28  from Gnumed.business import gmPerson
     29
     30  from Gnumed.wxpython import gmRegetMixin
     31
     32  from Gnumed.exporters import gmTimelineExporter
     33
     34
     35  _log = logging.getLogger('gm.ui.tl')
     36
     37  #============================================================
     38  from Gnumed.timelinelib.canvas.data import TimePeriod
     39
     40  # activate experimental container features
     41  from Gnumed.timelinelib.features.experimental import experimentalfeatures
     42  experimentalfeatures.EXTENDED_CONTAINER_HEIGHT.set_active(True)
     43  experimentalfeatures.EXTENDED_CONTAINER_STRATEGY.set_active(True)
     44
     45  from Gnumed.timelinelib.canvas.data.timeperiod import TimePeriod
     46  from Gnumed.timelinelib.calendar.gregorian.gregorian import GregorianDateTime
     47
     48  #------------------------------------------------------------
     49  from Gnumed.timelinelib.canvas import TimelineCanvas    # works because of __init__.py
     50
     51 -class cEMRTimelinePnl(TimelineCanvas):
     52
     53 -        def __init__(self, *args, **kwargs):
     54                  TimelineCanvas.__init__(self, args[0])  # args[0] should be "parent"
     55
     56                  self.__init_ui()
     57                  self.__register_interests()
     58
     59          #--------------------------------------------------------
     60 -        def __init_ui(self):
     61                  appearance = self.GetAppearance()
     62                  appearance.set_balloons_visible(True)
     63                  appearance.set_hide_events_done(False)
     64                  appearance.set_colorize_weekends(True)
     65                  appearance.set_display_checkmark_on_events_done(True)
     66
     67                  self.InitDragScroll(direction = wx.BOTH)
     68                  self.InitZoomSelect()
     69                  return
     70                  """
     71              appearance.set_legend_visible(self.config.show_legend)
     72              appearance.set_minor_strip_divider_line_colour(self.config.minor_strip_divider_line_colour)
     73              appearance.set_major_strip_divider_line_colour(self.config.major_strip_divider_line_colour)
     74              appearance.set_now_line_colour(self.config.now_line_colour)
     75              appearance.set_weekend_colour(self.config.weekend_colour)
     76              appearance.set_bg_colour(self.config.bg_colour)
     77              appearance.set_draw_period_events_to_right(self.config.draw_point_events_to_right)
     78              appearance.set_text_below_icon(self.config.text_below_icon)
     79              appearance.set_minor_strip_font(self.config.minor_strip_font)
     80              appearance.set_major_strip_font(self.config.major_strip_font)
     81              appearance.set_balloon_font(self.config.balloon_font)
     82              appearance.set_legend_font(self.config.legend_font)
     83              appearance.set_center_event_texts(self.config.center_event_texts)
     84              appearance.set_never_show_period_events_as_point_events(self.config.never_show_period_events_as_point_events)
     85              appearance.set_week_start(self.config.get_week_start())
     86              appearance.set_use_inertial_scrolling(self.config.use_inertial_scrolling)
     87              appearance.set_fuzzy_icon(self.config.fuzzy_icon)
     88              appearance.set_locked_icon(self.config.locked_icon)
     89              appearance.set_hyperlink_icon(self.config.hyperlink_icon)
     90              appearance.set_vertical_space_between_events(self.config.vertical_space_between_events)
     91              appearance.set_skip_s_in_decade_text(self.config.skip_s_in_decade_text)
     92              appearance.set_never_use_time(self.config.never_use_time)
     93              appearance.set_legend_pos(self.config.legend_pos)
     94                  """
     95          #--------------------------------------------------------
     96          # event handling
     97          #--------------------------------------------------------
     98 -        def __register_interests(self):
     99                  self.Bind(wx.EVT_MOUSEWHEEL, self._on_mousewheel_action)
    100                  self.Bind(wx.EVT_MOTION, self._on_mouse_motion)
    101                  self.Bind(wx.EVT_LEFT_DCLICK, self._on_left_dclick)
    102                  self.Bind(wx.EVT_LEFT_DOWN, self._on_left_down)
    103                  self.Bind(wx.EVT_LEFT_UP, self._on_left_up)
    104                  self.Bind(wx.EVT_RIGHT_DOWN, self._on_right_down)
    105                  self.Bind(wx.EVT_RIGHT_UP, self._on_right_up)
    106
    107          #self.Bind(wx.EVT_MIDDLE_DOWN, self._on_middle_down)
    108
    109          #--------------------------------------------------------
    110 -        def _on_mouse_motion(self, evt):
    111                  # not scrolling or zooming:
    112                  self.DisplayBalloons(evt)
    113
    114                  # in case we are drag-scrolling:
    115                  self.DragScroll(evt)
    116
    117                  # in case we are drag-zooming:
    118                  self.DragZoom(evt)
    119
    120          #--------------------------------------------------------
    121 -        def _on_mousewheel_action(self, evt):
    122                  self.SpecialScrollVerticallyOnMouseWheel(evt)
    123
    124          #--------------------------------------------------------
    125 -        def _on_left_dclick(self, evt):
    126                  self.CenterAtCursor(evt)
    127
    128          #--------------------------------------------------------
    129 -        def _on_left_down(self, evt):
    130                  self.StartDragScroll(evt)
    131
    132          #--------------------------------------------------------
    133 -        def _on_left_up(self, evt):
    134                  self.StopDragScroll()
    135
    136          #--------------------------------------------------------
    137 -        def _on_right_down(self, evt):
    138                  self.StartZoomSelect(evt)
    139
    140          #--------------------------------------------------------
    141 -        def _on_right_up(self, evt):
    142                  # right down-up sequence w/o mouse motion leads to
    143                  # "cannot zoom in deeper than 1 minute"
    144                  try:
    145                          self.StopDragZoom()
    146                  except ValueError:
    147                          _log.exception('drag-zoom w/o mouse motion')
    148
    149          #--------------------------------------------------------
    150          # internal API
    151          #--------------------------------------------------------
    152 -        def center_at_today(self):
    153                  now = gmDateTime.pydt_now_here()
    154                  g_now = GregorianDateTime(now.year, now.month, now.day, now.hour, now.minute, now.second).to_time()
    155                  self.Navigate(lambda tp: tp.center(g_now))
    156
    157          #--------------------------------------------------------
    158 -        def clear_timeline(self):
    159                  self.SetTimeline(None)
    160
    161          #--------------------------------------------------------
    162 -        def open_timeline(self, tl_filename):
    163                  if not self._validate_timeline_file(tl_filename):
    164                          gmDispatcher.send(signal = 'statustext', msg = 'Timeline file failed to validate.')
    165                  from Gnumed.timelinelib.db import db_open
    166                  db = db_open(tl_filename)
    167                  db.display_in_canvas(self)
    168                  self.fit_care_era()
    169
    170          #--------------------------------------------------------
    171 -        def export_as_svg(self, filename=None):
    172                  if filename is None:
    173                          filename = gmTools.get_unique_filename(suffix = '.svg')
    174                  self.SaveAsSvg(filename)
    175                  return filename
    176
    177          #--------------------------------------------------------
    178 -        def export_as_png(self, filename=None):
    179                  if filename is None:
    180                          filename = gmTools.get_unique_filename(suffix = '.png')
    181                  self.SaveAsPng(filename)
    182                  return filename
    183
    184          #--------------------------------------------------------
    185 -        def fit_all_events(self):
    186                  all_events = self._controller.get_timeline().get_all_events()
    187                  if len(all_events) == 0:
    188                          period4all_events = None
    189                  start = self._first_time(all_events)
    190                  end = self._last_time(all_events)
    191                  period4all_events = TimePeriod(start, end).zoom(-1)
    192
    193                  if period4all_events is None:
    194                          return
    195                  if period4all_events.is_period():
    196                          self.Navigate(lambda tp: tp.update(period4all_events.start_time, period4all_events.end_time))
    197                  else:
    198                          self.Navigate(lambda tp: tp.center(period4all_events.mean_time()))
    199
    200          #--------------------------------------------------------
    201 -        def _first_time(self, events):
    202                  start_time = lambda event: event.get_start_time()
    203                  return start_time(min(events, key=start_time))
    204
    205          #--------------------------------------------------------
    206 -        def _last_time(self, events):
    207                  end_time = lambda event: event.get_end_time()
    208                  return end_time(max(events, key = end_time))
    209
    210          #--------------------------------------------------------
    211 -        def fit_care_era(self):
    212                  all_eras = self._controller.get_timeline().get_all_eras()
    213                  care_era = [ e for e in all_eras if e.name == gmTimelineExporter.ERA_NAME_CARE_PERIOD ][0]
    214                  era_period = care_era.time_period
    215                  if era_period.is_period():
    216                          self.Navigate(lambda tp: tp.update(era_period.start_time, era_period.end_time))
    217                  else:
    218                          self.Navigate(lambda tp: tp.center(era_period.mean_time()))
    219
    220          #--------------------------------------------------------
    221 -        def fit_last_year(self):
    222                  end = gmDateTime.pydt_now_here()
    223                  g_end = GregorianDateTime(end.year, end.month, end.day, end.hour, end.minute, end.second).to_time()
    224                  g_start = GregorianDateTime(end.year - 1, end.month, end.day, end.hour, end.minute, end.second).to_time()
    225                  last_year = TimePeriod(g_start, g_end)
    226                  self.Navigate(lambda tp: tp.update(last_year.start_time, last_year.end_time))
    227
    228          #--------------------------------------------------------
    229 -        def _validate_timeline_file(self, tl_filename):
    230                  xsd_name = 'timeline.xsd'
    231                  xsd_paths = [
    232                          os.path.join(gmTools.gmPaths().system_app_data_dir, 'resources', 'timeline', xsd_name),
    233                          # maybe in dev tree
    234                          os.path.join(gmTools.gmPaths().local_base_dir, 'resources', 'timeline', xsd_name)
    235                  ]
    236                  xml_schema = None
    237                  for xsd_filename in xsd_paths:
    238                          _log.debug('XSD: %s', xsd_filename)
    239                          if not os.path.exists(xsd_filename):
    240                                  _log.warning('not found')
    241                                  continue
    242                          try:
    243                                  xml_schema = lxml_etree.XMLSchema(file = xsd_filename)
    244                                  break
    245                          except lxml_etree.XMLSchemaParseError:
    246                                  _log.exception('cannot parse')
    247                  if xml_schema is None:
    248                          _log.error('no XSD found')
    249                          return False
    250
    251                  with open(tl_filename, encoding = 'utf-8') as tl_file:
    252                          try:
    253                                  xml_doc = lxml_etree.parse(tl_file)
    254                          except lxml_etree.XMLSyntaxError:
    255                                  _log.exception('[%s] does not parse as XML', tl_filename)
    256                                  return False
    257
    258                  if xml_schema.validate(xml_doc):
    259                          _log.debug('[%s] seems valid', tl_filename)
    260                          return True
    261
    262                  _log.warning('[%s] does not validate against [%s]', tl_filename, xsd_filename)
    263                  for entry in xml_schema.error_log:
    264                          _log.debug(entry)
    265                  return False
    266
    267  #============================================================
    268  from Gnumed.wxGladeWidgets import wxgEMRTimelinePluginPnl
    269
    270 -class cEMRTimelinePluginPnl(wxgEMRTimelinePluginPnl.wxgEMRTimelinePluginPnl, gmRegetMixin.cRegetOnPaintMixin):
    271          """Panel holding a number of widgets. Used as notebook page."""
    272 -        def __init__(self, *args, **kwargs):
    273                  self.__tl_file = None
    274                  wxgEMRTimelinePluginPnl.wxgEMRTimelinePluginPnl.__init__(self, *args, **kwargs)
    275                  gmRegetMixin.cRegetOnPaintMixin.__init__(self)
    276  #               self.__init_ui()
    277                  self.__register_interests()
    278
    279          #--------------------------------------------------------
    280          # event handling
    281          #--------------------------------------------------------
    282 -        def __register_interests(self):
    283                  gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
    284  #               gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._schedule_data_reget)
    285
    286          #--------------------------------------------------------
    287 -        def _on_pre_patient_unselection(self):
    288                  self._PNL_timeline.clear_timeline()
    289
    290          #--------------------------------------------------------
    291 -        def _on_refresh_button_pressed(self, event):
    292                  self._populate_with_data()
    293
    294          #--------------------------------------------------------
    295 -        def _on_save_button_pressed(self, event):
    296                  if self.__tl_file is None:
    297                          return
    298                  dlg = wx.FileDialog (
    299                          parent = self,
    300                          message = _("Save timeline as images (SVG, PNG) under..."),
    301                          defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
    302                          defaultFile = 'timeline.svg',
    303                          wildcard = '%s (*.svg)|*.svg' % _('SVG files'),
    304                          style = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT
    305                  )
    306                  choice = dlg.ShowModal()
    307                  fname = dlg.GetPath()
    308                  dlg.DestroyLater()
    309                  if choice != wx.ID_OK:
    310                          return False
    311
    312                  self._PNL_timeline.export_as_svg(filename = fname)
    313                  self._PNL_timeline.export_as_png(filename = gmTools.fname_stem_with_path(fname) + '.png')
    314
    315          #--------------------------------------------------------
    316 -        def _on_print_button_pressed(self, event):
    317                  if self.__tl_file is None:
    318                          return
    319                  tl_image_file = self._PNL_timeline.export_as_png()
    320                  gmMimeLib.call_viewer_on_file(aFile = tl_image_file, block = None)
    321
    322          #--------------------------------------------------------
    323 -        def _on_export_area_button_pressed(self, event):
    324                  if self.__tl_file is None:
    325                          return
    326                  pat = gmPerson.gmCurrentPatient()
    327                  if not pat.connected:
    328                          return
    329                  pat.export_area.add_file(filename = self.__tl_file, hint = _('timeline data (xml)'))
    330                  pat.export_area.add_file(filename = self._PNL_timeline.export_as_png(), hint = _('timeline image (png)'))
    331                  pat.export_area.add_file(filename = self._PNL_timeline.export_as_svg(), hint = _('timeline image (svg)'))
    332
    333          #--------------------------------------------------------
    334 -        def _on_zoom_in_button_pressed(self, event):
    335                  self._PNL_timeline.zoom_in()
    336
    337          #--------------------------------------------------------
    338 -        def _on_zoom_out_button_pressed(self, event):
    339                  self._PNL_timeline.zoom_out()
    340
    341          #--------------------------------------------------------
    342 -        def _on_go2day_button_pressed(self, event):
    343                  self._PNL_timeline.center_at_today()
    344
    345          #--------------------------------------------------------
    346 -        def _on_zoom_fit_all_button_pressed(self, event):
    347                  self._PNL_timeline.fit_all_events()
    348
    349          #--------------------------------------------------------
    350 -        def _on_zoom_fit_last_year_button_pressed(self, event):
    351                  self._PNL_timeline.fit_last_year()
    352
    353          #--------------------------------------------------------
    354 -        def _on_zoom_fit_care_period_button_pressed(self, event):
    355                  self._PNL_timeline.fit_care_era()
    356
    357          #--------------------------------------------------------
    358          # notebook plugin glue
    359          #--------------------------------------------------------
    360 -        def repopulate_ui(self):
    361                  self._populate_with_data()
    362
    363          #--------------------------------------------------------
    364          # internal API
    365          #--------------------------------------------------------
    366  #       def __init_ui(self):
    367  #               pass
    368          #--------------------------------------------------------
    369          # reget mixin API
    370          #
    371          # remember to call
    372          #       self._schedule_data_reget()
    373          # whenever you learn of data changes from database
    374          # listener threads, dispatcher signals etc.
    375          #--------------------------------------------------------
    376 -        def _populate_with_data(self):
    377                  pat = gmPerson.gmCurrentPatient()
    378                  if not pat.connected:
    379                          return True
    380
    381                  wx.BeginBusyCursor()
    382                  try:
    383                          self.__tl_file = gmTimelineExporter.create_timeline_file(patient = pat)
    384                          self._PNL_timeline.open_timeline(self.__tl_file)
    385                  except Exception:               # more specifically: TimelineIOError
    386                          _log.exception('cannot load EMR from timeline XML')
    387                          self._PNL_timeline.clear_timeline()
    388                          self.__tl_file = gmTimelineExporter.create_fake_timeline_file(patient = pat)
    389                          self._PNL_timeline.open_timeline(self.__tl_file)
    390                          return True
    391                  finally:
    392                          wx.EndBusyCursor()
    393
    394                  return True
    395
    396  #============================================================
    397