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):
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 #--------------------------------------------------------
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 #--------------------------------------------------------
351 self._PNL_timeline.fit_last_year()
352 353 #--------------------------------------------------------
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