/******************************************************************************* * Copyright (c) 2007-2014 G. Weirich, A. Brögli and A. Häffner. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * rgw - initial API and implementation * rgw - 2014: Changes for Elexis 2.x ******************************************************************************/ package ch.elexis.molemax.data; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import ch.elexis.core.ui.UiDesk; import ch.elexis.core.data.activator.CoreHub; import ch.elexis.core.data.events.ElexisEventDispatcher; import ch.elexis.data.Patient; import ch.elexis.data.PersistentObject; import ch.elexis.data.Query; import ch.elexis.molemax.views.MolemaxPrefs; import ch.elexis.core.ui.util.Log; import ch.elexis.core.ui.util.SWTHelper; import ch.rgw.io.FileTool; import ch.rgw.tools.ExHandler; import ch.rgw.tools.StringTool; import ch.rgw.tools.TimeTool; import ch.rgw.tools.VersionInfo; /** * A Tracker is one image region with all its details and follow-ups * * @author Gerry * */ public class Tracker extends PersistentObject { private static final String TABLENAME = "CH_ELEXIS_MOLEMAX"; private static final String VERSION = "0.2.0"; private static final String createTable = "CREATE TABLE " + TABLENAME + " (" + "ID VARCHAR(25) primary key, " + "deleted CHAR(1) default '0', " + "patientID VARCHAR(25), " + "parentID VARCHAR(25), " + "date CHAR(8)," + "slot CHAR(3), " + "koord VARCHAR(40), " + // x-y-w-h-num-ext " ExtInfo BLOB, " + "lastupdate BIGINT);"; private static final String createIndex = " CREATE INDEX MLMX1 ON " + TABLENAME + " (patientID);"; private static final String insertVersion = "INSERT INTO " + TABLENAME + " (ID,koord) VALUES ('VERSION','" + VERSION + "');"; private static final String createDB = createTable + createIndex + insertVersion; private static final String updateDB011 = "ALTER TABLE " + TABLENAME + " ADD parentID VARCHAR(25);"; private static final String updateDB020 = "ALTER TABLE " + TABLENAME + " ADD lastupdate BIGINT;"; static Log log = Log.get("Molemax"); private static int[] map = { 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11 }; Image image = null; static { addMapping(TABLENAME, "PatientID=patientID", "ParentID=parentID", "Datum=S:D:date", "slot", "koord", "ExtInfo"); Tracker version = load("VERSION"); if (!version.exists()) { if (PersistentObject.tableExists(TABLENAME)) { createOrModifyTable("DROP TABLE " + TABLENAME); } createOrModifyTable(createDB); new MolemaxACL().initializeDefaults(CoreHub.acl); } VersionInfo vi = new VersionInfo(version.get("koord")); if ((vi.isOlder(VERSION))) { if (vi.isEqual(new VersionInfo("0.1.0"))) { createOrModifyTable(updateDB011); version.set("koord", VERSION); log.log("Update auf " + VERSION, Log.TRACE); } else if (vi.isEqual(new VersionInfo("0.1.1"))) { createOrModifyTable(updateDB020); version.set("koord", VERSION); log.log("Update auf " + VERSION, Log.TRACE); } else { SWTHelper.alert("Kann Molemax nicht starten", "Zu alte Version der Datenbank"); } } } /** * A Child Tracker: A detail within a region image * * @param p * patient * @param parent * parent tracker (that denotes the region) * @param date * date of sequence to add this detail. If null: today * @param slot * whicht region this detail belongs * @param pos * position of this detail within the region */ public Tracker(final Patient p, final Tracker parent, String date, final int slot, final Rectangle pos) { create(null); if (date == null) { date = new TimeTool().toString(TimeTool.DATE_GER); } set(new String[] { "PatientID", "ParentID", "Datum", "slot", "koord" }, new String[] { p.getId(), parent.getId(), date, Integer.toString(slot), makeFilename(pos, null) }); } /** * a parent tracker is one of the 12 regions to store. If in the directory * of the originating file are exactly 12 images with subseqeuntly ascending * sequence numbers in their filename, and this image is first of sequence, * and slot 0 is selected, then all 12 images can be loaded * * @param p * patient this image belongs to * @param date * date of the sequence this image belongs to * @param slot * which of the 12 basic regions this image belongs to * @param file * file in which the image resides */ public Tracker(final Patient p, final String date, final int slot, final File file) { StringBuilder sb = new StringBuilder(); sb.append(makeDescriptor(p, date, slot)); File dir = new File(sb.toString()); if ((!dir.exists()) && (!dir.mkdirs())) { SWTHelper .showError( "Schreibfehler", "Konnte Verzeichnis " + dir.getAbsolutePath() + " nicht erstellen. Speicherverzeichnis korrekt angegeben?"); return; } sb.append(File.separator); String ext = FileTool.getExtension(file.getName()); String fname = "base." + ext; sb.append(fname); File out = new File(sb.toString()); if (!FileTool.copyFile(file, out, FileTool.REPLACE_IF_EXISTS)) { SWTHelper.showError("I/O Fehler", "Kann das Bild nicht übertragen"); } else { create(null); set(new String[] { "PatientID", "ParentID", "Datum", "slot", "koord" }, new String[] { p.getId(), "NIL", date, Integer.toString(slot), fname }); } if (slot == 0) { Pattern pattern = Pattern.compile( "([a-z_\\-]+)([0-9]+)(\\.[a-z0-9]+)", Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(file.getName()); if (matcher.matches()) { File path = file.getParentFile(); String prefix = matcher.group(1); String val = matcher.group(2); int numlen = val.length(); String postfix = matcher.group(3); int v = Integer.parseInt(val); boolean bSerie = true; for (int i = v; i < v + 11; i++) { String tryname = prefix + StringTool.pad(StringTool.LEFT, '0', Integer.toString(i), numlen) + postfix; if (!new File(path, tryname).exists()) { bSerie = false; break; } } if (bSerie) { if (SWTHelper .askYesNo("Ganze Serie einlesen?", "Dieses Bild scheint das erste Bild einer Serie zu sein. Ganze Serie einlesen?")) { for (int i = v + 1; i < v + 12; i++) { String tryname = prefix + StringTool.pad(StringTool.LEFT, '0', Integer.toString(i), numlen) + postfix; int slotnr = map[i - v]; Tracker trytracker = new Tracker(p, date, slotnr, new File(path, tryname)); } ElexisEventDispatcher.fireSelectionEvent(p); // GlobalEvents.getInstance().fireSelectionEvent(p); } } } } } /** * Copy the file containing an image to the directory location it belongs * (according to the database information of this tracker). If a file for * the same tracker exists, the index portion of the filename is increased * * @param file * the file to import. Must contain a valid image * @return true on success */ public boolean setFile(final File file) { int idx = 0; File out; do { StringBuilder sb = new StringBuilder(); sb.append(makeFilename()).append("-").append(idx); String ext = FileTool.getExtension(file.getName()); sb.append(".").append(ext); out = new File(sb.toString()); idx++; } while (out.exists()); if (!FileTool.copyFile(file, out, FileTool.REPLACE_IF_EXISTS)) { SWTHelper.showError("I/O Fehler", "Kann das Bild nicht übertragen"); return false; } else { set("koord", out.getName()); return true; } } public void dispose() { if (image != null) { image.dispose(); image = null; } } private File findSequenceFile() { String path = makeFilename(); File file = new File(path); if (!file.exists()) { String fname = get("koord"); String[] flds = FileTool.getNakedFilename(fname).split("-"); String ext = FileTool.getExtension(fname); if (flds.length > 4) { String seq = flds[4]; if (seq.matches("[0-9]+")) { int is = Integer.parseInt(seq); while (is-- > 0) { try { fname = makeFilename(Integer.parseInt(flds[0]), Integer.parseInt(flds[1]), Integer.parseInt(flds[2]), Integer.parseInt(flds[3]), is, ext); set("koord", fname); path = makeFilename(); file = new File(path); if (file.exists() && file.canRead()) { return file; } } catch (Exception ex) { ExHandler.handle(ex); } } } } return null; } return file; } /** * Image des Bildes erzeugen. Achtung: dieses muss nach Gebrauch mit * dispose() wieder entsorgt werden. * * @return ein SWT-Image */ public Image createImage() { if (image != null) { return image; } File file = findSequenceFile(); if (file == null) { return null; } FileInputStream fis; try { fis = new FileInputStream(file); image = new Image(UiDesk.getDisplay(), fis); return image; } catch (FileNotFoundException e) { ExHandler.handle(e); return null; } catch (Exception ex) { ExHandler.handle(ex); return image; } } public Image createImageScaled(final Point size) { Image orig = createImage(); Image scaled = new Image(UiDesk.getDisplay(), orig.getImageData() .scaledTo(size.x, size.y)); return scaled; } public int getSlot() { return checkZero(get("slot")); } public Rectangle getBounds() { String koord = get("koord"); String[] k = koord.split("[\\.-]"); if (k.length > 3) { try { return new Rectangle(Integer.parseInt(k[0]), Integer.parseInt(k[1]), Integer.parseInt(k[2]), Integer.parseInt(k[3])); } catch (NumberFormatException nx) { ExHandler.handle(nx); } } else { Image img = createImage(); if (img != null) { ImageData imd = img.getImageData(); return new Rectangle(imd.x, imd.y, imd.width, imd.height); } } return null; } @Override public String getLabel() { TimeTool tt = new TimeTool(get("Datum")); return tt.toString(TimeTool.DATE_GER); } public static Tracker load(final String id) { return new Tracker(id); } protected Tracker(final String id) { super(id); } protected Tracker() { } @Override protected String getTableName() { return TABLENAME; } public Patient getPatient() { return Patient.load(get("PatientID")); } public Tracker getParent() { String parentID = checkNull(get("ParentID")); if (parentID.equals("NIL")) { return null; } Tracker ret = load(parentID); if (ret.isValid()) { return ret; } return null; } public String getDate() { return get("Datum"); } /** * compose a filename to the image file that belongs to this tracker * * @return */ String makeFilename() { StringBuilder ret = new StringBuilder(); Tracker parent = getParent(); String date = getDate(); if (parent != null) { date = parent.getDate(); } ret.append(makeDescriptor(getPatient(), date, getSlot())) .append(File.separator).append(get("koord")); return ret.toString(); } /** * Load all children and return as stack of images * * @param parent * the parent tracker whose children to load * @return a stack that contains the parent at its bottom and all the * children above ordered by date */ public static Tracker[] getImageStack(final Tracker parent) { if (parent == null) { return new Tracker[0]; } Query<Tracker> qbe = new Query<Tracker>(Tracker.class); qbe.add("PatientID", "=", parent.get("PatientID")); qbe.add("ParentID", "=", parent.getId()); List<Tracker> list = qbe.execute(); Collections.sort(list, new Comparator<Tracker>() { public int compare(final Tracker arg0, final Tracker arg1) { if ((arg0 != null) && (arg1 != null)) { TimeTool tt0 = new TimeTool(arg0.get("Datum")); TimeTool tt1 = new TimeTool(arg1.get("Datum")); int i = tt0.compareTo(tt1); if (i == 0) { Rectangle r0 = arg0.getBounds(); Rectangle r1 = arg1.getBounds(); if ((r0 != null) && (r1 != null)) { return SWTHelper.size(r0) - SWTHelper.size(r1); } } return i; } return 0; } }); Tracker[] ret = new Tracker[list.size() + 1]; for (int i = 1; i < ret.length; i++) { ret[i] = list.get(i - 1); } ret[0] = parent; return ret; } public static String getLastSequenceDate(final Patient pat) { Query<Tracker> qbe = new Query<Tracker>(Tracker.class); qbe.add("PatientID", "=", pat.getId()); qbe.add("ParentID", "=", "NIL"); List<Tracker> list = qbe.execute(); if (list.size() > 0) { Tracker ret = list.get(0); if (list.size() > 1) { TimeTool lastDate = new TimeTool( TimeTool.BEGINNING_OF_UNIX_EPOCH); TimeTool cmp = new TimeTool(); for (Tracker tracker : list) { cmp.set(tracker.get("Datum")); // System.out.println(cmp.dump()); // System.out.println(lastDate.dump()); if (cmp.isAfter(lastDate)) { lastDate.set(cmp); ret = tracker; } } } return ret.getDate(); } return null; } /** * Load the base Tracker for a given slot, patient and date * * @param patient * patient * @param date * date of sequence. If null: Any sequence * @param slot * image region * @return a Tracker for the requested patient and region. Might be null id * no such tracker exists */ public static Tracker loadBase(final Patient patient, final String date, final int slot) { Query<Tracker> qbe = new Query<Tracker>(Tracker.class); qbe.add("PatientID", "=", patient.getId()); qbe.add("slot", "=", Integer.toString(slot)); qbe.add("ParentID", "=", "NIL"); if (date != null) { qbe.add("Datum", "=", date); } List<Tracker> list = qbe.execute(); if (list.size() > 0) { Tracker ret = list.get(0); if ((date == null) && (list.size() > 1)) { TimeTool lastDate = new TimeTool( TimeTool.BEGINNING_OF_UNIX_EPOCH); TimeTool cmp = new TimeTool(); for (Tracker tracker : list) { cmp.set(tracker.get("Datum")); if (cmp.isAfter(lastDate)) { lastDate = cmp; ret = tracker; } } } return ret; } return null; } /** * Find the Tracker that is topmost at a given point * * @param slot * Array of all trackers to match * @param x * koordinate * @param y * koordinate * @return index of the last Tracker that contains the given point */ public static int getTrackerAtPoint(final Tracker[] slot, final int x, final int y) { for (int i = slot.length - 1; i >= 0; i--) { Rectangle rec = slot[i].getBounds(); if (rec.contains(x, y)) { return i; } } return 0; } /** * Find all Trackers that contain a given point * * @param slot * @param x * @param y * @return */ public static List<Tracker> getTrackersAtPoint(final Tracker[] slot, final int x, final int y) { ArrayList<Tracker> ret = new ArrayList<Tracker>(slot.length); for (int i = 1; i < slot.length; i++) { Rectangle rec = slot[i].getBounds(); if (rec == null) { continue; } if (rec.contains(x, y)) { ret.add(slot[i]); } } return ret; } /** * Create the full path used for images of the given slot * * @param p * Patient * @param date * date of sequence (if null: today) * @param slot * image slot * @return the full path of the directory where images of this slot are * stored */ public static String makeDescriptor(final Patient p, String date, final int slot) { if (date == null) { date = getLastSequenceDate(p); } StringBuilder ret = new StringBuilder(); ret.append(CoreHub.localCfg.get(MolemaxPrefs.BASEDIR, "")).append( File.separator); String name = p.getName(); ret.append(name.length() > 2 ? name.substring(0, 2) : name); String vname = p.getVorname(); ret.append(vname.length() > 2 ? vname.substring(0, 2) : vname); ret.append(p.getPatCode()).append(File.separator) .append(new TimeTool(date).toString(TimeTool.DATE_COMPACT)) .append(File.separator).append(slot); return ret.toString(); } /** * create a filename from koordinates and an extension * * @param x * @param y * @param w * @param h * @param ext * @return */ private static String makeFilename(final int x, final int y, final int w, final int h, final int seq, final String ext) { StringBuilder sb = new StringBuilder(); sb.append(x).append("-").append(y).append("-").append(w).append("-") .append(h).append("-").append(seq); if (ext != null) { sb.append(".").append(ext); } return sb.toString(); } private static String makeFilename(final Rectangle rec, final String ext) { StringBuilder sb = new StringBuilder(); sb.append(Integer.toString(rec.x)).append("-") .append(Integer.toString(rec.y)).append("-") .append(Integer.toString(rec.width)).append("-") .append(Integer.toString(rec.height)); if (ext != null) { sb.append(".").append(ext); } return sb.toString(); } public static void delete(Tracker[] myTracker) { if (myTracker != null) { for (Tracker t : myTracker) { if (t != null) { t.delete(); } } myTracker = new Tracker[0]; } } public static void dispose(Tracker[] myTracker) { if (myTracker != null) { for (Tracker t : myTracker) { if (t != null) { t.dispose(); } } myTracker = new Tracker[0]; } } /* * (non-Javadoc) * * @see ch.elexis.data.PersistentObject#delete() */ @Override public boolean delete() { if (image != null) { image.dispose(); image = null; } if (isValid()) { String fname = makeFilename(); File file = new File(fname); if (file.exists()) { file.delete(); } } return super.delete(); } @Override public boolean isValid() { if (getPatient().isValid()) { return super.isValid(); } return false; } @SuppressWarnings("unchecked") public void setInfoString(final String name, final String text) { Map extinfo = getMap("ExtInfo"); extinfo.put(name, text); setMap("ExtInfo", extinfo); } public String getInfoString(final String name) { Map extinfo = getMap("ExtInfo"); return checkNull((String) extinfo.get(name)); } }