« 2009年2月 | トップページ | 2009年5月 »

2009年3月

2009年3月27日 (金)

ORCA マスター登録ソフト

ORCA のマスターのうち,ユーザーが登録できる 001000001 - 001000999 の領域を設定するソフト。開院前にマスターを作るときに作ったソフトを手直ししたもの。当院では,001000xxx の xxx 部分が,100番台は内服1回,200番台は内服2回,300番台は内服3回,400番台は内服その他,500番台は屯用,600番台は外用,700番台は点眼,800番以降は部位と決めて,OpenDolphin もそれにあわせてカスタマイズしてある。大切なマスターを壊すこともできるソフトなので,実運用での使用は推奨しない。


ダウンロード UserMasterUtility.zip (3128.7K)

2009年3月25日 (水)

OpenDolphin をできるだけ安易に試す方法

OpenDolphin のシステムは,クライアントとサーバ(JBoss AS)からなっており,堅牢なシステムが構築できる反面,業者さんを介さずに自分だけで試してみるのが難しいという面もある。 そこで,Mac 1台で,できるだけ安易に OpenDolphin を試す方法を考えてみた。

Jbossas JBoss AS のダウンロードページから,JBoss AS 4.2.3.GA をダウンロードする。展開すると左のようなファイルが入っている。
Runshfile2 とりあえず,bin フォルダ内の run.sh をダブルクリックして立ち上げてみる。一度立ち上げることにより,必要なフォルダ類も初期化される。
Runsh_2 ターミナルが立ち上がって,起動画面が流れる。起動していることを確認したら,コントロール-C を押して,JBoss AS を終了する。
Ear 次に,デプロイ用のファイルをダウンロードして展開する。
ダウンロード OpenDolphin-JBoss4.2.3-HSQLDB-ear.zip (180.5K)
  • localDB.properties,localDB.script
    HSQLDB のデータファイル。テスト患者10人(患者番号 00001〜00010)と,サンプルスタンプをいくつか登録してある。
  • login-config.xml
    ログインで認証する際の設定ファイル
  • openDolphin-1.3.0.1.ear
    デプロイする OpenDolphin 本体。データベースとして JBoss AS に標準で入っている HSQLDB を使用するように設定してある。
Conf JBoss AS のフォルダを server/default/conf とたどって行って,もともと中に入っている login-config.xml を,OpenDolphin デプロイ用の login-config.xml と入れ替えコピーする。
Hsqldb JBoss AS のフォルダを server/default/data/hypersonic とたどって行って,もともと中に入っている localDB.properties と localDB.script を,OpenDolphin デプロイ用の localDB.properties,localDB.script と入れ替えコピーする。
Deploy JBoss AS のフォルダを server/default/deploy とたどって行って,中に openDolphin-1.3.0.1.ear をコピーする。
Runshfile2 JBoss AS の bin フォルダの中にある run.sh をダブルクリックする。
Runsh Terminal が立ち上がって,起動メッセージが流れる。しばらくすると,openDolphin がデプロイされて,JBoss AS が起動する。
Jboss_web_2 ブラウザで,http://localhost:8080/web-console にアクセスしてみると,OpenDolphin がデプロイされていることが確認できる。
Client クライアントソフトをダウンロードして展開する。
ダウンロード OpenDolphin-JBoss4.2.3-macosx-client.zip (17714.0K)
このクライアントは,Macintosh + JBoss AS 4.2.3 用に構築してある。OpenDolphin-1.3.0.1.app をダブルクリックする。
Client2 ログイン画面が立ち上がる。まずは,設定をクリック。
Client3 IPアドレスを localhost,ユーザIDを admin とする。
Client4 パスワードにも admin を入力して,ログインをクリック。
Client6_2 OpenDolphin 起動。まずは,患者検索のタブをクリックする。
Client7 登録されているテスト患者を検索してみる。ID 00001 を入力したところ。
Client8 右クリックして,受付登録を選択する。
Client9 靴アイコンをクリックするか,しばらく待っていると,受付リストに出てくる。これをクリックしてカルテ作成に入る。

上記は Macintosh の例だが,Windows でも多分できるのではないかと思う。Windows の場合,JBoss AS の起動には,run.sh ではなく,run.bat を使う。また,クライアントも Windows 用が必要。
Windows 用に構築してみたもの(Java 1.5用。動作についてはあまり試していない。)
ダウンロード OpenDolphin-JBoss4.2.3-win-client.zip (22707.1K)

OpenDolphin では,患者登録したりスタンプを作ったりするのに ORCA が必須なので,さらに色々試してみるためには ORCA サーバを立ち上げなければならない。これも VirtualBox を使うと,新しいマシンを準備しなくても,比較的安易に仮想 ORCA サーバを作ることができる[参照]。そうすると,Mac 1台で,OpenDolphin-ORCA のシステムを試用してみることも可能となる。これも,多分 Windows でも同じようにできるのではないかと思う。

2009年3月24日 (火)

VirtualBox に ORCA インストール

ORCA 簡単インストールというページがあって,簡単にインストールできる方法が公開されている。いろいろ面倒な設定をスクリプトがやってくれるので,驚異的に楽である。これを使わせていただいて,Ubuntu-ORCA を Macintosh の VirtualBox にインストールしてみた。

Vb1 VirtualBox を使うと,Intel チップの Macintosh で Linux を走らせることができる。
VirtualBox はここからダウンロードできる。
Vb2 まずは,メニューから「仮想メディアマネージャ」を選択。
「新規」をクリック。
Vb3 新規仮想ディスク作成ウィザードが立ち上がる。
Vb4 可変サイズで作成
Vb5 サイズは 10G で作ってみた。
Vb6 これで仮想ディスクは完成。
Vb7 次に,Ubuntu のインストール CD イメージを追加する。
ここで追加しておくと,CD に焼かなくてもイメージからインストールできる。
Vb11 CD/DVD イメージのタブを選んで,「追加」をクリックする。
Vb12 ダウンロードした,Ubuntu のインストール CD イメージを選択する。
Vb13 登録完了。次に,メニューから「仮想マシン」→「新規」を選んで,仮想マシン作成ウィザードを立ち上げる。
Vb21 仮想マシン作成ウィザード。
Vb22 名前と OS タイプの入力。
Vb23 メモリは 512M 割り当ててみた。
Vb24 さっき作った仮想ディスクを割り当てる。
Vb25 仮想マシン作成完了。
Vb311 「仮想マシン」→「設定」を選択。
「一般」→「高度」のタブを選択して,PAE/NX を有効化にチェックを入れる。
Vb312 ネットワークを選択,「割り当て」をホストインターフェースとする。
Vb32 いよいよ「起動」をクリックする。しかし,インストール CD のイメージから立ち上がらない。
Vb33 CD イメージをマウントしてやる必要がある。
Vb34 登録しておいた ubuntu のインストールイメージ CD を選択する。
Vb35 しかる後に,リセットする。
Vb41 これで,ubuntu のインストール CD が立ち上がる。
後は,ORCA 簡単インストールに公開されている方法をそのままなぞればインストールできる。
Vb57 ただし,ubuntuinst44.sh の 69行目がエラーになってしまう。/etc/init.d/cups restart となっている部分を,/etc/init.d/cupsys restart に変更する必要がある。
Vb65 インストール完了後,glclient はここに入っている。
Vb66 ホストは localhost にセット。ID,パスワードは両方とも ormaster。
Vb71 ORCA が立ち上がる。
OpenDolphin との接続実験のためには,さらに,デジタルグローブ社のホームページORCAと連携の記事の通り IP アドレスを設定したり,Postgres が外部からの接続を受け付けるように設定したりする必要がある。なお,この方法でインストールした ORCA の医療機関 ID は JPN000000000000 になっている。

OpenDolphin-1.3.0.1 ソース公開

変更が多岐にわたり,また,以前変更したところを更に見直して変更したところも増えてきたので,現時点の全ソースを公開しておくことにした。バージョンは,オリジナルの 1.3.0 にいろいろ手を加えたので,1.3.0.1 とした。オリジナルバージョンと区別するために,新しいアイコンも作ってみた。


    ※ このソースには 09/3/25 以降の変更は反映されていません。

検索機能強化

オリジナルの患者検索機能に加えて,メモ欄の検索機能,カルテ内容の検索機能を付けた。また,テーブルのヘッダをクリックすると,その項目でソートされるようにした。

Patientsearch01 患者検索は,オリジナルの 1.3.0 にも付いていた機能。漢字名,カタカナ名,ID,受診日等で検索できる。左は,カタカナ名「サトウ」で検索してみたところ。
Patientsearch02 1.3.0 のオリジナルにはソート機能が付いていなかったので(最新バージョンでは付いているようだ),TableSorter を組み込んでソートできるようにした。タイトルバーをクリックすると ID 順,漢字コード順,カタカナコード順,生年月日順にソートできる。左は,ID 番号でソートしたところ。
Patientsearch03 TableSorter のソート機能は辞書順に並べるので,年齢表示がされていると正しくソートされない。
生年月日順に並べるためには,まず年齢表示のチェックを外す。
Patientsearch04 年齢表示を消すと辞書順に並べても大丈夫。
Memosearch メモ検索のボタンを押すとメモ内容を検索できる。左は,メモに「看護師さん」と書いてあるカルテを検索したところ。
Kartesearch01 カルテ内検索は,試しにプログラムしてみたものの,結構時間がかかる。サーバに負荷もかかっていると思う。アマチュアプログラマーの限界か。カルテ内容のどこかにキーワードがひっかると,そのカルテをリストアップし,リストに上がった項目をダブルクリックすると,そのカルテが表示され,さらにカルテ内部を検索するという2段階の検索方法にした。
Kartesearch02 検索結果画面でリストに上がったカルテをダブルクリックすると,そのカルテが表示される。今度は,そのカルテ内容からキーワードを検索する。まず,メニューから「全てを選択」あるいは,コマンド−A をおすと,文書履歴が全部選択されて,全てのカルテが表示される。
Kartesearch03 メニューから「検索」を選ぶか,コマンド−F をおすと,キーワード入力画面がでる。キーワードを入力して検索開始をクリックする。
Kartesearch04 キーワードがある部分が画面の真ん中に来る。メニューから「次を検索」を選ぶか,コマンド-G を押すと,次の見つかったところが次々表示される。

開院後1年たったが,1年くらい真面目に通院している方のカルテは結構長くなってしまう。そういう方に,「大分前にもらった頭のクスリ下さい」と言われても,すぐ対応できるので便利になった。

変更または作成したコード(かなりいじったので変更内容までメモしきれない)
  • client/DocumentHistory.java
  • client/KarteDocumentViewer.java
  • client/FindAndView.java
  • client/FindDialog.java
  • client/FindDialogView.java
  • client/FindTableModel.java
  • table/TableSorter.java
  • table/ObjectReflectTableSorter.java
  • plugin/PatientSearchImpl.java
  • client/DocumentPeeker.java
  • delegater/DocumentPeekerDelegater.java
  • ejb/RemoteDocumentPeekerService.java
  • ejb/RemoteDocumentPeekerServiceImpl.java

状態アイコン表示色

状態アイコンの背景色を増やした。病名が付いていない場合,初診の場合に加えて,カルテ記載がされていない場合を加えた。状態に応じて背景の色を変えることで表示する。

Mainwindow 黄色:初診で病名がまだ付いていない。青:初診で病名ついた。橙:カルテ未記載。

患者さんをなるべくお待たせしないため,混んできたら,診察終了後とりあえず会計に必要なオーダーだけ ORCA に送って,所見などの記載はあとに回すことにしている。この日は,9時半ころまではちゃんと記載していたが,その後記載できなくなりオレンジマークが増えていることが分かる。あとで患者さんが切れてから,オレンジと黄色のカルテを記載していくことにしている。

client/DocumentPeeker.java の作成

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package open.dolphin.client;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.List;
import open.dolphin.delegater.DocumentDelegater;
import open.dolphin.dto.DiagnosisSearchSpec;
import open.dolphin.dto.DocumentSearchSpec;
import open.dolphin.infomodel.DocInfoModel;
import open.dolphin.infomodel.DocumentModel;
import open.dolphin.infomodel.IInfoModel;
import open.dolphin.infomodel.KarteBean;
import open.dolphin.infomodel.ModuleModel;
import open.dolphin.infomodel.PatientVisitModel;
import open.dolphin.infomodel.ProgressCourse;
import org.apache.log4j.Logger;

/**
 * ちょっとカルテ内容をチェックして pvt に必要な情報をセットする
 */
public class DocumentPeeker {

    private Logger logger;

    private DocumentDelegater ddl;
    private GregorianCalendar today;
    private GregorianCalendar yesterday;

    public DocumentPeeker() {
        today = new GregorianCalendar();
        ddl = new DocumentDelegater();
        yesterday = new GregorianCalendar();
        yesterday.add(GregorianCalendar.DATE, -1);
        yesterday.set(Calendar.HOUR_OF_DAY, 23);

        logger = ClientContext.getBootLogger();
    }

    public void setByomeiCount(PatientVisitModel pvt) {
        KarteBean karte = ddl.getKarte(pvt.getPatient().getId(), today.getTime());

        // PatientVisitModel に病名数をセットする
        Long karteId = karte.getId();
        DiagnosisSearchSpec spec = new DiagnosisSearchSpec();
        spec.setCode(DiagnosisSearchSpec.PATIENT_SEARCH);
        spec.setKarteId(karteId);
        int byomeiCount = ddl.getDiagnosisList(spec).size(); // 今までについている病名の総数
        int byomeiCountToday = 0;
        if (byomeiCount != 0) {
            spec.setFromDate(yesterday.getTime());
            byomeiCountToday = ddl.getDiagnosisList(spec).size(); // 今日ついた病名の総数
        }

        pvt.setByomeiCount(byomeiCount);
        pvt.setByomeiCountToday(byomeiCountToday);
        //logger.info("pt id = " + pvt.getPatientId());
        //logger.info("total byomei count = " + byomeiCount + " today byomei count = " + byomeiCountToday);
    }

    // カルテに pvt.getLeastKarteSize() 文字以下の記載しか無ければ empty karte と判定
    public void checkEmptyKarte(PatientVisitModel pvt) {

        KarteBean karte = ddl.getKarte(pvt.getPatient().getId(), today.getTime());

        Long karteId = karte.getId();
        DocumentSearchSpec spec = new DocumentSearchSpec();
        spec.setKarteId(karteId);
        spec.setFromDate(yesterday.getTime());
        spec.setDocType(IInfoModel.DOCTYPE_KARTE);
        spec.setIncludeModifid(false);
        spec.setCode(DocumentSearchSpec.DOCTYPE_SEARCH);
        spec.setAscending(false);

        List<DocInfoModel> docInfoList = (List<DocInfoModel>) ddl.getDocumentList(spec);

        pvt.setKarteEmpty(false);
        //logger.info("pvt.getPatientId() = " + pvt.getPatientId());
        //logger.info("docInfoList size = " + docInfoList.size());

        // 今日のカルテがない人はマークしない。今日のカルテを作った段階で,カルテ内容チェックする
        if (docInfoList.size() != 0) {
            // DocumentModule をとるための手続
            Long id = docInfoList.get(0).getDocPk();
            List<Long> docIds = new ArrayList<Long>();
            docIds.add(id);
            List<DocumentModel> docList = ddl.getDocuments(docIds);

            DocumentModel model = docList.get(0);
            Collection<ModuleModel> modules = model.getModules();
            String xml = null;
            if (modules != null) {
                for (ModuleModel bean : modules) {
                    String role = bean.getModuleInfo().getStampRole();

                    if (role.equals(IInfoModel.ROLE_SOA_SPEC))
                        xml = ((ProgressCourse) bean.getModel()).getFreeText();
                }
                if (extractText(xml).length() <= pvt.getLeasKarteSize()) pvt.setKarteEmpty(true);
            } else pvt.setKarteEmpty(true);
        }
    }

    // カルテの内容から text を抜き出す
    private String extractText(String xml) {
        StringBuffer buf = new StringBuffer();
        String head[] = xml.split("<text>");
        for (String str : head) {
            String tail[] = str.split("</text>");
            if (tail.length == 2) buf.append(tail[0].trim());
        }
        //logger.info("extractText: " + buf.toString());
        return buf.toString();
    }
}

plugin/WatingListImpl.java の編集

public class WatingListImpl extends AbstractMainComponent {
 ・
 ・
  private WatingListView view;
////↓
    private DocumentPeeker peeker = new DocumentPeeker();
    private final Color SHOSHIN_COLOR = new Color(180,220,240); //青っぽい色
    private final Color KARTE_EMPTY_COLOR = new Color(250,200,160); //茶色っぽい色
    private final Color DIAGNOSIS_EMPTY_COLOR = new Color(243,255,15); //黄色
////↑
 ・
 ・
protected class PvtChecker implements Runnable {
 ・
 ・
  // 結果を追加する
  if (newVisitCount > 0) {
    for (int i = 0; i < newVisitCount; i++) {
////↓ 受付番号表示
    PatientVisitModel pvt = (PatientVisitModel) result.get(i);
    pvt.setNumber(firstResult+i+1);

    // 病名が1つでもあるかどうか,初診かどうかを pvt にセット
    peeker.setByomeiCount(pvt);
    // カルテ記載されているかどうか
    peeker.checkEmptyKarte(pvt);
////↑
    dataList.add(result.get(i));
 ・
 ・
protected class PvtChecker2 implements Callable {
 ・
 ・
  //
  // cnt 以降は新しいレコードなのでそのまま追加する
  //
  for (int i = index; i < result.size(); i++) {

////↓  // 受付番号表示
    PatientVisitModel pvt = (PatientVisitModel) result.get(i);
    pvt.setNumber(firstResult+i+1);
    dataList.add(result.get(i));
    //dataList.add(result.get(index++));

    // 病名が1つでもあるかどうか,初診かどうかを pvt にセット
    peeker.setByomeiCount(pvt);
    // カルテ記載されているかどうか
    peeker.checkEmptyKarte(pvt);
////↑
 ・
 ・
protected class KarteStateRenderer extends DefaultTableCellRenderer {
    Color fore = pvt != null && pvt.getState() == ChartImpl.CANCEL_PVT ? CANCEL_PVT_COLOR : table.getForeground();
    this.setForeground(fore);
  }
            
////↓ 状態に応じて背景色を変更
  if (pvt != null) {
    // 初診
    if (pvt.isShoshin()) this.setBackground(SHOSHIN_COLOR);
    // カルテ記載がまだ
    if (pvt.isKarteEmpty()) this.setBackground(KARTE_EMPTY_COLOR);
    // 病名ついてない
    if (!pvt.hasByomei()) this.setBackground(DIAGNOSIS_EMPTY_COLOR);
  }
////↑

client/KarteEditor.java の編集

private void save2(final SaveParams params) throws DolphinException {
 ・
 ・
  //
  // SOAPane をダンプし model に追加する
  // 
  KartePaneDumper_2 dumper = new KartePaneDumper_2();
  KarteStyledDocument doc = (KarteStyledDocument) soaPane.getTextPane().getDocument();
  dumper.dump(doc);
  ModuleModel[] soa = dumper.getModule();
  if (soa != null && soa.length > 0) {
    logger.debug("soaPane dumped, number of SOA modules = " + soa.length);
    model.addModule(soa);
  } else {
    logger.debug("soaPane dumped, no module");
  }

////↓   カルテ記載があるかどうか記録。10文字以下だったら未記載と判定。
    PatientVisitModel pvt = this.getContext().getPatientVisit();
    if (doc.getLength() <= pvt.getLeasKarteSize()) pvt.setKarteEmpty(true);
    else pvt.setKarteEmpty(false);
////↑

client/DiagnosisDocument.java の編集

class DiagnosisPutTask extends DBTask> {
 ・
 ・
  @Override
  protected List<Long> doInBackground() throws Exception {
 ・
 ・
////↓ PatientVisitModel に病名数をセットする 
    new DocumentPeeker().setByomeiCount(getContext().getPatientVisit());
////↑

infomodel/PatientVisitModel.java の編集

 //カルテ記載がまだかどうか
    public void setKarteEmpty(boolean k) {
        karteEmpty = k;
    }
    public boolean isKarteEmpty() {
        return karteEmpty;
    }
    public int getLeasKarteSize() {
        return LEAST_KARTE_SIZE;
    }

2009年3月22日 (日)

ショートカットいろいろ

いろいろショートカットキーを付けた。

 

AllergyEditor.java

AllergyEditor で,コマンド-Wでウインドウを閉じる。
////    閉じるボタン追加
//// Object[] options = new Object[]{addBtn,clearBtn};
Object[] options = new Object[]{addBtn,clearBtn, "閉じる"};
        
JOptionPane pane = new JOptionPane(view,
    JOptionPane.PLAIN_MESSAGE,
    JOptionPane.DEFAULT_OPTION,
    null,
    options, addBtn);
dialog = pane.createDialog(inspector.getContext().getFrame(), ClientContext.getFrameTitle("アレルギー登録"));

////    command-w でウインドウクローズ
InputMap im = dialog.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.META_MASK);
im.put(key, "close-window");
dialog.getRootPane().getActionMap().put("close-window", new AbstractAction() {
  public void actionPerformed(ActionEvent e) {
    dialog.dispose();
  }
});

 

SaveDialog.java

CLAIM 送るかどうかをスペースキーで切り替える。
public SaveDialog(Window parent) {
 ・
 ・
////↓   SPACE で CLAIM 送信のチェックボックスの ON/OFF をする
  InputMap im = okButton.getInputMap(JComponent.WHEN_FOCUSED);
  KeyStroke spaceKey = KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0);
  // もともと,SPACE には okButton 'pressed' が割り当てられているので,これを削除する
  while (im != null ){
    im.remove(spaceKey);
    im = im.getParent();
  }
  // 登録
  im = dialog.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
  im.put(spaceKey, "toggle-claim");
  dialog.getRootPane().getActionMap().put("toggle-claim", new AbstractAction() {
    public void actionPerformed(ActionEvent e) {
      sendClaim.doClick();
    }
  });
////↑
}

 

plulgin/SchemaEditorImpl.java

// ショートカット登録
InputMap im = view.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
// command-z = undo
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.META_DOWN_MASK), "undo");
view.getRootPane().getActionMap().put("undo", new AbstractAction(){
  public void actionPerformed(ActionEvent e) {
    undoBtn.doClick();
  }
});
// escape = cancel
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "escape");
  view.getRootPane().getActionMap().put("escape", new AbstractAction(){
    public void actionPerformed(ActionEvent e) {
      cancelBtn.doClick();
    }
  });
// enter = カルテに展開
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "enter");
view.getRootPane().getActionMap().put("enter", new AbstractAction(){
  public void actionPerformed(ActionEvent e) {
    okBtn.doClick();
  }
});

2009年3月20日 (金)

メニューに項目を追加

メニューに新しい項目を追加する方法
  1. MacMenuFactory.java に項目を加える(2ヶ所)
    @Action
    public void selectAll() {
        chart.sendToChain("selectAll");
    }
    
    // SelectAll
    JMenuItem selectAll = new JMenuItem();
    selectAll.setName("selectAll");
    selectAll.setAction(actionMap.get("selectAll"));
    setAccelerator(selectAll, KeyEvent.VK_A);
    edit.add(selectAll);
    
  2. MacMenuFactory_ja.properties に日本語を追加
    selectAll.Action.text=全て選択
    
  3. GUIConst.java に項目を追加
    public static final String ACTION_SELECT_ALL = "selectAll";
    
  4. メニューの有効/無効の設定
    こんな風なところがあるクラスに項目を加える
    public void controlMenu() {
        chart.enabledAction(GUIConst.ACTION_SAVE, false); // 保存
        chart.enabledAction(GUIConst.ACTION_PRINT, false); // 印刷
        chart.enabledAction(GUIConst.ACTION_SELECT_ALL, true);
    
  5. メニューを有効にしたクラスで selectAll() を作成する。メニューから selectAll を選ぶと,selectAll() が呼ばれる。
    public void selectAll() {
        System.out.println("---- TODO ----");
    }
    
  6. component からキーを横取りする場合
    component.getInputMap().remove(KeyStroke.getKeyStroke('A', InputEvent.META_MASK));
    

2009年3月16日 (月)

Java 1.6対応

今年10月で Java 1.5 のサポートが終了するらしい。現在使用中の Dolphin サーバは JBoss AS 4.0.5.GA + Java 1.5である。何の問題もなく動いているし,サーバとは言っても非公開の院内サーバなので,職員にハッカーがいない限りセキュリティアップデートはどうでもいい。なので,サポート終了しても無視して 1.5 を使い続けるつもりであるが,どうしても 1.6 にしなければならなくなったときのために,JBoss AS 4.2.3.GA + Java 1.6 で動作させてみた。


サーバ

  • Ubuntu 8.04LTS server version を使用
  • Postgresql 8.3 と sun-java6-sdk を apt インストール
  • JBoss AS 4.2.3.GA-jdk6 をダウンロード,/usr/local/ に展開
  • リンクを張って JAVA_HOME=/usr/lib/jdk,JBOSS_HOME=/usr/local/jboss になるようにセットアップ
  • jboss_init_ubuntu.sh を作成して,/etc/init.d/jboss で start/stop できるようにセットアップ。JBOSS_HOST をきちんと設定しないとつながらない(4.0.5 のように 0.0.0.0 にするとつながらない)。
  • postgres-ds.xml,login-config.xml,ear ファイルはそのまま使える
  • /usr/local/jboss/server/default/lib に postgres jdbc ドライバ(postgresql-8.3-604.jdbc4.jar)を入れる


クライアント

  • /usr/local/jboss/client から以下のライブラリをクライアントの lib にコピー(アップデート)。
    • commons-logging.jar
    • ejb3-persistence.jar
    • hibernate-annotations.jar
    • hibernate-client.jar
    • jboss-annotations-ejb3.jar
    • jboss-ejb3-client.jar
    • jboss-ejbx.jar
    • jboss-j2ee.jar
    • jboss-system-client.jar
    • jbossall-client.jar
    • log4j.jar
    • mail.jar
実運用での実験はしていないが,とりあえず動いている。実運用での実験は,どうしても Java 1.6 にしなくてはならなくなった時に考える。

2009年3月12日 (木)

スタンプ入れ替えロック

Stampbox スタンプボックスの項目は,ドラッグアンドドロップで順番を入れ替えることができるが,カルテ入力で操作しているうちに操作ミスで知らずに順番を変えてしまって,いつものところにスタンプがなくて探すのに時間がかかることがあった。そこで,順番入れ替えは通常はロックしておき,必要なときだけロック解除して入れ替えることにした。

ロックしていてもカルテとのドラッグアンドドロップはできるので,普段はロックしておき,順番入れ替えの必要が生じたときのみロック解除して操作する。

client/StampTreePanel.java の編集

public StampTreePanel(StampTree tree) {
 ・
 ・
  if (treeEntity != null && (!treeEntity.equals(IInfoModel.ENTITY_TEXT))) {
    infoArea = new JTextArea();
    infoArea.setMargin(new Insets(3, 2, 3, 2));
    infoArea.setLineWrap(true);
    infoArea.setPreferredSize(new Dimension(250, 40));
    Font font = GUIFactory.createSmallFont();
    infoArea.setFont(font);

////↓ロックボタンを付ける
    final JToggleButton lockBtn = new JToggleButton();
    lockBtn.setIcon(new ImageIcon(getClass().getResource("/open/dolphin/resources/images/lockOn.gif")));
    lockBtn.setSelectedIcon(new ImageIcon(getClass().getResource("/open/dolphin/resources/images/lockOff.gif")));
    lockBtn.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 1));
    lockBtn.setMaximumSize(new java.awt.Dimension(32, 32));
    lockBtn.setMinimumSize(new java.awt.Dimension(32, 32));
    lockBtn.setPreferredSize(new java.awt.Dimension(32, 32));
    lockBtn.setToolTipText("ツリー内での入れ替えをロックします");

    lockBtn.addActionListener(new ActionListener() {
       public void actionPerformed(ActionEvent e) {
         if (lockBtn.isSelected()) { // 選択されていたらロック解除
             stampTree.setLocked(false);
         } else stampTree.setLocked(true);
       }
    });
            
    JPanel infoPanel = new JPanel();
    infoPanel.setLayout(new BorderLayout());
    infoPanel.add(infoArea, BorderLayout.CENTER);
    infoPanel.add(lockBtn, BorderLayout.EAST);
            
    this.add(infoPanel, BorderLayout.SOUTH);
    // this.add(infoArea, BorderLayout.SOUTH);
////↑

client/StampTree.java の編集

////↓ ツリー入れ替えロック用
public boolean isLocked() {
  return isLocked;
}
public void setLocked(boolean locked) {
  isLocked = locked;
}
////↑

client/StampTreeTransferHandler.java の編集

// ツリー入れ替えロック用
private boolean isLocked;
  public boolean isLocked() {
    return isLocked;
}
public void setLocked(boolean locked) {
  isLocked = locked;
}
// FlavorがStampTreeNodeの時
// StampTree 内の DnD
//
////↓スタンプ入れ替えが lock されていたら StampTree 内の DnD を禁止
if (tr.isDataFlavorSupported(stampTreeNodeFlavor) && (selected != null) && !isLocked) {
////↑
//if (tr.isDataFlavorSupported(stampTreeNodeFlavor) && (selected != null)) {

2009年3月 9日 (月)

Mac のメニューバー

Macintosh では,メニューは画面の上端のメニュー領域に表示される。そのため,メニューを持たないスタンプウインドウやシェーマウインドウを選ぶと,メニューが消えてしまう。消えても実用上問題はないのだが,どうも気になる。これを消えないようにするため,StampBox と SchemaBox (ImageBox) にもメニューを組み込んだ。

 

ImageBox.java の編集

private void initComponent() {
 ・
 ・
////↓   mac で SchemaBox にもメニューバーを出す
  if (isMac) {
    WindowSupport windowSupport = WindowSupport.create(title);
    frame = windowSupport.getFrame();
    javax.swing.JMenuBar myMenuBar = windowSupport.getMenuBar();
    mediator = new Mediator(this);
    AbstractMenuFactory appMenu = AbstractMenuFactory.getFactory();

    // mainWindow の menuSupport をセットしておけばメニュー処理は mainWindow がしてくれる
    appMenu.setMenuSupports(getContext().getMenuSupport(), mediator);
    appMenu.build(myMenuBar);
    mediator.registerActions(appMenu.getActionMap());
    mediator.disableAllMenus();
    String[] enables = new String[]{
      GUIConst.ACTION_SHOW_STAMPBOX,
    };
    mediator.enableMenus(enables);
  } else frame = new JFrame(title);
////↑
 ・
 ・

StampBoxPlugin.java の編集

public void start() {
 ・
 ・ 
///↓   mac で StampBox にもメニューバーを出す
  if (isMac) {
     WindowSupport windowSupport = WindowSupport.create(title);
     frame = windowSupport.getFrame();
     javax.swing.JMenuBar myMenuBar = windowSupport.getMenuBar();
     mediator = new Mediator(this);
     AbstractMenuFactory appMenu = AbstractMenuFactory.getFactory();

     // mainWindow の menuSupport をセットしておけばメニュー処理は mainWindow がしてくれる
     appMenu.setMenuSupports(getContext().getMenuSupport(), mediator);
     appMenu.build(myMenuBar);
     mediator.registerActions(appMenu.getActionMap());
     mediator.disableAllMenus();
     String[] enables = new String[]{
       GUIConst.ACTION_SHOW_SCHEMABOX,
     };
     mediator.enableMenus(enables);
  } else frame = new JFrame(title);
////↑
 ・
 ・

2009年3月 6日 (金)

初診・再診の自動入力

カルテ新規作成時に,初診・再診を自動入力するようにする。当院での特異的な各種条件を判定して,スタンプの名前で適切なスタンプを選ぶしくみ。

client/LastVisit.java の作成

package open.dolphin.client;

import java.util.GregorianCalendar;

/**
 * 最終受診日を調べる DocumentHistory のテーブルキメ打ちしているので,降順にしてないとダメ
 *   今日受診していたら今日の日付,今日受診していない場合は History の最終受診日
 *   今日受診していても,History の最終受診日を知りたい場合は InHistory を使う
 */
public class LastVisit {

    private Chart context;

    public LastVisit (Chart context) {
        this.context = context;
    }

    /*
     * DocumentHistory 上の LastVisit (表の左上スミをキメ打ち) 2008-02-01 形式
     */
    public String getLastVisitInHistory() {
        DocumentHistoryView dhv = (DocumentHistoryView) context.getDocumentHistory().getPanel();
        return (String) dhv.getTable().getModel().getValueAt(0, 0); // 表の左上スミをキメ打ち 
    }

    public int[] getLastVisitInHistoryYmd() {
        int[] lastVisitYmd = {2008,2,1};
        String lastVisit = getLastVisitInHistory();
        if (lastVisit == null) return null;

        lastVisitYmd[0] = Integer.valueOf(lastVisit.substring(0,4));
        lastVisitYmd[1] = Integer.valueOf(lastVisit.substring(5,7)) - 1; // gc では month は 0 から始まるので注意
        lastVisitYmd[2] = Integer.valueOf(lastVisit.substring(8));
        return lastVisitYmd;
    }

    public GregorianCalendar getLastVisitInHistoryGc() {
        int[] lv = getLastVisitInHistoryYmd();
        return new GregorianCalendar(lv[0],lv[1],lv[2]);
    }

    /*
     * 最終受診日を 2008-02-01 形式で返す
     */
    public String getLastVisit() {
        // pvt: 今日受診していたら今日の日付,受診していなければ null が返る。PvgDateTrimはTimeが日付でDateが時間
        String pvt = context.getPatientVisit().getPvtDateTrimTime();
        // 今日の受診がない場合,DocumentHistory の table から読んでくる。今日の受診がある場合は,今日をセット
        return pvt == null? getLastVisitInHistory() : pvt;
    }
    
    /*
     * 最終受診日を,int 配列 int[0]=year, int[1]=month, int[2]=day で返す (month は 0-11)
     */
    public int[] getLastVisitYmd() {
        int[] lastVisitYmd = {2008,2,1};
        String lastVisit = getLastVisit();

        lastVisitYmd[0] = Integer.valueOf(lastVisit.substring(0,4));
        lastVisitYmd[1] = Integer.valueOf(lastVisit.substring(5,7)) - 1; // gc では month は 0 から始まるので注意
        lastVisitYmd[2] = Integer.valueOf(lastVisit.substring(8));
        return lastVisitYmd;
    }

    /*
     * 最終受診日を gregorian calendar で返す
     */
    public GregorianCalendar getLastVisitGc() {
        int lv[] = {2008,2,1,12,00};
        String pvtDate = context.getPatientVisit().getPvtDateTrimTime();
        String pvtTime = context.getPatientVisit().getPvtDateTrimDate();

        // 受診していないのに新規カルテを作成した場合,暫定的に受診日を今日とする(あり得ない操作)
        if (pvtDate == null || pvtTime == null) {
            logger.info("new karte created without patient visit");
            return new GregorianCalendar();
        }
        lv[0] = Integer.valueOf(pvtDate.substring(0,4)); //year
        lv[1] = Integer.valueOf(pvtDate.substring(5,7)) - 1; //month
        lv[2] = Integer.valueOf(pvtDate.substring(8)); //day
        lv[3] = Integer.valueOf(pvtTime.substring(0,2)); //hour
        lv[4] = Integer.valueOf(pvtTime.substring(3)); //minute
        return new GregorianCalendar(lv[0],lv[1],lv[2],lv[3],lv[4]);
    }
}

client/StampTreeModules.java の作成

package open.dolphin.client;

import java.util.Calendar;
import java.util.GregorianCalendar;
import open.dolphin.delegater.StampDelegater;
import open.dolphin.infomodel.IInfoModel;
import open.dolphin.infomodel.ModuleInfoBean;
import open.dolphin.infomodel.ModuleModel;
import open.dolphin.infomodel.StampModel;
import open.dolphin.util.BeanUtils;
import org.apache.log4j.Logger;

/**
 * StampTree から ModuleModel を取り出す
 * @author pinus
 */
public class StampTreeModules {

    private Logger logger;
    
    private final int SHOSHIN = 0;
    private final int SHOSHIN_RYOTAN = 1;
    private final int SHOSHIN_YAKAN = 2;
    private final int SHOSHIN_RYOTAN_YAKAN = 3;
    private final int SAISHIN = 4;
    private final int SAISHIN_YAKAN = 5;
    private final String[] SHOSHIN_SAISHIN = {"初診", "初診(療養担当)", "初診(夜間・早朝等)", 
        "初診(療養担当)(夜間・早朝等)", "再診(診療所)", "再診(診療所)(夜間・早朝等)"};
    
    private Chart context;
    private ChartMediator mediator;
    private GregorianCalendar today;
    private LastVisit lv;

    public StampTreeModules(Chart context) {
        this.context = context;
        mediator = context.getChartMediator();
        today = new GregorianCalendar();
        lv = new LastVisit(context);

        logger = ClientContext.getBootLogger();
    }

    /*
     * 初診・再診を判定して,module として返す
     */
    public ModuleModel getBaseCharge() {
        /*
         * 初診か再診か判定
         * 初診,初診(療養担当),初診(夜間・早朝等),初診(療養担当)(夜間・早朝等),再診(診療所),再診(診療所)(夜間・早朝等)
         */

        StampTree tree =  mediator.getStampTree("baseChargeOrder");

        // 初診・再診の情報判定
        int index = 0;
        if (isShoshin()) {
            if (isRyotan()) {
                if (isYakan()) index = SHOSHIN_RYOTAN_YAKAN;
                else index = SHOSHIN_RYOTAN;
            } else {
                if (isYakan()) index = SHOSHIN_YAKAN;
                else index = SHOSHIN;
            }
        } else {
            if (isYakan()) index = SAISHIN_YAKAN;
            else index = SAISHIN;
        }

        logger.info("initial stamp = " + SHOSHIN_SAISHIN[index]);

        // スタンプ選択
        ModuleModel mModel = new ModuleModel();
        ModuleInfoBean stamp = null;

        for (int i=0; i<tree.getRowCount(); i++) {
            StampTreeNode sn = (StampTreeNode) tree.getPathForRow(i).getLastPathComponent();
            if (sn.isLeaf()) {
                ModuleInfoBean bean = sn.getStampInfo();
                String name = bean.getStampName();
                if (name.equals(SHOSHIN_SAISHIN[index])) {
                    stamp = bean;
                    break;
                }
            }
        }

        if (stamp != null) {
            StampDelegater sdl = new StampDelegater();
            // Stamp モデルをデータベースから取ってくる
            StampModel sModel = sdl.getStamp(stamp.getStampId());
            // Stamp モデルから info モデルを作る
            IInfoModel iModel = (IInfoModel) BeanUtils.xmlDecode(sModel.getStampBytes());
            // info モデル(実体)と stamp(情報) を module model にセット
            mModel.setModel(iModel);
            mModel.setModuleInfo(stamp);
        } else mModel = null;
        
        return mModel;
    }

    private boolean isShoshin() {
        int[] lastVisit = lv.getLastVisitInHistoryYmd();

        // 受診歴が無ければ初診で返す
        if (lastVisit == null) return true;

        // 受診歴がある場合は,3ヶ月たったかどうか判定
        int dif_year = today.get(Calendar.YEAR) - lastVisit[0];
        int dif_month = today.get(Calendar.MONTH) - lastVisit[1];
        int dif_day = today.get(Calendar.DATE) - lastVisit[2];

        // 2年以上離れていたら初診
        if (dif_year >= 2) return true;
        // 1年差の場合は月差を調整
        if (dif_year == 1) dif_month = dif_month + 12;
        // 4ヶ月以上離れていたら初診
        if (dif_month >= 4) return true;
        // 3ヶ月離れている場合は,日付を比較
        else if (dif_month == 3) {
            if (dif_day >= 0) return true;
            else return false;
        // それ以外は再診
        } else return false;
    }

    private boolean isRyotan() {
        int month = today.get(Calendar.MONTH);
        // 5月〜10月は療養担当手当なし
        if (month >=4 & month <=9) return false;
        return true;
    }

    private boolean isYakan() {
        // 土曜日で 12時以降なら夜間
        GregorianCalendar gc = lv.getLastVisitGc();
        if ((gc.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) &
                (gc.get(Calendar.AM_PM) == Calendar.PM)) return true;
        else return false;
    }
}

client/ChartImpl.java の編集

public DocumentModel getKarteModelToEdit(NewKarteParams params) {
 ・
 ・
  // カルテモデルを生成する
  DocumentModel model = new DocumentModel();

////↓   初診・再診 stampTreeModule を自動入力する
  ModuleModel mm = new StampTreeModules(this).getBaseCharge();
  if (mm != null) model.addModule(mm);
////↑
 ・
 ・
private void copyModel(DocumentModel oldModel, DocumentModel newModel, boolean applyRp) {

  //
  // 前回処方を適用する場合
  //
  if (applyRp) {
    Collection<ModuleModel> modules = oldModel.getModules();
    if (modules != null) {
      Collection<ModuleModel> apply = new ArrayList<ModuleModel>(5);
////↓  初診・再診 stampTreeModule を自動入力する
      ModuleModel mm = new StampTreeModules(this).getBaseCharge();
      if (mm != null) apply.add(mm);
////↑
 ・
 ・
    //
    // 処方かどうかを判定する
    //
    if (((ClaimBundle) model).getClassCode().startsWith("2")) {
      apply.add(bean);
    }
////↓ ついでに,処置もコピー
    if (((ClaimBundle) model).getClassCode().startsWith("4")) {
      apply.add(bean);
    }
////↑

2009年3月 1日 (日)

シェーマエディタの改造

Drawtest
  • ツールのレイアウト,カーソルの形をお絵かきソフト風に変更
  • 色と線の太さを1クリックで選択できるようにパレットにした
  • 鉛筆ツールの追加:ドラッグで自由な線,クリックで点,Option-クリックで複数の点を描画
  • 消しゴムツールの追加(不透明の白で上書きしてるだけ)
スクリーンショットは Macintosh (quaqua laf) のものだが,Windows でも使えるようだった。

 

インストール方法

plugin フォルダから SchemaEditor.jar を取り出して,代わりに DrawTest.jar を入れる。

 

DrawTest.jar とソースのダウンロード

ダウンロード DrawTest.zip (611.6K)

« 2009年2月 | トップページ | 2009年5月 »