//
// ICSReader.java
//
/*
OME Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package loci.formats.in;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.zip.GZIPInputStream;
import loci.common.DateTools;
import loci.common.Location;
import loci.common.RandomAccessInputStream;
import loci.formats.FormatException;
import loci.formats.FormatReader;
import loci.formats.FormatTools;
import loci.formats.MetadataTools;
import loci.formats.meta.MetadataStore;
import ome.xml.model.primitives.PositiveFloat;
import ome.xml.model.primitives.PositiveInteger;
/**
* ICSReader is the file format reader for ICS (Image Cytometry Standard)
* files. More information on ICS can be found at http://libics.sourceforge.net
*
* TODO : remove sub-C logic once N-dimensional support is in place
* see http://dev.loci.wisc.edu/trac/java/ticket/398
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/in/ICSReader.java">Trac</a>,
* <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/in/ICSReader.java;hb=HEAD">Gitweb</a></dd></dl>
*
* @author Melissa Linkert melissa at glencoesoftware.com
*/
public class ICSReader extends FormatReader {
// -- Constants --
/** Newline characters. */
public static final String NL = "\r\n";
public static final String[] DATE_FORMATS = {
"EEEE, MMMM dd, yyyy HH:mm:ss",
"EEE dd MMMM yyyy HH:mm:ss",
"EEE MMM dd HH:mm:ss yyyy",
"EE dd MMM yyyy HH:mm:ss z",
"HH:mm:ss dd\\MM\\yy"
};
// key token value matching regexes within the "document" category.
//
// this table is alphabetized for legibility only.
//
// however it is important that the most qualified regex list goes first,
// e.g. { "a", "b" } must precede { "a" }.
private static final String[][] DOCUMENT_KEYS = {
{ "date" }, // the full key is "document date"
{ "document", "average" },
{ "document" },
{ "gmtdate" },
{ "label" }
};
// key token value matching regexes within the "history" category.
//
// this table is alphabetized for legibility only.
//
// however it is important that the most qualified regex list goes first,
// e.g. { "a", "b" } must precede { "a" }.
private static final String[][] HISTORY_KEYS = {
{ "a\\d" }, // the full key is "history a1", etc.
{ "acquisition", "acquire\\..*." },
{ "acquisition", "laserbox\\..*." },
{ "acquisition", "modules\\(.*." },
{ "acquisition", "objective", "position" },
{ "adc", "resolution" },
{ "atd_hardware", "ver" },
{ "atd_libraries", "ver" },
{ "atd_microscopy", "ver" },
{ "author" },
{ "averagecount" },
{ "averagequality" },
{ "beam", "zoom" },
{ "binning" },
{ "bits/pixel" },
{ "black", "level" },
{ "black", "level\\*" },
{ "black_level" },
{ "camera", "manufacturer" },
{ "camera", "model" },
{ "camera" },
{ "cfd", "holdoff" },
{ "cfd", "limit", "high" },
{ "cfd", "limit", "low" },
{ "cfd", "zc", "level" },
{ "channel\\*" },
{ "collection", "time" },
{ "cols" },
{ "company" },
{ "count", "increment" },
{ "created", "on" },
{ "creation", "date" },
{ "cube", "descriptio" }, // sic; not found in sample files
{ "cube", "description" }, // correction; not found in sample files
{ "cube", "emm", "nm" },
{ "cube", "exc", "nm" },
{ "cube" },
{ "date" },
{ "dategmt" },
{ "dead", "time", "comp" },
{ "desc", "exc", "turret" },
{ "desc", "emm", "turret" },
{ "detector", "type" },
{ "detector" },
{ "dimensions" },
{ "direct", "turret" },
{ "dither", "range" },
{ "dwell" },
{ "excitationfwhm" },
{ "experiment" },
{ "experimenter" },
{ "expon.", "order" },
{ "exposure" },
{ "exposure_time" },
{ "ext", "latch", "delay" },
{ "extents" },
{ "filterset", "dichroic", "name" },
{ "filterset", "dichroic", "nm" },
{ "filterset", "emm", "name" },
{ "filterset", "emm", "nm" },
{ "filterset", "exc", "name" },
{ "filterset", "exc", "nm" },
{ "filterset" },
{ "filter\\*" },
{ "firmware" },
{ "fret", "backgr\\d"},
{ "frametime" },
{ "gain" },
{ "gain\\d" },
{ "gain\\*" },
{ "gamma" },
{ "icsviewer", "ver" },
{ "ht\\*" },
{ "id" },
{ "illumination", "mode", "laser" },
{ "illumination", "mode" },
{ "image", "bigendian" },
{ "image", "bpp" },
{ "image", "form" }, // not found in sample files
{ "image", "physical_sizex" },
{ "image", "physical_sizey" },
{ "image", "sizex" },
{ "image", "sizey" },
{ "labels" },
{ "lamp", "manufacturer" },
{ "lamp", "model" },
{ "laser", "firmware" },
{ "laser", "manufacturer" },
{ "laser", "model" },
{ "laser", "power" },
{ "laser", "rep", "rate" },
{ "laser", "type" },
{ "laser\\d", "intensity" },
{ "laser\\d", "name" },
{ "laser\\d", "wavelength" },
{ "left" },
{ "length" },
{ "line", "compressio" }, // sic
{ "line", "compression" }, // correction; not found in sample files
{ "linetime" },
{ "magnification" },
{ "manufacturer" },
{ "max", "photon", "coun" }, // sic
{ "max", "photon", "count" }, // correction; not found in sample files
{ "memory", "bank" },
{ "metadata", "format", "ver" },
{ "microscope", "built", "on" },
{ "microscope", "name" },
{ "microscope" },
{ "mirror", "\\d" },
{ "mode" },
{ "noiseval" },
{ "no.", "frames" },
{ "objective", "detail" },
{ "objective", "immersion" },
{ "objective", "mag" },
{ "objective", "magnification" },
{ "objective", "na" },
{ "objective", "type" },
{ "objective", "workingdistance" },
{ "objective" },
{ "offsets" },
{ "other", "text" },
{ "passcount" },
{ "pinhole" },
{ "pixel", "clock" },
{ "pixel", "time" },
{ "pmt" },
{ "polarity" },
{ "region" },
{ "rep", "period" },
{ "repeat", "time" },
{ "revision" },
{ "routing", "chan", "x" },
{ "routing", "chan", "y" },
{ "rows" },
{ "scan", "borders" },
{ "scan", "flyback" },
{ "scan", "pattern" },
{ "scan", "pixels", "x" },
{ "scan", "pixels", "y" },
{ "scan", "pos", "x" },
{ "scan", "pos", "y" },
{ "scan", "resolution" },
{ "scan", "speed" },
{ "scan", "zoom" },
{ "scanner", "lag" },
{ "scanner", "pixel", "time" },
{ "scanner", "resolution" },
{ "scanner", "speed" },
{ "scanner", "xshift" },
{ "scanner", "yshift" },
{ "scanner", "zoom" },
{ "shutter\\d" },
{ "shutter", "type" },
{ "software" },
{ "spectral", "bin_definition" },
{ "spectral", "calibration", "gain", "data" },
{ "spectral", "calibration", "gain", "mode" },
{ "spectral", "calibration", "offset", "data" },
{ "spectral", "calibration", "offset", "mode" },
{ "spectral", "calibration", "sensitivity", "mode" },
{ "spectral", "central_wavelength" },
{ "spectral", "laser_shield" },
{ "spectral", "laser_shield_width" },
{ "spectral", "resolution" },
{ "stage", "controller" },
{ "stage", "firmware" },
{ "stage", "manufacturer" },
{ "stage", "model" },
{ "stage", "pos" },
{ "stage", "positionx" },
{ "stage", "positiony" },
{ "stage", "positionz" },
{ "stage_xyzum" },
{ "step\\d", "channel", "\\d" },
{ "step\\d", "gain", "\\d" },
{ "step\\d", "laser" },
{ "step\\d", "name" },
{ "step\\d", "pinhole" },
{ "step\\d", "pmt", "ch", "\\d" },
{ "step\\d", "shutter", "\\d" },
{ "step\\d" },
{ "stop", "on", "o'flow" },
{ "stop", "on", "time" },
{ "study" },
{ "sync", "freq", "div" },
{ "sync", "holdoff" },
{ "sync" },
{ "tac", "gain" },
{ "tac", "limit", "low" },
{ "tac", "offset" },
{ "tac", "range" },
{ "tau\\d" },
{ "tcspc", "adc", "res" },
{ "tcspc", "adc", "resolution" },
{ "tcspc", "approx", "adc", "rate" },
{ "tcspc", "approx", "cfd", "rate" },
{ "tcspc", "approx", "tac", "rate" },
{ "tcspc", "bh" },
{ "tcspc", "cfd", "holdoff" },
{ "tcspc", "cfd", "limit", "high" },
{ "tcspc", "cfd", "limit", "low" },
{ "tcspc", "cfd", "zc", "level" },
{ "tcspc", "clock", "polarity" },
{ "tcspc", "collection", "time" },
{ "tcspc", "count", "increment" },
{ "tcspc", "dead", "time", "enabled" },
{ "tcspc", "delay" },
{ "tcspc", "dither", "range" },
{ "tcspc", "left", "border" },
{ "tcspc", "line", "compression" },
{ "tcspc", "mem", "offset" },
{ "tcspc", "operation", "mode" },
{ "tcspc", "overflow" },
{ "tcspc", "pixel", "clk", "divider" },
{ "tcspc", "pixel", "clock" },
{ "tcspc", "routing", "x" },
{ "tcspc", "routing", "y" },
{ "tcspc", "scan", "x" },
{ "tcspc", "scan", "y" },
{ "tcspc", "sync", "divider" },
{ "tcspc", "sync", "holdoff" },
{ "tcspc", "sync", "rate" },
{ "tcspc", "sync", "threshold" },
{ "tcspc", "sync", "zc", "level" },
{ "tcspc", "tac", "gain" },
{ "tcspc", "tac", "limit", "high" },
{ "tcspc", "tac", "limit", "low" },
{ "tcspc", "tac", "offset" },
{ "tcspc", "tac", "range" },
{ "tcspc", "time", "window" },
{ "tcspc", "top", "border" },
{ "tcspc", "total", "frames" },
{ "tcspc", "total", "time" },
{ "tcspc", "trigger" },
{ "tcspc", "x", "sync", "polarity" },
{ "tcspc", "y", "sync", "polarity" },
{ "text" },
{ "time" },
{ "title" },
{ "top" },
{ "transmission" },
{ "trigger" },
{ "type" },
{ "units" },
{ "version" },
{ "wavelength\\*" },
{ "x", "amplitude" },
{ "y", "amplitude" },
{ "x", "delay" },
{ "y", "delay" },
{ "x", "offset" },
{ "y", "offset" },
{ "z", "\\(background\\)" }
};
// key token value matching regexes within the "layout" category.
//
// this table is alphabetized for legibility only.
//
// however it is important that the most qualified regex list goes first,
// e.g. { "a", "b" } must precede { "a" }.
private static final String[][] LAYOUT_KEYS = {
{ "coordinates" }, // the full key is "layout coordinates"
{ "order" },
{ "parameters" },
{ "real_significant_bits" },
{ "significant_bits" },
{ "significant_channels" },
{ "sizes" }
};
// key token value matching regexes within the "parameter" category.
//
// this table is alphabetized for legibility only.
//
// however it is important that the most qualified regex list goes first,
// e.g. { "a", "b" } must precede { "a" }.
private static final String[][] PARAMETER_KEYS = {
{ "allowedlinemodes" }, // the full key is "parameter allowedlinemodes"
{ "ch" },
{ "higher_limit" },
{ "labels" },
{ "lower_limit" },
{ "origin" },
{ "range" },
{ "sample_width", "ch" },
{ "sample_width" },
{ "scale" },
{ "units", "adc-units", "channels" },
{ "units", "adc-units", "nm" },
{ "units" }
};
// key token value matching regexes within the "representation" category.
//
// this table is alphabetized for legibility only.
//
// however it is important that the most qualified regex list goes first,
// e.g. { "a", "b" } must precede { "a" }.
private static final String[][] REPRESENTATION_KEYS = {
{ "byte_order" }, // the full key is "representation byte_order"
{ "compression" },
{ "format" },
{ "sign" }
};
// key token value matching regexes within the "sensor" category.
//
// this table is alphabetized for legibility only.
//
// however it is important that the most qualified regex list goes first,
// e.g. { "a", "b" } must precede { "a" }.
private static final String[][] SENSOR_KEYS = {
{ "model" }, // the full key is "sensor model"
{ "s_params", "channels" },
{ "s_params", "exphotoncnt" },
{ "s_params", "lambdaem" },
{ "s_params", "lambdaex" },
{ "s_params", "numaperture" },
{ "s_params", "pinholeradius" },
{ "s_params", "pinholespacing" },
{ "s_params", "refinxlensmedium" }, // sic; not found in sample files
{ "s_params", "refinxmedium" }, // sic; not found in sample files
{ "s_params", "refrinxlensmedium" },
{ "s_params", "refrinxmedium" },
{ "type" }
};
// key token value matching regexes within the "view" category.
//
// this table is alphabetized for legibility only.
//
// however it is important that the most qualified regex list goes first,
// e.g. { "a", "b" } must precede { "a" }.
private static final String[][] VIEW_KEYS = {
{ "view", "color", "lib", "lut" }, // the full key is
// "view view color lib lut"
{ "view", "color", "count" },
{ "view", "color", "doc", "scale" },
{ "view", "color", "mode", "rgb", "set" },
{ "view", "color", "mode", "rgb" },
{ "view", "color", "schemes" },
{ "view", "color", "view", "active" },
{ "view", "color" },
{ "view\\d", "alpha" },
{ "view\\d", "alphastate" },
{ "view\\d", "annotation", "annellipse" },
{ "view\\d", "annotation", "annpoint" },
{ "view\\d", "autoresize" },
{ "view\\d", "axis" },
{ "view\\d", "blacklevel" },
{ "view\\d", "color" },
{ "view\\d", "cursor" },
{ "view\\d", "dimviewoption" },
{ "view\\d", "gamma" },
{ "view\\d", "ignoreaspect" },
{ "view\\d", "intzoom" },
{ "view\\d", "live" },
{ "view\\d", "order" },
{ "view\\d", "port" },
{ "view\\d", "position" },
{ "view\\d", "saturation" },
{ "view\\d", "scale" },
{ "view\\d", "showall" },
{ "view\\d", "showcursor" },
{ "view\\d", "showindex" },
{ "view\\d", "size" },
{ "view\\d", "synchronize" },
{ "view\\d", "tile" },
{ "view\\d", "useunits" },
{ "view\\d", "zoom" },
{ "view\\d" },
{ "view" }
};
// These strings appeared in the former metadata field categories but are not
// found in the LOCI sample files.
//
// The former metadata field categories table did not save the context, i.e.
// the first token such as "document" or "history" and other intermediate
// tokens. The preceding tables such as DOCUMENT_KEYS or HISTORY_KEYS use
// this full context.
//
// In an effort at backward compatibility, these will be used to form key
// value pairs if key/value pair not already assigned and they match anywhere
// in the input line.
//
private static String[][] OTHER_KEYS = {
{ "cube", "descriptio" }, // sic; also listed in HISTORY_KEYS
{ "cube", "description" }, // correction; also listed in HISTORY_KEYS
{ "image", "form" }, // also listed in HISTORY_KEYS
{ "refinxlensmedium" }, // Could be a mispelling of "refrinxlensmedium";
// also listed in SENSOR_KEYS
{ "refinxmedium" }, // Could be a mispelling of "refinxmedium";
// also listed in SENSOR_KEYS
{ "scil_type" },
{ "source" }
};
// -- Fields --
/** Current filename. */
private String currentIcsId;
private String currentIdsId;
/** Flag indicating whether current file is v2.0. */
private boolean versionTwo;
/** Image data. */
private byte[] data;
/** Offset to pixel data. */
private long offset;
/** Whether or not the pixels are GZIP-compressed. */
private boolean gzip;
private GZIPInputStream gzipStream;
/** Whether or not the image is inverted along the Y axis. */
private boolean invertY;
/** Whether or not the channels represent lifetime histogram bins. */
private boolean lifetime;
/** Dimensional reordering for lifetime data */
private String labels;
/** The length of each channel axis. */
private Vector<Integer> channelLengths;
/** The type of each channel axis. */
private Vector<String> channelTypes;
private int prevImage;
private boolean hasInstrumentData = false;
private boolean storedRGB = false;
// -- Constructor --
/** Constructs a new ICSReader. */
public ICSReader() {
super("Image Cytometry Standard", new String[] {"ics", "ids"});
domains = new String[] {FormatTools.LM_DOMAIN, FormatTools.FLIM_DOMAIN,
FormatTools.UNKNOWN_DOMAIN};
hasCompanionFiles = true;
datasetDescription = "One .ics and possibly one .ids with a similar name";
}
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isSingleFile(String) */
public boolean isSingleFile(String id) throws FormatException, IOException {
// check if we have a v2 ICS file - means there is no companion IDS file
RandomAccessInputStream f = new RandomAccessInputStream(id);
boolean singleFile = f.readString(17).trim().equals("ics_version\t2.0");
f.close();
return singleFile;
}
/* @see loci.formats.IFormatReader#getDomains() */
public String[] getDomains() {
FormatTools.assertId(currentId, true, 1);
String[] domain = new String[] {FormatTools.GRAPHICS_DOMAIN};
if (getChannelDimLengths().length > 1) {
domain[0] = FormatTools.FLIM_DOMAIN;
}
else if (hasInstrumentData) {
domain[0] = FormatTools.LM_DOMAIN;
}
return domain;
}
/* @see loci.formats.IFormatReader#getChannelDimLengths() */
public int[] getChannelDimLengths() {
FormatTools.assertId(currentId, true, 1);
int[] len = new int[channelLengths.size()];
for (int i=0; i<len.length; i++) {
len[i] = channelLengths.get(i).intValue();
}
return len;
}
/* @see loci.formats.IFormatReader#getChannelDimTypes() */
public String[] getChannelDimTypes() {
FormatTools.assertId(currentId, true, 1);
return channelTypes.toArray(new String[channelTypes.size()]);
}
/* @see loci.formats.IFormatReader#isInterleaved(int) */
public boolean isInterleaved(int subC) {
FormatTools.assertId(currentId, true, 1);
return subC == 0 && core[0].interleaved;
}
/* @see loci.formats.IFormatReader#fileGroupOption(String) */
public int fileGroupOption(String id) throws FormatException, IOException {
return FormatTools.MUST_GROUP;
}
/**
* @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int)
*/
public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h)
throws FormatException, IOException
{
FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h);
int bpp = FormatTools.getBytesPerPixel(getPixelType());
int len = FormatTools.getPlaneSize(this);
int pixel = bpp * getRGBChannelCount();
int rowLen = FormatTools.getPlaneSize(this, w, 1);
int[] coordinates = getZCTCoords(no);
int[] prevCoordinates = getZCTCoords(prevImage);
if (!gzip) {
in.seek(offset + no * (long) len);
}
else {
long toSkip = (no - prevImage - 1) * (long) len;
if (gzipStream == null || no <= prevImage) {
FileInputStream fis = null;
toSkip = no * (long) len;
if (versionTwo) {
fis = new FileInputStream(currentIcsId);
fis.skip(offset);
}
else {
fis = new FileInputStream(currentIdsId);
toSkip += offset;
}
try {
gzipStream = new GZIPInputStream(fis);
}
catch (IOException e) {
// the 'gzip' flag is set erroneously
gzip = false;
in.seek(offset + no * (long) len);
gzipStream = null;
}
}
if (gzipStream != null) {
while (toSkip > 0) {
toSkip -= gzipStream.skip(toSkip);
}
data = new byte[len * (storedRGB ? getSizeC() : 1)];
int toRead = data.length;
while (toRead > 0) {
toRead -= gzipStream.read(data, data.length - toRead, toRead);
}
}
}
int sizeC = lifetime ? 1 : getSizeC();
if (!isRGB() && sizeC > 4 && channelLengths.size() == 1 && storedRGB) {
// channels are stored interleaved, but because there are more than we
// can display as RGB, we need to separate them
in.seek(offset +
(long) len * getIndex(coordinates[0], 0, coordinates[2]));
if (!gzip && data == null) {
data = new byte[len * getSizeC()];
in.read(data);
}
else if (!gzip && (coordinates[0] != prevCoordinates[0] ||
coordinates[2] != prevCoordinates[2]))
{
in.read(data);
}
for (int row=y; row<h + y; row++) {
for (int col=x; col<w + x; col++) {
System.arraycopy(data, bpp * ((no % getSizeC()) + sizeC *
(row * getSizeX() + col)), buf, bpp * (row * w + col), bpp);
}
}
}
else if (gzip) {
RandomAccessInputStream s = new RandomAccessInputStream(data);
readPlane(s, x, y, w, h, buf);
s.close();
}
else {
readPlane(in, x, y, w, h, buf);
}
if (invertY) {
byte[] row = new byte[rowLen];
for (int r=0; r<h/2; r++) {
int topOffset = r * rowLen;
int bottomOffset = (h - r - 1) * rowLen;
System.arraycopy(buf, topOffset, row, 0, rowLen);
System.arraycopy(buf, bottomOffset, buf, topOffset, rowLen);
System.arraycopy(row, 0, buf, bottomOffset, rowLen);
}
}
prevImage = no;
return buf;
}
/* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */
public String[] getSeriesUsedFiles(boolean noPixels) {
FormatTools.assertId(currentId, true, 1);
if (versionTwo) {
return noPixels ? null : new String[] {currentIcsId};
}
return noPixels ? new String[] {currentIcsId} :
new String[] {currentIcsId, currentIdsId};
}
/* @see loci.formats.IFormatReader#close(boolean) */
public void close(boolean fileOnly) throws IOException {
super.close(fileOnly);
if (!fileOnly) {
currentIcsId = null;
currentIdsId = null;
data = null;
versionTwo = false;
gzip = false;
invertY = false;
lifetime = false;
prevImage = 0;
hasInstrumentData = false;
storedRGB = false;
if (gzipStream != null) {
gzipStream.close();
}
gzipStream = null;
}
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
protected void initFile(String id) throws FormatException, IOException {
super.initFile(id);
LOGGER.info("Finding companion file");
String icsId = id, idsId = id;
int dot = id.lastIndexOf(".");
String ext = dot < 0 ? "" : id.substring(dot + 1).toLowerCase();
if (ext.equals("ics")) {
// convert C to D regardless of case
char[] c = idsId.toCharArray();
c[c.length - 2]++;
idsId = new String(c);
}
else if (ext.equals("ids")) {
// convert D to C regardless of case
char[] c = icsId.toCharArray();
c[c.length - 2]--;
icsId = new String(c);
}
if (icsId == null) throw new FormatException("No ICS file found.");
Location icsFile = new Location(icsId);
if (!icsFile.exists()) throw new FormatException("ICS file not found.");
LOGGER.info("Checking file version");
// check if we have a v2 ICS file - means there is no companion IDS file
RandomAccessInputStream f = new RandomAccessInputStream(icsId);
if (f.readString(17).trim().equals("ics_version\t2.0")) {
in = new RandomAccessInputStream(icsId);
versionTwo = true;
}
else {
if (idsId == null) throw new FormatException("No IDS file found.");
Location idsFile = new Location(idsId);
if (!idsFile.exists()) throw new FormatException("IDS file not found.");
currentIdsId = idsId;
in = new RandomAccessInputStream(currentIdsId);
}
f.close();
currentIcsId = icsId;
LOGGER.info("Reading metadata");
Double[] pixelSizes = null;
Double[] timestamps = null;
String[] units = null;
String[] axes = null;
int[] axisLengths = null;
String byteOrder = null, rFormat = null, compression = null;
// parse key/value pairs from beginning of ICS file
RandomAccessInputStream reader = new RandomAccessInputStream(icsId);
reader.seek(0);
reader.readString(NL);
String line = reader.readString(NL);
boolean signed = false;
StringBuffer textBlock = new StringBuffer();
double[] sizes = null;
Integer[] emWaves = null, exWaves = null;
Double[] stagePos = null;
String imageName = null, date = null, description = null;
Double magnification = null, lensNA = null, workingDistance = null;
String objectiveModel = null, immersion = null, lastName = null;
Hashtable<Integer, Double> gains = new Hashtable<Integer, Double>();
Hashtable<Integer, Double> pinholes = new Hashtable<Integer, Double>();
Hashtable<Integer, Integer> wavelengths = new Hashtable<Integer, Integer>();
Hashtable<Integer, String> channelNames = new Hashtable<Integer, String>();
String laserModel = null;
String laserManufacturer = null;
Double laserPower = null;
Double laserRepetitionRate = null;
String detectorManufacturer = null;
String detectorModel = null;
String microscopeModel = null;
String microscopeManufacturer = null;
String experimentType = null;
Double exposureTime = null;
String filterSetModel = null;
String dichroicModel = null;
String excitationModel = null;
String emissionModel = null;
while (line != null && !line.trim().equals("end") &&
reader.getFilePointer() < reader.length() - 1)
{
line = line.trim();
if (line.length() > 0) {
// split the line into tokens
String[] tokens = tokenize(line);
String token0 = tokens[0].toLowerCase();
String[] keyValue = null;
// version category
if (token0.equals("ics_version")) {
String value = concatenateTokens(tokens, 1, tokens.length);
addGlobalMeta(token0, value);
}
// filename category
else if (token0.equals("filename")) {
imageName = concatenateTokens(tokens, 1, tokens.length);
addGlobalMeta(token0, imageName);
}
// layout category
else if (token0.equals("layout")) {
keyValue = findKeyValue(tokens, LAYOUT_KEYS);
String key = keyValue[0];
String value = keyValue[1];
addGlobalMeta(key, value);
if (key.equalsIgnoreCase("layout sizes")) {
StringTokenizer t = new StringTokenizer(value);
axisLengths = new int[t.countTokens()];
for (int n=0; n<axisLengths.length; n++) {
try {
axisLengths[n] = Integer.parseInt(t.nextToken().trim());
}
catch (NumberFormatException e) {
LOGGER.debug("Could not parse axis length", e);
}
}
}
else if (key.equalsIgnoreCase("layout order")) {
StringTokenizer t = new StringTokenizer(value);
axes = new String[t.countTokens()];
for (int n=0; n<axes.length; n++) {
axes[n] = t.nextToken().trim();
}
}
else if (key.equalsIgnoreCase("layout significant_bits")) {
core[0].bitsPerPixel = Integer.parseInt(value);
}
}
// representation category
else if (token0.equals("representation")) {
keyValue = findKeyValue(tokens, REPRESENTATION_KEYS);
String key = keyValue[0];
String value = keyValue[1];
addGlobalMeta(key, value);
if (key.equalsIgnoreCase("representation byte_order")) {
byteOrder = value;
}
else if (key.equalsIgnoreCase("representation format")) {
rFormat = value;
}
else if (key.equalsIgnoreCase("representation compression")) {
compression = value;
}
else if (key.equalsIgnoreCase("representation sign")) {
signed = value.equals("signed");
}
}
// parameter category
else if (token0.equals("parameter")) {
keyValue = findKeyValue(tokens, PARAMETER_KEYS);
String key = keyValue[0];
String value = keyValue[1];
addGlobalMeta(key, value);
if (key.equalsIgnoreCase("parameter scale")) {
// parse physical pixel sizes and time increment
pixelSizes = splitDoubles(value);
}
else if (key.equalsIgnoreCase("parameter t")) {
// parse explicit timestamps
timestamps = splitDoubles(value);
}
else if (key.equalsIgnoreCase("parameter units")) {
// parse units for scale
units = value.split("\\s+");
}
if (getMetadataOptions().getMetadataLevel() !=
MetadataLevel.MINIMUM)
{
if (key.equalsIgnoreCase("parameter ch")) {
String[] names = value.split(" ");
for (int n=0; n<names.length; n++) {
channelNames.put(new Integer(n), names[n].trim());
}
}
}
}
// history category
else if (token0.equals("history")) {
keyValue = findKeyValue(tokens, HISTORY_KEYS);
String key = keyValue[0];
String value = keyValue[1];
addGlobalMeta(key, value);
Double doubleValue = null;
try {
doubleValue = new Double(value);
}
catch (NumberFormatException e) {
// ARG this happens a lot; spurious error in most cases
LOGGER.debug("Could not parse double value '{}'", value, e);
}
if (key.equalsIgnoreCase("history software") &&
value.indexOf("SVI") != -1) {
// ICS files written by SVI Huygens are inverted on the Y axis
invertY = true;
}
else if (key.equalsIgnoreCase("history date") ||
key.equalsIgnoreCase("history created on"))
{
if (value.indexOf(" ") > 0) {
date = value.substring(0, value.lastIndexOf(" "));
date = DateTools.formatDate(date, DATE_FORMATS);
}
}
else if (key.equalsIgnoreCase("history creation date")) {
date = DateTools.formatDate(value, DATE_FORMATS);
}
else if (key.equalsIgnoreCase("history type")) {
// HACK - support for Gray Institute at Oxford's ICS lifetime data
if (value.equalsIgnoreCase("time resolved") ||
value.equalsIgnoreCase("FluorescenceLifetime"))
{
lifetime = true;
}
experimentType = value;
}
else if (key.equalsIgnoreCase("history labels")) {
// HACK - support for Gray Institute at Oxford's ICS lifetime data
labels = value;
}
else if (getMetadataOptions().getMetadataLevel() !=
MetadataLevel.MINIMUM) {
if (key.equalsIgnoreCase("history") ||
key.equalsIgnoreCase("history text"))
{
textBlock.append(value);
textBlock.append("\n");
metadata.remove(key);
}
else if (key.startsWith("history gain")) {
Integer n = new Integer(0);
try {
n = new Integer(key.substring(12).trim());
n = new Integer(n.intValue() - 1);
}
catch (NumberFormatException e) { }
if (doubleValue != null) {
gains.put(n, doubleValue);
}
}
else if (key.startsWith("history laser") &&
key.endsWith("wavelength")) {
int laser =
Integer.parseInt(key.substring(13, key.indexOf(" ", 13))) - 1;
value = value.replaceAll("nm", "").trim();
try {
wavelengths.put(new Integer(laser), new Integer(value));
}
catch (NumberFormatException e) {
LOGGER.debug("Could not parse wavelength", e);
}
}
else if (key.equalsIgnoreCase("history Wavelength*")) {
String[] waves = value.split(" ");
for (int i=0; i<waves.length; i++) {
wavelengths.put(new Integer(i), new Integer(waves[i]));
}
}
else if (key.equalsIgnoreCase("history laser manufacturer")) {
laserManufacturer = value;
}
else if (key.equalsIgnoreCase("history laser model")) {
laserModel = value;
}
else if (key.equalsIgnoreCase("history laser power")) {
try {
laserPower = new Double(value); //TODO ARG i.e. doubleValue
}
catch (NumberFormatException e) { }
}
else if (key.equalsIgnoreCase("history laser rep rate")) {
String repRate = value;
if (repRate.indexOf(" ") != -1) {
repRate = repRate.substring(0, repRate.lastIndexOf(" "));
}
laserRepetitionRate = new Double(repRate);
}
else if (key.equalsIgnoreCase("history objective type") ||
key.equalsIgnoreCase("history objective"))
{
objectiveModel = value;
}
else if (key.equalsIgnoreCase("history objective immersion")) {
immersion = value;
}
else if (key.equalsIgnoreCase("history objective NA")) {
lensNA = doubleValue;
}
else if (key.equalsIgnoreCase
("history objective WorkingDistance")) {
workingDistance = doubleValue;
}
else if (key.equalsIgnoreCase("history objective magnification") ||
key.equalsIgnoreCase("history objective mag"))
{
magnification = doubleValue;
}
else if (key.equalsIgnoreCase("history camera manufacturer")) {
detectorManufacturer = value;
}
else if (key.equalsIgnoreCase("history camera model")) {
detectorModel = value;
}
else if (key.equalsIgnoreCase("history author") ||
key.equalsIgnoreCase("history experimenter"))
{
lastName = value;
}
else if (key.equalsIgnoreCase("history extents")) {
String[] lengths = value.split(" ");
sizes = new double[lengths.length];
for (int n=0; n<sizes.length; n++) {
try {
sizes[n] = Double.parseDouble(lengths[n].trim());
}
catch (NumberFormatException e) {
LOGGER.debug("Could not parse axis length", e);
}
}
}
else if (key.equalsIgnoreCase("history stage_xyzum")) {
String[] positions = value.split(" ");
stagePos = new Double[positions.length];
for (int n=0; n<stagePos.length; n++) {
try {
stagePos[n] = new Double(positions[n]);
}
catch (NumberFormatException e) {
LOGGER.debug("Could not parse stage position", e);
}
}
}
else if (key.equalsIgnoreCase("history stage positionx")) {
if (stagePos == null) {
stagePos = new Double[3];
}
stagePos[0] = new Double(value); //TODO doubleValue
}
else if (key.equalsIgnoreCase("history stage positiony")) {
if (stagePos == null) {
stagePos = new Double[3];
}
stagePos[1] = new Double(value);
}
else if (key.equalsIgnoreCase("history stage positionz")) {
if (stagePos == null) {
stagePos = new Double[3];
}
stagePos[2] = new Double(value);
}
else if (key.equalsIgnoreCase("history other text")) {
description = value;
}
else if (key.startsWith("history step") && key.endsWith("name")) {
Integer n = new Integer(key.substring(12, key.indexOf(" ", 12)));
channelNames.put(n, value);
}
else if (key.equalsIgnoreCase("history cube")) {
channelNames.put(new Integer(channelNames.size()), value);
}
else if (key.equalsIgnoreCase("history cube emm nm")) {
if (emWaves == null) {
emWaves = new Integer[1];
}
emWaves[0] = new Integer(value.split(" ")[1].trim());
}
else if (key.equalsIgnoreCase("history cube exc nm")) {
if (exWaves == null) {
exWaves = new Integer[1];
}
exWaves[0] = new Integer(value.split(" ")[1].trim());
}
else if (key.equalsIgnoreCase("history microscope")) {
microscopeModel = value;
}
else if (key.equalsIgnoreCase("history manufacturer")) {
microscopeManufacturer = value;
}
else if (key.equalsIgnoreCase("history Exposure")) {
String expTime = value;
if (expTime.indexOf(" ") != -1) {
expTime = expTime.substring(0, expTime.indexOf(" "));
}
exposureTime = new Double(expTime);
}
else if (key.equalsIgnoreCase("history filterset")) {
filterSetModel = value;
}
else if (key.equalsIgnoreCase("history filterset dichroic name")) {
dichroicModel = value;
}
else if (key.equalsIgnoreCase("history filterset exc name")) {
excitationModel = value;
}
else if (key.equalsIgnoreCase("history filterset emm name")) {
emissionModel = value;
}
}
}
// document category
else if (token0.equals("document")) {
keyValue = findKeyValue(tokens, DOCUMENT_KEYS);
String key = keyValue[0];
String value = keyValue[1];
addGlobalMeta(key, value);
}
// sensor category
else if (token0.equals("sensor")) {
keyValue = findKeyValue(tokens, SENSOR_KEYS);
String key = keyValue[0];
String value = keyValue[1];
addGlobalMeta(key, value);
if (getMetadataOptions().getMetadataLevel() !=
MetadataLevel.MINIMUM)
{
if (key.equalsIgnoreCase("sensor s_params LambdaEm")) {
String[] waves = value.split(" ");
emWaves = new Integer[waves.length];
for (int n=0; n<emWaves.length; n++) {
try {
emWaves[n] = new Integer((int) Double.parseDouble(waves[n]));
}
catch (NumberFormatException e) {
LOGGER.debug("Could not parse emission wavelength", e);
}
}
}
else if (key.equalsIgnoreCase("sensor s_params LambdaEx")) {
String[] waves = value.split(" ");
exWaves = new Integer[waves.length];
for (int n=0; n<exWaves.length; n++) {
try {
exWaves[n] = new Integer((int) Double.parseDouble(waves[n]));
}
catch (NumberFormatException e) {
LOGGER.debug("Could not parse excitation wavelength", e);
}
}
}
else if (key.equalsIgnoreCase("sensor s_params PinholeRadius")) {
String[] pins = value.split(" ");
int channel = 0;
for (int n=0; n<pins.length; n++) {
if (pins[n].trim().equals("")) continue;
try {
pinholes.put(new Integer(channel++), new Double(pins[n]));
}
catch (NumberFormatException e) {
LOGGER.debug("Could not parse pinhole", e);
}
}
}
}
}
// view category
else if (token0.equals("view")) {
keyValue = findKeyValue(tokens, VIEW_KEYS);
String key = keyValue[0];
String value = keyValue[1];
// handle "view view color lib lut Green Fire green", etc.
if (key.equalsIgnoreCase("view view color lib lut")) {
int index;
int redIndex = value.toLowerCase().lastIndexOf("red");
int greenIndex = value.toLowerCase().lastIndexOf("green");
int blueIndex = value.toLowerCase().lastIndexOf("blue");
if (redIndex > 0 && redIndex > greenIndex && redIndex > blueIndex) {
index = redIndex + "red".length();
}
else if (greenIndex > 0 &&
greenIndex > redIndex && greenIndex > blueIndex) {
index = greenIndex + "green".length();
}
else if (blueIndex > 0 &&
blueIndex > redIndex && blueIndex > greenIndex) {
index = blueIndex + "blue".length();
}
else {
index = value.indexOf(' ');
}
if (index > 0) {
key = key + ' ' + value.substring(0, index);
value = value.substring(index + 1);
}
}
// handle "view view color mode rgb set Default Colors" and
// "view view color mode rgb set blue-green-red", etc.
else if (key.equalsIgnoreCase("view view color mode rgb set")) {
int index = value.toLowerCase().lastIndexOf("colors");
if (index > 0) {
index += "colors".length();
}
else {
index = value.indexOf(' ');
}
if (index > 0) {
key = key + ' ' + value.substring(0, index);
value = value.substring(index + 1);
}
}
addGlobalMeta(key, value);
}
else {
LOGGER.debug("Unknown category " + token0);
}
}
line = reader.readString(NL);
}
reader.close();
hasInstrumentData = emWaves != null || exWaves != null || lensNA != null ||
stagePos != null || magnification != null || workingDistance != null ||
objectiveModel != null || immersion != null;
addGlobalMeta("history text", textBlock.toString());
LOGGER.info("Populating core metadata");
core[0].rgb = false;
core[0].dimensionOrder = "XY";
// find axis sizes
channelLengths = new Vector<Integer>();
channelTypes = new Vector<String>();
int bitsPerPixel = 0;
for (int i=0; i<axes.length; i++) {
if (i >= axisLengths.length) break;
if (axes[i].equals("bits")) {
bitsPerPixel = axisLengths[i];
while (bitsPerPixel % 8 != 0) bitsPerPixel++;
if (bitsPerPixel == 24 || bitsPerPixel == 48) bitsPerPixel /= 3;
}
else if (axes[i].equals("x")) {
core[0].sizeX = axisLengths[i];
}
else if (axes[i].equals("y")) {
core[0].sizeY = axisLengths[i];
}
else if (axes[i].equals("z")) {
core[0].sizeZ = axisLengths[i];
if (getDimensionOrder().indexOf("Z") == -1) {
core[0].dimensionOrder += "Z";
}
}
else if (axes[i].equals("t")) {
if (getSizeT() == 0) core[0].sizeT = axisLengths[i];
else core[0].sizeT *= axisLengths[i];
if (getDimensionOrder().indexOf("T") == -1) {
core[0].dimensionOrder += "T";
}
}
else {
if (core[0].sizeC == 0) core[0].sizeC = axisLengths[i];
else core[0].sizeC *= axisLengths[i];
channelLengths.add(new Integer(axisLengths[i]));
storedRGB = getSizeX() == 0;
core[0].rgb = getSizeX() == 0 && getSizeC() <= 4 && getSizeC() > 1;
if (getDimensionOrder().indexOf("C") == -1) {
core[0].dimensionOrder += "C";
}
if (axes[i].startsWith("c")) {
channelTypes.add(FormatTools.CHANNEL);
}
else if (axes[i].equals("p")) {
channelTypes.add(FormatTools.PHASE);
}
else if (axes[i].equals("f")) {
channelTypes.add(FormatTools.FREQUENCY);
}
else channelTypes.add("");
}
}
if (channelLengths.size() == 0) {
channelLengths.add(new Integer(1));
channelTypes.add(FormatTools.CHANNEL);
}
core[0].dimensionOrder =
MetadataTools.makeSaneDimensionOrder(getDimensionOrder());
if (getSizeZ() == 0) core[0].sizeZ = 1;
if (getSizeC() == 0) core[0].sizeC = 1;
if (getSizeT() == 0) core[0].sizeT = 1;
core[0].interleaved = isRGB();
core[0].indexed = false;
core[0].falseColor = false;
core[0].metadataComplete = true;
core[0].littleEndian = true;
// HACK - support for Gray Institute at Oxford's ICS lifetime data
if (lifetime && labels != null) {
int binCount = 0;
String newOrder = null;
if (labels.equalsIgnoreCase("t x y")) {
// nominal X Y Z is actually C X Y (which is X Y C interleaved)
newOrder = "XYCZT";
core[0].interleaved = true;
binCount = core[0].sizeX;
core[0].sizeX = core[0].sizeY;
core[0].sizeY = core[0].sizeZ;
core[0].sizeZ = 1;
}
else if (labels.equalsIgnoreCase("x y t")) {
// nominal X Y Z is actually X Y C
newOrder = "XYCZT";
binCount = core[0].sizeZ;
core[0].sizeZ = 1;
}
else {
LOGGER.debug("Lifetime data, unexpected 'history labels' " + labels);
}
if (newOrder != null) {
core[0].dimensionOrder = newOrder;
core[0].sizeC = binCount;
core[0].cLengths = new int[] {binCount};
core[0].cTypes = new String[] {FormatTools.LIFETIME};
}
}
// do not modify the Z, T, or channel counts after this point
core[0].imageCount = getSizeZ() * getSizeT();
if (!isRGB()) core[0].imageCount *= getSizeC();
if (byteOrder != null) {
String firstByte = byteOrder.split(" ")[0];
int first = Integer.parseInt(firstByte);
core[0].littleEndian = rFormat.equals("real") ? first == 1 : first != 1;
}
gzip = (compression == null) ? false : compression.equals("gzip");
if (versionTwo) {
String s = in.readString(NL);
while (!s.trim().equals("end")) s = in.readString(NL);
}
offset = in.getFilePointer();
int bytes = bitsPerPixel / 8;
if (bitsPerPixel < 32) core[0].littleEndian = !isLittleEndian();
boolean fp = rFormat.equals("real");
core[0].pixelType = FormatTools.pixelTypeFromBytes(bytes, signed, fp);
LOGGER.info("Populating OME metadata");
MetadataStore store = makeFilterMetadata();
MetadataTools.populatePixels(store, this, true);
// populate Image data
store.setImageName(imageName, 0);
if (date != null) store.setImageAcquiredDate(date, 0);
else MetadataTools.setDefaultCreationDate(store, id, 0);
if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
store.setImageDescription(description, 0);
// link Instrument and Image
String instrumentID = MetadataTools.createLSID("Instrument", 0);
store.setInstrumentID(instrumentID, 0);
store.setMicroscopeModel(microscopeModel, 0);
store.setMicroscopeManufacturer(microscopeManufacturer, 0);
store.setImageInstrumentRef(instrumentID, 0);
store.setExperimentID(MetadataTools.createLSID("Experiment", 0), 0);
store.setExperimentType(getExperimentType(experimentType), 0);
// populate Dimensions data
if (pixelSizes != null) {
for (int i=0; i<pixelSizes.length; i++) {
Double pixelSize = pixelSizes[i];
String axis = axes != null && axes.length > i ? axes[i] : "";
String unit = units != null && units.length > i ? units[i] : "";
if (axis.equals("x")) {
if (pixelSize > 0 && checkUnit(unit, "um", "microns")) {
store.setPixelsPhysicalSizeX(new PositiveFloat(pixelSize), 0);
}
}
else if (axis.equals("y")) {
if (pixelSize > 0 && checkUnit(unit, "um", "microns")) {
store.setPixelsPhysicalSizeY(new PositiveFloat(pixelSize), 0);
}
}
else if (axis.equals("z")) {
if (pixelSize > 0 && checkUnit(unit, "um", "microns")) {
store.setPixelsPhysicalSizeZ(new PositiveFloat(pixelSize), 0);
}
}
else if (axis.equals("t")) {
if (checkUnit(unit, "ms")) {
store.setPixelsTimeIncrement(1000 * pixelSize, 0);
}
}
}
}
else if (sizes != null) {
if (sizes.length > 0 && sizes[0] > 0) {
store.setPixelsPhysicalSizeX(new PositiveFloat(sizes[0]), 0);
}
if (sizes.length > 1) {
sizes[1] /= getSizeY();
if (sizes[1] > 0) {
store.setPixelsPhysicalSizeY(new PositiveFloat(sizes[1]), 0);
}
}
}
// populate Plane data
if (timestamps != null) {
for (int t=0; t<timestamps.length; t++) {
if (t >= getSizeT()) break; // ignore superfluous timestamps
if (timestamps[t] == null) continue; // ignore missing timestamp
double deltaT = timestamps[t];
if (Double.isNaN(deltaT)) continue; // ignore invalid timestamp
// assign timestamp to all relevant planes
for (int z=0; z<getSizeZ(); z++) {
for (int c=0; c<getEffectiveSizeC(); c++) {
int index = getIndex(z, c, t);
store.setPlaneDeltaT(deltaT, 0, index);
}
}
}
}
// populate LogicalChannel data
for (int i=0; i<getEffectiveSizeC(); i++) {
if (channelNames.containsKey(i)) {
store.setChannelName(channelNames.get(i), 0, i);
}
if (pinholes.containsKey(i)) {
store.setChannelPinholeSize(pinholes.get(i), 0, i);
}
if (emWaves != null && i < emWaves.length && emWaves[i].intValue() > 0)
{
store.setChannelEmissionWavelength(
new PositiveInteger(emWaves[i]), 0, i);
}
if (exWaves != null && i < exWaves.length && exWaves[i].intValue() > 0)
{
store.setChannelExcitationWavelength(
new PositiveInteger(exWaves[i]), 0, i);
}
}
// populate Laser data
Integer[] lasers = wavelengths.keySet().toArray(new Integer[0]);
Arrays.sort(lasers);
for (int i=0; i<lasers.length; i++) {
store.setLaserID(MetadataTools.createLSID("LightSource", 0, i), 0, i);
store.setLaserWavelength(
new PositiveInteger(wavelengths.get(lasers[i])), 0, i);
store.setLaserType(getLaserType("Other"), 0, i);
store.setLaserLaserMedium(getLaserMedium("Other"), 0, i);
store.setLaserManufacturer(laserManufacturer, 0, i);
store.setLaserModel(laserModel, 0, i);
store.setLaserPower(laserPower, 0, i);
store.setLaserRepetitionRate(laserRepetitionRate, 0, i);
}
if (lasers.length == 0 && laserManufacturer != null) {
store.setLaserID(MetadataTools.createLSID("LightSource", 0, 0), 0, 0);
store.setLaserType(getLaserType("Other"), 0, 0);
store.setLaserLaserMedium(getLaserMedium("Other"), 0, 0);
store.setLaserManufacturer(laserManufacturer, 0, 0);
store.setLaserModel(laserModel, 0, 0);
store.setLaserPower(laserPower, 0, 0);
store.setLaserRepetitionRate(laserRepetitionRate, 0, 0);
}
// populate FilterSet data
if (filterSetModel != null) {
store.setFilterSetID(MetadataTools.createLSID("FilterSet", 0, 0), 0, 0);
store.setFilterSetModel(filterSetModel, 0, 0);
String dichroicID = MetadataTools.createLSID("Dichroic", 0, 0);
String emFilterID = MetadataTools.createLSID("Filter", 0, 0);
String exFilterID = MetadataTools.createLSID("Filter", 0, 1);
store.setDichroicID(dichroicID, 0, 0);
store.setDichroicModel(dichroicModel, 0, 0);
store.setFilterSetDichroicRef(dichroicID, 0, 0);
store.setFilterID(emFilterID, 0, 0);
store.setFilterModel(emissionModel, 0, 0);
store.setFilterSetEmissionFilterRef(emFilterID, 0, 0, 0);
store.setFilterID(exFilterID, 0, 1);
store.setFilterModel(excitationModel, 0, 1);
store.setFilterSetExcitationFilterRef(exFilterID, 0, 0, 0);
}
// populate Objective data
if (objectiveModel != null) store.setObjectiveModel(objectiveModel, 0, 0);
if (immersion == null) immersion = "Other";
store.setObjectiveImmersion(getImmersion(immersion), 0, 0);
if (lensNA != null) store.setObjectiveLensNA(lensNA, 0, 0);
if (workingDistance != null) {
store.setObjectiveWorkingDistance(workingDistance, 0, 0);
}
if (magnification != null) {
store.setObjectiveCalibratedMagnification(magnification, 0, 0);
}
store.setObjectiveCorrection(getCorrection("Other"), 0, 0);
// link Objective to Image
String objectiveID = MetadataTools.createLSID("Objective", 0, 0);
store.setObjectiveID(objectiveID, 0, 0);
store.setImageObjectiveSettingsID(objectiveID, 0);
// populate Detector data
String detectorID = MetadataTools.createLSID("Detector", 0, 0);
store.setDetectorID(detectorID, 0, 0);
store.setDetectorManufacturer(detectorManufacturer, 0, 0);
store.setDetectorModel(detectorModel, 0, 0);
store.setDetectorType(getDetectorType("Other"), 0, 0);
for (Integer key : gains.keySet()) {
int index = key.intValue();
if (index < getEffectiveSizeC()) {
store.setDetectorSettingsGain(gains.get(key), 0, index);
store.setDetectorSettingsID(detectorID, 0, index);
}
}
// populate Experimenter data
if (lastName != null) {
String experimenterID = MetadataTools.createLSID("Experimenter", 0);
store.setExperimenterID(experimenterID, 0);
store.setExperimenterLastName(lastName, 0);
store.setExperimenterDisplayName(lastName, 0);
}
// populate StagePosition data
if (stagePos != null) {
for (int i=0; i<getImageCount(); i++) {
if (stagePos.length > 0) {
store.setPlanePositionX(stagePos[0], 0, i);
addGlobalMeta("X position for position #1", stagePos[0]);
}
if (stagePos.length > 1) {
store.setPlanePositionY(stagePos[1], 0, i);
addGlobalMeta("Y position for position #1", stagePos[1]);
}
if (stagePos.length > 2) {
store.setPlanePositionZ(stagePos[2], 0, i);
addGlobalMeta("Z position for position #1", stagePos[2]);
}
}
}
if (exposureTime != null) {
for (int i=0; i<getImageCount(); i++) {
store.setPlaneExposureTime(exposureTime, 0, i);
}
}
}
}
// -- Helper methods --
/*
* String tokenizer for parsing metadata. Splits on any white-space
* characters. Tabs and spaces are often used interchangeably in real-life ICS
* files.
*
* Also splits on 0x04 character which appears in "paul/csarseven.ics" and
* "paul/gci/time resolved_1.ics".
*
* Also respects double quote marks, so that
* Modules("Confocal C1 Grabber").BarrierFilter(2)
* is just one token.
*
* If not for the last requirement, the one line
* String[] tokens = line.split("[\\s\\x04]+");
* would work.
*/
private String[] tokenize(String line) {
List<String> tokens = new ArrayList<String>();
boolean inWhiteSpace = true;
boolean withinQuotes = false;
StringBuffer token = null;
for (int i = 0; i < line.length(); ++i) {
char c = line.charAt(i);
if (Character.isWhitespace(c) || c == 0x04) {
if (withinQuotes) {
// retain white space within quotes
token.append(c);
}
else if (!inWhiteSpace) {
// put out pending token string
inWhiteSpace = true;
if (token.length() > 0) {
tokens.add(token.toString());
token = null;
}
}
}
else {
if ('"' == c) {
// toggle quotes
withinQuotes = !withinQuotes;
}
if (inWhiteSpace) {
inWhiteSpace = false;
// start a new token string
token = new StringBuffer();
}
// build token string
token.append(c);
}
}
// put out any pending token strings
if (null != token && token.length() > 0) {
tokens.add(token.toString());
}
return tokens.toArray(new String[0]);
}
/* Given a list of tokens and an array of lists of regular expressions, tries
* to find a match. If no match is found, looks in OTHER_KEYS.
*/
String[] findKeyValue(String[] tokens, String[][] regexesArray) {
String[] keyValue = findKeyValueForCategory(tokens, regexesArray);
if (null == keyValue) {
keyValue = findKeyValueOther(tokens, OTHER_KEYS);
}
if (null == keyValue) {
String key = tokens[0];
String value = concatenateTokens(tokens, 1, tokens.length);
keyValue = new String[] { key, value };
}
return keyValue;
}
/*
* Builds a string from a list of tokens.
*/
private String concatenateTokens(String[] tokens, int start, int stop) {
StringBuffer returnValue = new StringBuffer();
for (int i = start; i < tokens.length && i < stop; ++i) {
returnValue.append(tokens[i]);
if (i < stop - 1) {
returnValue.append(' ');
}
}
return returnValue.toString();
}
/*
* Given a list of tokens and an array of lists of regular expressions, finds
* a match. Returns key/value pair if matched, null otherwise.
*
* The first element, tokens[0], has already been matched to a category, i.e.
* 'history', and the regexesArray is category-specific.
*/
private String[] findKeyValueForCategory(String[] tokens,
String[][] regexesArray) {
String[] keyValue = null;
int index = 0;
for (String[] regexes : regexesArray) {
if (compareTokens(tokens, 1, regexes, 0)) {
int splitIndex = 1 + regexes.length; // add one for the category
String key = concatenateTokens(tokens, 0, splitIndex);
String value = concatenateTokens(tokens, splitIndex, tokens.length);
keyValue = new String[] { key, value };
break;
}
++index;
}
return keyValue;
}
/* Given a list of tokens and an array of lists of regular expressions, finds
* a match. Returns key/value pair if matched, null otherwise.
*
* The first element, tokens[0], represents a category and is skipped. Look
* for a match of a list of regular expressions anywhere in the list of tokens.
*/
private String[] findKeyValueOther(String[] tokens, String[][] regexesArray) {
String[] keyValue = null;
for (String[] regexes : regexesArray) {
for (int i = 1; i < tokens.length - regexes.length; ++i) {
// does token match first regex?
if (tokens[i].toLowerCase().matches(regexes[0])) {
// do remaining tokens match remaining regexes?
if (1 == regexes.length || compareTokens(tokens, i + 1, regexes, 1)) {
// if so, return key/value
int splitIndex = i + regexes.length;
String key = concatenateTokens(tokens, 0, splitIndex);
String value = concatenateTokens(tokens, splitIndex, tokens.length);
keyValue = new String[] { key, value };
break;
}
}
}
if (null != keyValue) {
break;
}
}
return keyValue;
}
/*
* Compares a list of tokens with a list of regular expressions.
*/
private boolean compareTokens(String[] tokens, int tokenIndex,
String[] regexes, int regexesIndex) {
boolean returnValue = true;
int i, j;
for (i = tokenIndex, j = regexesIndex; j < regexes.length; ++i, ++j) {
if (i >= tokens.length || !tokens[i].toLowerCase().matches(regexes[j])) {
returnValue = false;
break;
}
}
return returnValue;
}
/** Splits the given string into a list of {@link Double}s. */
private Double[] splitDoubles(String v) {
StringTokenizer t = new StringTokenizer(v);
Double[] values = new Double[t.countTokens()];
for (int n=0; n<values.length; n++) {
String token = t.nextToken().trim();
try {
values[n] = new Double(token);
}
catch (NumberFormatException e) {
LOGGER.debug("Could not parse double value '{}'", token, e);
}
}
return values;
}
/** Verifies that a unit matches the expected value. */
private boolean checkUnit(String actual, String... expected) {
if (actual == null || actual.equals("")) return true; // undefined is OK
for (String exp : expected) {
if (actual.equals(exp)) return true; // unit matches expected value
}
LOGGER.debug("Unexpected unit '{}'; expected '{}'", actual, expected);
return false;
}
}