« 2010年12月 | トップページ | 2011年2月 »

2011年1月

2011年1月28日 (金)

Chart#isReadOnly() 対応

Chart#setReadOnly(boolean) というメソッドがあって,これをセットするとカルテの編集ができなくなって欲しいのだが,気にしないでいろいろ直していたら,結構編集できるようになってしまっていた。isReadOnly() 判定を各所に挿入して厳密化した。

MemoInspector.java

private void initComponents() {
 :
  // isReadOnly対応
  memoArea.setEnabled(!context.isReadOnly());

DocumentHistory.java

private void initComponents() {
 :
  JTextField tf = new JTextField();
  // isReadOnly対応
  tf.setEnabled(!context.isReadOnly());

AllergyInspector.java

PhysicalInspector.java

private void initComponents() {
 :
  MouseAdapter ma = new MouseAdapter() {
    private void mabeShowPopup(MouseEvent e) {
      if (e.isPopupTrigger()) {
//      isReadOnly対応
        if (context.isReadOnly()) return;

DiagnosisDocument.java

private void initialize() {
 :
  // ポップアップメニュー用設定 (isReadOnly対応)
  if (!getContext().isReadOnly()) {
    DiagnosisDocumentPopupMenu popup = new DiagnosisDocumentPopupMenu(this);
 :
private JPanel createDiagnosisPanel() {
  :
  // TransferHandler を設定する (isReadOnly対応)
  if (!getContext().isReadOnly()) {
    diagTable.setTransferHandler(new DiagnosisTransferHandler(this));
    diagTable.setDragEnabled(true);
  }

KarteDocumentViewer.java

public void openKarte() {
//pns   ダブルクリックで modifyKarte することにした (isReadOnly対応)
  if (!getContext().isReadOnly()) modifyKarte();

ChartImpl

public void newKarte() {
  if (isReadOnly()) return;

2011年1月26日 (水)

記載医師名表示

カルテのタイトル部分に,記載した人の名前が出るようにした。

Doctor

client/KarteViewer2.java

public void start() {
 :
  //
  // 確定日を分かりやすい表現に変える
  //
  StringBuilder timeStamp = new StringBuilder();
  timeStamp.append(ModelUtils.getDateAsFormatString(
    model.getDocInfo().getFirstConfirmDate(), IInfoModel.KARTE_DATE_FORMAT));
            
  if (model.getDocInfo().getStatus().equals(IInfoModel.STATUS_TMP)) {
    timeStamp.append(UNDER_TMP_SAVE);
  }
//pns^ timeStamp にカルテ作成者を入れる
  String drName = model.getCreator().getCommonName();
  timeStamp.append(" 記載医師: ");
  timeStamp.append(drName);
//pns$
  timeStampLabel.setText(timeStamp.toString());

2011年1月22日 (土)

PvtChecker の見直し

WatingListImpl.java の PvtChecker は,定期チェックとボタンによる手動チェックの2系統に別れている。

 定期チェックでは,新しく加わったリストのみサーバに取りに行く。新規受付がなければチェックだけで,受付があっても多くて1回にせいぜい1〜2件のデータしか転送しないので,チェックはすぐ終わる。しかし,既に受付になったデータの変更は読みに行かないので,例えば診察室の端末で診察終了しても,受付の端末には反映されない。同様に,受付の端末で受診キャンセルしても,診察室の端末には反映されない。受付済みのリストに起きた変更を反映させるには,ボタンによる手動チェックをかける必要がある。

 ボタンによる手動チェックでは,クライアントで「診察未終了」となっているデータを含めてサーバに取りに行く。受付端末で診察未終了状態だが,診察室で診察終了したカルテがあった場合,手動チェックをかけると受付端末でも診察終了フラグが立つ。そのかわり,取りに行くデータ量が多くなるので時間がかかる。

 これらの問題を両立させるために,全てのカルテの状態(診察終了,未終了等)だけ素早く送ることができれば,定期チェックにカルテの状態チェックを組み合わせて,診察室と受付端末が定期チェックのタイミングで常に一致するようにできるのではないかと考えた。

 問題は毎回のデータ転送にかかる時間であるが,今のところ一瞬で終わっており問題ないようだ(最近すいているので何とも言えないが)。試しに 120件の pvt でチェックをしてみたが,200〜300 msec で終わるようなので患者さんが増えても大丈夫そう。

ejb/RemotePvtServiceImpl.java

/**
 * 今日の pvt の state だけもってくる
 * @return
 */
public ArrayList<Integer> getPvtState();

ejb/RemotePvtServiceImpl.java

/**
 * pvt state だけスピーディーにとってくる by pns
 * @return
 */
public final int BYOMEI_COUNT_MASK = 100;
public final int BYOMEI_COUNT_TODAY_MASK = 10000;

public ArrayList<Integer> getPvtState() {
  ArrayList<Integer> list = new ArrayList();

  String fid = getCallersFacilityId(ctx);
  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  String date = sdf.format(new Date());
  Collection result = em.createQuery("from PatientVisitModel p where p.facilityId = :fid and p.pvtDate >= :date order by p.pvtDate")
             .setParameter("fid", fid)
             .setParameter("date", date)
             .getResultList();

  for (Iterator iter = result.iterator(); iter.hasNext(); ) {

    PatientVisitModel pvt = (PatientVisitModel) iter.next();
    Integer state = pvt.getState();
    state = state + pvt.getByomeiCount() * BYOMEI_COUNT_MASK;
    state = state + pvt.getByomeiCountToday() * BYOMEI_COUNT_TODAY_MASK;
    list.add(state);
  }
  return list;
}

delegater/PVTDelegater.java

public ArrayList<Integer> getPvtState() {
  try {
    return getService().getPvtState();
  } catch (NamingException e) {
    e.printStackTrace();
    processError(e);
  }
  return null;
}

plugin/PvtStateDecoder.java

package open.dolphin.client;

/**
 * PVT state をできるだけスピーディーに取り出すために,server から
 * pvtState + 100*byomeiCount + 10000*byomeiCountToday の形で送信するようにした
 * それを decode するためのクラス
 * @author pns
 */
class PvtStateDecoder {
  public final int BYOMEI_COUNT_MASK = 100;
  public final int BYOMEI_COUNT_TODAY_MASK = 10000;
  private int state;
  private int byomeiCount;
  private int byomeiCountToday;

  public void decode(int encoded) {
    state = encoded + 10;

    byomeiCountToday = state/BYOMEI_COUNT_TODAY_MASK;
    state = state - byomeiCountToday * BYOMEI_COUNT_TODAY_MASK;

    byomeiCount = state/BYOMEI_COUNT_MASK;
    state = state - byomeiCount * BYOMEI_COUNT_MASK;

    state = state - 10;
  }
  public int getState() {
    return state;
  }
  public int getByomeiCount() {
    return byomeiCount;
  }
  public int getByomeiCountToday() {
    return byomeiCountToday;
  }
}

WatingListImpl.java

大幅にリファクタリングした

2011年1月20日 (木)

FileInspector の作成

「新 iMac で画面が広くなった記念改造」の一環として,FileInspector を作った。当院では添書や写真などを /Volumes/Documents/{$id} というフォルダに保存して管理している。FileInspector では,そのフォルダの内容を表示する。 また,ファイルのクリックで qlmanager を呼び出して,QuickLook するようにした。
 ついでに,以前作った KickAppleScript.java を,ExecuteScript.java として改造,AppleScript は,ScriptManager を使って,コードを java ソースに組み込むようにした。

Quicklook1

Quicklook2

FileInspector のファイル名をクリックすると QuickLook でファイル内容を閲覧することができる

 

client/FileInspector.java

util/ExecuteScript.java

2011年1月19日 (水)

DiagnosisInspector 作成

新しい iMac になったら,画面がものすごく広くなったため(20": 1680x1050→ 27": 2560x1440),カルテの左側エリアにかなり余裕ができた。そこで,診断名を表示する DiagnosisInspector.java を作った。 インスペクタを開くときに,診断をサーバから読み込んで表示する。サーバとの通信はそのときだけで,その後は DiagnosisDocument から情報を受け取って表示するしくみ。

Diagnosisinspector

Windolphin

Macintosh の画面 Windows XP の画面

 

client/DiagnosisInspector.java

client/DiagnosisDocument.java

public static final String MAIN_DIAGNOSIS = "主病名";
public static final String SUSPECTED_DIAGNOSIS = "疑い病名";
 :
// DiagnosisInspector
DiagnosisInspector diagnosisInspector;
 :
private void initialize() {
 :
  // PatientInspector に新しく作った DiagnosisInspector と連絡
  diagnosisInspector = ((ChartImpl) getContext()).getDiagnosisInspector();
}
 :
public void start() {
 :
  // DiagnosisInspector に自分を教える
  diagnosisInspector.setDiagnosisDocument(this);
 :
public void getDiagnosisHistory(Date past) {
 :
    // 最後に有効期限(disUseDate)が99999999以外に設定されていたら移行病名としてセット
    for (Object rd : list) checkIkouByomei((RegisteredDiagnosisModel) rd);

    // 新しく作った DiagnosisInspector を update
    diagnosisInspector.update();
  }
 :
class DiagnosisPutTask extends DBTask> {
 :
  protected void succeeded(List list) {
 :
    // DiagnosisInspector に連絡
    diagnosisInspector.update();
  }

client/ChartImpl.java

/**
 * 病名インスペクタを返す
 */
public DiagnosisInspector getDiagnosisInspector() {
  return inspector.getDiagnosisInspector();
}

project/ProjectStub.java

public String getFifthInspector() {
  return prefs.get("fifthInspector", PatientInspector.INSPECTOR_ITEMS[5]); // 病名
}
public void setFifthInspector(String fifthInspector) {
   prefs.put("fifthInspector", fifthInspector);
}

client/PatientInspector.java

private boolean bDiagnosis;
private boolean bFile;
public static String[] INSPECTOR_ITEMS = new String[]{
  "メモ", "カレンダー", "文書履歴", "アレルギー", "身長体重", "病名", "関連文書", "なし"
};
// 病名インスペクタ
private DiagnosisInspector diagnosisInspector;
public DiagnosisInspector getDiagnosisInspector() {
  return diagnosisInspector;
}
// 関連文書インスペクタ → ついでに作った
private FileInspector fileInspector;
public FileInspector getFileInspector() {
  return fileInspector;
}
 :
private void initComponents() {
  String diagnosisTitle = "病名";
  String fileTitle = "関連文書";
  String fifthInspector = Project.getPreferences().get("fifthInspector", INSPECTOR_ITEMS[5]); //"病名"
  diagnosisInspector = new DiagnosisInspector(context);
  fileInspector = new FileInspector(context);
  diagnosisInspector.getPanel().setPreferredSize(new Dimension(prefW, 100));
  fileInspector.getPanel().setPreferredSize(new Dimension(prefW, 100));
 :
  layoutRow(container, fifthInspector);
  if (!bDiagnosis) {
    tabbedPane.addTab(diagnosisTitle, diagnosisInspector.getPanel());
  }
  if (!bFile) {
    tabbedPane.addTab(fileTitle, fileInspector.getPanel());
  }
 :
private void layoutRow(JPanel content, String itype) {
 :
  else if (itype.equals(INSPECTOR_ITEMS[5])) { // "病名"
    diagnosisInspector.getPanel().setBorder(BorderFactory.createTitledBorder("病名"));
    content.add(diagnosisInspector.getPanel());
    bDiagnosis = true;

  } else if (itype.equals(INSPECTOR_ITEMS[6])) { // "関連文書"
    fileInspector.getPanel().setBorder(BorderFactory.createTitledBorder("関連文書"));
    content.add(fileInspector.getPanel());
    bFile = true;
  }

client/KarteSettingPanel.java

private static int INSPECTOR_LENGTH = 5;
 :
initComponents() {
 :
  label = new JLabel("4番目:", SwingConstants.RIGHT);
  gbb.add(label, 0, row, 1, 1, GridBagConstraints.EAST);
  gbb.add(inspectorCompo[row], 1, row, 1, 1, GridBagConstraints.WEST);
  row++;
  label = new JLabel("ボトム:", SwingConstants.RIGHT);
  gbb.add(label, 0, row, 1, 1, GridBagConstraints.EAST);
  gbb.add(inspectorCompo[row], 1, row, 1, 1, GridBagConstraints.WEST);
  row++;
 :
private void bindModelToView() {
  inspectorCompo[4].setSelectedItem(model.getFifthInspector());
 :
private void bindViewToModel() {
  model.setFifthInspector((String) inspectorCompo[4].getSelectedItem());
 :
class KarteModel {
  private String fifthInspector;
 :
  public void populate(ProjectStub stub) {
    setFifthInspector(stub.getFifthInspector());
 :
  public void restore(ProjectStub stub) {
     stub.setFifthInspector(getFifthInspector());
 :
  public String getFifthInspector() {
    return fifthInspector;
  }
  public void setFifthInspector(String fifthInspector) {
    this.fifthInspector = fifthInspector;
  }

2011年1月17日 (月)

SaveDialog の改造

オリジナルでは,カルテ保存の時,保存するためのダイアログが2回出る。考えてみたらムダなので,1回ですむようにした。すっかり2回リターンを打つ癖が付いてしまっていたが,2〜3日で慣れた。

Savedialogold

Savedialognew

修正前:ダイアログが2回出る 修正後:2つを1つにまとめた

 

client/SaveDialog2.java

client/EditorFrame.java

public void close() {    
  if (mode == EditorMode.EDITOR) {
    if (editor.isDirty()) {
      // キャンセル,破棄の処理は editor でまとめてすることにした
      editor.save();
    } else {
      stop();
    }
  } else {
    stop();
  }
}

client/KarteEditor.java

private SaveParams getSaveParams(boolean joinAreaNetwork) {
 :
  SaveDialog2 sd = new SaveDialog2(parent);
 :
 :
public void save() {
 :
  int selection = params.getSelection();
  if (selection == SaveDialog2.SAVE || selection == SaveDialog2.TMP_SAVE) {
    save2(params);
  }
  else if (selection == SaveDialog2.DISPOSE) {
    // save 前に SAVE_DONE を送って dispose する
    boundSupport.firePropertyChange(KarteEditor.SAVE_DONE, false, true);
  }
 :

client/SaveParams.java

// 保存,一時保存,破棄,キャンセル
private int selection;
public int getSelection() {
  return selection;
}
public void setSelection(int selection) {
  this.selection = selection;
}

2011年1月16日 (日)

PatientSearchImpl の複数行選択対応

WatingListImpl.java の複数行選択対応に合わせて,PatientSearchImpl.java も複数行選択に対応させた。

plugin/PatientSearchImpl.java

変更箇所

private PatientModel[] selectedPatient;
public PatientModel[] getSelectedPatinet() 
public void setSelectedPatinet(PatientModel[] model) 
private void initComponents()  (SelectionModel の属性を設定)
private void controlMenu() 
private void canOpen()
public void exportSearchResult() 
private void connect() (addSelectionListener の部分)
public void openKarte() 
public void openKarte(PatientModel pm) 
public void addAsPvt() 
class AddAsPvtTask extends Task
class ContextListener 

WatingListImpl の複数行選択対応

WatingListImpl.java を複数行選択に対応させた。これで,ご家族で受診された場合,リストから複数行を選択してコマンド+O でカルテを開くと,いっぺんにカルテが開ける。さらにコマンド+_ で整列させることもできるので,とても便利になった。

plugin/WatingListImpl.java

変更箇所

private PatientVisitModel[] selectedPvt
private int[] saveSelectedIndex
private void initComponents()  (SelectionModel の属性を設定)
        pvtTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        pvtTable.setRowSelectionAllowed(true);
public PatientModel[] getPatient()
public PatientVisitModel[] getSelectedPvt()
public void setSelectedPvt(PatientVisitModel[] selectedPvt)
private void controlMenu()
private void canOpen()
private void connect() (addSelectionListener の部分)
public void openKarte()
public void cancelVisit()
public void setBusy(boolean busy)
class ContextListener

2011年1月14日 (金)

com.apple.eawt.Application の仕様変更

Java for Mac OS X 10.6 Update 3 から com.apple.eawt.Application の仕様がかわったようだ。
今まで addApplicationListenerとなっていたところを,個別に set するようになっている。

 

client/Dolphin.java

// Mac Application Menu
com.apple.eawt.Application fApplication = com.apple.eawt.Application.getApplication();

// ...について
fApplication.setAboutHandler(new com.apple.eawt.AboutHandler() {
  public void handleAbout(AboutEvent ae) {
    showAbout();
  }
});
// 終了
fApplication.setQuitHandler(new com.apple.eawt.QuitHandler() {
  public void handleQuitRequestWith(QuitEvent qe, QuitResponse qr) {
    processExit();
    qr.cancelQuit(); // processExit() で終了をキャンセルした場合
  }
});
// 環境設定
fApplication.setPreferencesHandler(new com.apple.eawt.PreferencesHandler() {
  public void handlePreferences(PreferencesEvent pe) {
    //ログイン画面の段階で,メニューから環境設定を選択すると,stateMgr = null のまま doPreference に入ってしまう
    if (stateMgr != null) doPreference();
  }
});

2011年1月13日 (木)

インスペクタを整列させる

患者さんがご家族で受診された場合など,インスペクタを3つくらい同時に開いて診察することがある。ウインドウメニューから,コマンドでインスペクタを整列できるようにした。

Arrange

 

helper/WindowSupport.java

// frame を整列させるときの初期位置と移動幅
public static int INITIAL_X = 256;
public static int INITIAL_Y = 40;
public static int INITIAL_DX = 192;
public static int INITIAL_DY = 20;

// プライベートコンストラクタ
private WindowSupport(JFrame frame, JMenuBar menuBar, JMenu windowMenu, Action windowAction) {
 ・
 ・
//pns^
  // インスペクタを整列するアクションだけはあらかじめ入れておく
  // こうしておかないと,1回 window メニューを開かないと accelerator が効かないことになる
  windowMenu.add(new ArrangeInspectorAction());
//pns$
}

public void menuSelected(MenuEvent e) {
 ・
 ・
      wm.add(action);
      count++;
    }
  }
  // インスペクタウインドウを整列する
  if (count != 0) {
    wm.addSeparator();
    count = 0;
    Action a = new ArrangeInspectorAction();
    wm.add(a);
  }
}
/**
 * インスペクタを整列する action
 */
class ArrangeInspectorAction extends AbstractAction {
  public ArrangeInspectorAction() {
    putValue(Action.NAME, "インスペクタを整列");
    putValue(Action.SMALL_ICON, GUIConst.ICON_WINDOWS_22);
    putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_UNDERSCORE, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
  }
  public void actionPerformed(ActionEvent e) {
    JFrame f;
    int x = INITIAL_X; int y = INITIAL_Y; int width = 0; int height = 0;

    for (WindowSupport ws : allWindows) {
      f = ws.getFrame();
      if (f.getTitle().contains("インスペクタ")) {
        if (width == 0) width = f.getBounds().width;
        if (height == 0) height = f.getBounds().height;

        f.setBounds(x, y, width, height);
        f.toFront();
        x += INITIAL_DX; y += INITIAL_DY;
      }
    }
  }
}

2011年1月 9日 (日)

新しい iMac に移行して焦る

新 iMac で診療開始後,いきなり KarteEditor のウインドウが表示されず焦ったが,何回か繰り返しているうちに,よく見るといつの間にか左上に小さく表示されていた。それをドラッグして大きくして,あとはその大きさと位置で表示されるようになった。要するに,初期ウインドウの大きさの設定に失敗していた。
 原因は,EditorFrame.java で logger を取得し忘れていたため,editorFrameBounds.xml がない場合のコード中の logger 出力のところで exception を出していた。こういうのは移行してみないと分からないバグである。

client/EditorFrame.java

public EditorFrame() {
//pns^
  logger = ClientContext.getBootLogger();
//pns$
  allEditorFrames.add(this);
}

ちなみに,ウインドウの大きさなどの情報は,Mac では ~/Library/Application Support/OpenDolphin に,Windows XP では ~/Application Data/DigitalGlobe/OpenDolphin,Windows 7 では ~/AppData/Roaming/DigitalGlobe/OpenDolphin に .xml ファイルとして入っている。

当院のコンピューター(更新)

受付

メインDELL Precision Workstation 340,Pen4/2.4G,Windows XP(中古)
ORCA クライアント,Dolphin クライアントが走っている。添書や検査結果等の文書はスキャナで取り込んで,全てファイルサーバに転送する。クライアントには個人情報等の大切なデータは残らないようになっている。このマシンは開院の大分前に中古で買ってから,1回も故障せず4〜5年働いてくれている。
サブ1FUJITSU FMV-ESPRIMO D5250, Pentium Dual Core/1.6G, Windows XP(中古)
事務が2名体制になったので,ORCA クライアントは2台になった。このマシンの前は Pentium 3 の Windows 2000 マシンを使っていたが,Windows 2000 のサポートが終了になってしまったのを機会に,新しく中古マシンを買った。ファンの音がとても静かでよい。
サブ2iMac 20" C2D/2.16G (Late 2006), Mac OS X (中古)
白いボディーの iMac。院内掲示板表示とBGM担当。iTunes を使って,無料インターネットラジオを BGM として流している。

 

診察室

医師用iMac 27" i5/2.8G (Mid 2010),Mac OS X (新品)
3年間使った iMac 20" C2D/2.0G (Mid 2007) から今年買い換えた。ORCA と Dolphin のクライアントを走らせている。臨床写真や添書,検査結果等の書類はファイルサーバに保存し,そのサーバをデスクトップにマウントして,OS のファインダで検索したり閲覧したりするようにしている。
看護師用NEC Versa Pro, Pen4/1.8G , Windows XP(中古)
ノートマシン。看護師さんがカルテを閲覧するのに使用。

 

院長室

メインiMac 24" C2D/2.66G (Early 2009), Mac OS X (新品)
iMac ずいぶん買っているなと思う。
サブ Macintosh SE
改造の限りを尽くし,現在は G3/233MHz の Mac OS 9 マシンとなっている。さすがに現在は置物になっている。

 

サーバ室

メインショップブランド C2D/2.53G,Ubuntu 8.04 (64bit) + Xen3.2
ORCA サーバ,Dolphin サーバ,ファイルサーバ(いずれも Ubuntu 8.04 32bit)を Xen の Dom-U として走らせている。各サーバは,イメージファイルではなく,物理パーティション上に構築してある。全ての重要なデータはサーバマシンに保存して,クライアントには残さないようにしている。個人情報が入るマシンなので,カギのかかるサーバ室に隠してある。開院後9ヶ月で購入して,その後故障知らず。
サブ1ショップブランド C2D/2.2G,Ubuntu 8.04 (64bit) + Xen3.2
毎日診療終了後に Xen の Dom-U を丸ごとバックアップしてある。メインが故障した場合,診察室のターミナルからこのマシンに入って Dom-U を立ち上げれば,メインと全く同じサーバが立ち上がるので,クライアント側でネットワーク等の設定変更を全くしなくてもすぐに診療継続できる。なお,バックアップとしては,毎日データベースの dump をとり,圧縮・暗号化して自宅サーバにも保管している。このマシンはメインで使っていたが,開院9ヶ月で1回メモリが壊れてサブマシンに降格した。
サブ2自作 Pen4/2.6G,Ubuntu 7.10 + Xen 3.1
一度メインマシンが故障してサブマシンで運用していたが,その時,これが壊れたらどうしようとすごく不安になったので,バックアップマシンを増やした。このマシンは普段スイッチは切ってあり,メインが故障してサブ1で運用するはめになった時だけ動かすことにした。オンラインレセプトもこのマシンで送信している。

« 2010年12月 | トップページ | 2011年2月 »