/*
* #%L
* OME Bio-Formats package for reading and converting biological file formats.
* %%
* Copyright (C) 2005 - 2015 Open Microscopy Environment:
* - Board of Regents of the University of Wisconsin-Madison
* - Glencoe Software, Inc.
* - University of Dundee
* %%
* 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, see
* <http://www.gnu.org/licenses/gpl-2.0.html>.
* #L%
*/
package loci.formats.in;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import loci.common.DataTools;
import loci.common.Location;
import loci.common.RandomAccessInputStream;
import loci.common.xml.BaseHandler;
import loci.common.xml.XMLTools;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
import loci.formats.FormatReader;
import loci.formats.FormatTools;
import loci.formats.MetadataTools;
import loci.formats.meta.MetadataStore;
import loci.formats.tiff.IFD;
import loci.formats.tiff.TiffParser;
import ome.units.UNITS;
import ome.units.quantity.Length;
import ome.xml.model.primitives.NonNegativeInteger;
import ome.xml.model.primitives.PositiveFloat;
import ome.xml.model.primitives.PositiveInteger;
import org.xml.sax.Attributes;
/**
* OperettaReader is the file format reader for PerkinElmer Operetta data.
*
* @author Melissa Linkert melissa at glencoesoftware.com
*/
public class OperettaReader extends FormatReader {
// -- Constants --
private static final String XML_FILE = "Index.idx.xml";
private static final int XML_TAG = 65500;
// -- Fields --
private Plane[][] planes;
private MinimalTiffReader reader;
// -- Constructor --
/** Constructs a new Operetta reader. */
public OperettaReader() {
super("PerkinElmer Operetta", new String[] {"tif", "tiff", "xml"});
domains = new String[] {FormatTools.HCS_DOMAIN};
suffixSufficient = false;
datasetDescription =
"Directory with XML file and one .tif/.tiff file per plane";
}
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#getRequiredDirectories(String[]) */
@Override
public int getRequiredDirectories(String[] files)
throws FormatException, IOException
{
return 1;
}
/* @see loci.formats.IFormatReader#isSingleFile(String) */
@Override
public boolean isSingleFile(String id) throws FormatException, IOException {
return false;
}
/* @see loci.formats.IFormatReader#fileGroupOption(String) */
@Override
public int fileGroupOption(String id) throws FormatException, IOException {
return FormatTools.MUST_GROUP;
}
/* @see loci.formats.IFormatReader#isThisType(String, boolean) */
@Override
public boolean isThisType(String name, boolean open) {
String localName = new Location(name).getName();
if (localName.equals(XML_FILE)) {
return true;
}
Location parent = new Location(name).getAbsoluteFile().getParentFile();
Location xml = new Location(parent, XML_FILE);
if (!xml.exists()) {
return false;
}
return super.isThisType(name, open);
}
/* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
@Override
public boolean isThisType(RandomAccessInputStream stream) throws IOException {
TiffParser p = new TiffParser(stream);
IFD ifd = p.getFirstIFD();
if (ifd == null) return false;
Object s = ifd.getIFDValue(XML_TAG);
if (s == null) return false;
String xml = s instanceof String[] ? ((String[]) s)[0] : s.toString();
return xml.indexOf("Operetta") < 1024;
}
/* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */
@Override
public String[] getSeriesUsedFiles(boolean noPixels) {
FormatTools.assertId(currentId, true, 1);
ArrayList<String> files = new ArrayList<String>();
files.add(currentId);
for (Plane p : planes[getSeries()]) {
files.add(p.filename);
}
return files.toArray(new String[files.size()]);
}
/* @see loci.formats.IFormatReader#close(boolean) */
@Override
public void close(boolean fileOnly) throws IOException {
super.close(fileOnly);
if (!fileOnly) {
if (reader != null) {
reader.close();
}
reader = null;
planes = null;
}
}
/**
* @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int)
*/
@Override
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);
if (getSeries() < planes.length && no < planes[getSeries()].length) {
Plane p = planes[getSeries()][no];
if (new Location(p.filename).exists()) {
if (reader == null) {
reader = new MinimalTiffReader();
}
reader.setId(p.filename);
reader.openBytes(0, buf, x, y, w, h);
reader.close();
}
}
return buf;
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
@Override
protected void initFile(String id) throws FormatException, IOException {
// make sure that we have the XML file and not a TIFF file
if (!checkSuffix(id, "xml")) {
Location parent = new Location(id).getAbsoluteFile().getParentFile();
Location xml = new Location(parent, XML_FILE);
if (!xml.exists()) {
throw new FormatException("Could not find XML file " +
xml.getAbsolutePath());
}
initFile(xml.getAbsolutePath());
return;
}
else {
super.initFile(id);
}
// parse plate layout and image dimensions from the XML file
String xmlData = DataTools.readFile(id);
OperettaHandler handler = new OperettaHandler();
XMLTools.parseXML(xmlData, handler);
// sort the list of images by well and field indices
ArrayList<Plane> planeList = handler.getPlanes();
ArrayList<Integer> uniqueRows = new ArrayList<Integer>();
ArrayList<Integer> uniqueCols = new ArrayList<Integer>();
ArrayList<Integer> uniqueFields = new ArrayList<Integer>();
ArrayList<Integer> uniqueZs = new ArrayList<Integer>();
ArrayList<Integer> uniqueTs = new ArrayList<Integer>();
ArrayList<Integer> uniqueCs = new ArrayList<Integer>();
for (Plane p : planeList) {
if (!uniqueRows.contains(p.row)) {
uniqueRows.add(p.row);
}
if (!uniqueCols.contains(p.col)) {
uniqueCols.add(p.col);
}
if (!uniqueFields.contains(p.field)) {
uniqueFields.add(p.field);
}
if (!uniqueZs.contains(p.z)) {
uniqueZs.add(p.z);
}
if (!uniqueCs.contains(p.c)) {
uniqueCs.add(p.c);
}
if (!uniqueTs.contains(p.t)) {
uniqueTs.add(p.t);
}
}
Integer[] rows = uniqueRows.toArray(new Integer[uniqueRows.size()]);
Integer[] cols = uniqueCols.toArray(new Integer[uniqueCols.size()]);
Integer[] fields = uniqueFields.toArray(new Integer[uniqueFields.size()]);
Integer[] zs = uniqueZs.toArray(new Integer[uniqueZs.size()]);
Integer[] cs = uniqueCs.toArray(new Integer[uniqueCs.size()]);
Integer[] ts = uniqueTs.toArray(new Integer[uniqueTs.size()]);
Arrays.sort(rows);
Arrays.sort(cols);
Arrays.sort(fields);
Arrays.sort(zs);
Arrays.sort(ts);
Arrays.sort(cs);
int seriesCount = rows.length * cols.length * fields.length;
core.clear();
planes = new Plane[seriesCount][zs.length * cs.length * ts.length];
int nextSeries = 0;
for (int row=0; row<rows.length; row++) {
for (int col=0; col<cols.length; col++) {
for (int field=0; field<fields.length; field++) {
int nextPlane = 0;
for (int t=0; t<ts.length; t++) {
for (int z=0; z<zs.length; z++) {
for (int c=0; c<cs.length; c++) {
for (Plane p : planeList) {
if (p.row == rows[row] && p.col == cols[col] &&
p.field == fields[field] && p.t == ts[t] && p.z == zs[z] &&
p.c == cs[c])
{
planes[nextSeries][nextPlane] = p;
break;
}
}
nextPlane++;
}
}
}
nextSeries++;
}
}
}
reader = new MinimalTiffReader();
for (int i=0; i<seriesCount; i++) {
CoreMetadata ms = new CoreMetadata();
core.add(ms);
ms.sizeX = planes[i][0].x;
ms.sizeY = planes[i][0].y;
ms.sizeZ = uniqueZs.size();
ms.sizeC = uniqueCs.size();
ms.sizeT = uniqueTs.size();
ms.dimensionOrder = "XYCZT";
ms.rgb = false;
ms.imageCount = getSizeZ() * getSizeC() * getSizeT();
RandomAccessInputStream s =
new RandomAccessInputStream(planes[i][0].filename, 16);
TiffParser parser = new TiffParser(s);
parser.setDoCaching(false);
IFD firstIFD = parser.getFirstIFD();
ms.littleEndian = firstIFD.isLittleEndian();
ms.pixelType = firstIFD.getPixelType();
s.close();
}
// populate the MetadataStore
MetadataStore store = makeFilterMetadata();
MetadataTools.populatePixels(store, this, true);
store.setPlateID(MetadataTools.createLSID("Plate", 0), 0);
store.setPlateRows(new PositiveInteger(handler.getPlateRows()), 0);
store.setPlateColumns(new PositiveInteger(handler.getPlateColumns()), 0);
String plateAcqID = MetadataTools.createLSID("PlateAcquisition", 0, 0);
store.setPlateAcquisitionID(plateAcqID, 0, 0);
PositiveInteger fieldCount = FormatTools.getMaxFieldCount(fields.length);
if (fieldCount != null) {
store.setPlateAcquisitionMaximumFieldCount(fieldCount, 0, 0);
}
for (int row=0; row<rows.length; row++) {
for (int col=0; col<cols.length; col++) {
int well = row * cols.length + col;
store.setWellID(MetadataTools.createLSID("Well", 0, well), 0, well);
store.setWellRow(new NonNegativeInteger(rows[row]), 0, well);
store.setWellColumn(new NonNegativeInteger(cols[col]), 0, well);
for (int field=0; field<fields.length; field++) {
int imageIndex = well * fields.length + field;
String wellSampleID =
MetadataTools.createLSID("WellSample", 0, well, field);
store.setWellSampleID(wellSampleID, 0, well, field);
store.setWellSampleIndex(
new NonNegativeInteger(imageIndex), 0, well, field);
String imageID = MetadataTools.createLSID("Image", imageIndex);
store.setImageID(imageID, imageIndex);
store.setWellSampleImageRef(imageID, 0, well, field);
String name = "Well " + (well + 1) + ", Field " + (field + 1);
store.setImageName(name, imageIndex);
store.setPlateAcquisitionWellSampleRef(
wellSampleID, 0, 0, imageIndex);
}
}
}
if (getMetadataOptions().getMetadataLevel() != MetadataLevel.MINIMUM) {
store.setPlateName(handler.getPlateName(), 0);
store.setPlateDescription(handler.getPlateDescription(), 0);
store.setPlateExternalIdentifier(handler.getPlateIdentifier(), 0);
String experimenterID = MetadataTools.createLSID("Experimenter", 0);
store.setExperimenterID(experimenterID, 0);
store.setExperimenterLastName(handler.getExperimenterName(), 0);
for (int i=0; i<getSeriesCount(); i++) {
store.setImageExperimenterRef(experimenterID, i);
for (int c=0; c<getSizeC(); c++) {
store.setChannelName(planes[i][c].channelName, i, c);
}
store.setPixelsPhysicalSizeX(
FormatTools.getPhysicalSizeX(planes[i][0].resolutionX), i);
store.setPixelsPhysicalSizeY(
FormatTools.getPhysicalSizeY(planes[i][0].resolutionY), i);
for (int p=0; p<getImageCount(); p++) {
store.setPlanePositionX(planes[i][p].positionX, i, p);
store.setPlanePositionY(planes[i][p].positionY, i, p);
store.setPlanePositionZ(planes[i][p].positionZ, i, p);
}
}
}
}
// -- Helper classes --
class OperettaHandler extends BaseHandler {
// -- Fields --
private String currentName;
private Plane activePlane;
private String displayName;
private String plateID;
private String measurementTime;
private String plateName;
private String plateDescription;
private int plateRows, plateCols;
private ArrayList<Plane> planes = new ArrayList<Plane>();
private StringBuffer currentValue = new StringBuffer();
// -- OperettaHandler API methods --
public ArrayList<Plane> getPlanes() {
return planes;
}
public String getExperimenterName() {
return displayName;
}
public String getPlateIdentifier() {
return plateID;
}
public String getMeasurementTime() {
return measurementTime;
}
public String getPlateName() {
return plateName;
}
public String getPlateDescription() {
return plateDescription;
}
public int getPlateRows() {
return plateRows;
}
public int getPlateColumns() {
return plateCols;
}
// -- DefaultHandler API methods --
@Override
public void characters(char[] ch, int start, int length) {
String value = new String(ch, start, length);
currentValue.append(value);
}
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes)
{
currentValue.setLength(0);
currentName = qName;
if (qName.equals("Image") && attributes.getValue("id") == null) {
activePlane = new Plane();
}
}
@Override
public void endElement(String uri, String localName, String qName) {
String value = currentValue.toString();
if ("User".equals(currentName)) {
displayName = value;
}
else if ("PlateID".equals(currentName)) {
plateID = value;
}
else if ("MeasurementStartTime".equals(currentName)) {
measurementTime = value;
}
else if ("Name".equals(currentName)) {
plateName = value;
}
else if ("PlateTypeName".equals(currentName)) {
plateDescription = value;
}
else if ("PlateRows".equals(currentName)) {
plateRows = Integer.parseInt(value);
}
else if ("PlateColumns".equals(currentName)) {
plateCols = Integer.parseInt(value);
}
else if (activePlane != null) {
if ("URL".equals(currentName)) {
Location parent =
new Location(currentId).getAbsoluteFile().getParentFile();
activePlane.filename = new Location(parent, value).getAbsolutePath();
}
else if ("Row".equals(currentName)) {
activePlane.row = Integer.parseInt(value) - 1;
}
else if ("Col".equals(currentName)) {
activePlane.col = Integer.parseInt(value) - 1;
}
else if ("FieldID".equals(currentName)) {
activePlane.field = Integer.parseInt(value);
}
else if ("PlaneID".equals(currentName)) {
activePlane.z = Integer.parseInt(value);
}
else if ("ImageSizeX".equals(currentName)) {
activePlane.x = Integer.parseInt(value);
}
else if ("ImageSizeY".equals(currentName)) {
activePlane.y = Integer.parseInt(value);
}
else if ("TimepointID".equals(currentName)) {
activePlane.t = Integer.parseInt(value);
}
else if ("ChannelID".equals(currentName)) {
activePlane.c = Integer.parseInt(value);
}
else if ("ChannelName".equals(currentName)) {
activePlane.channelName = value;
}
else if ("ImageResolutionX".equals(currentName)) {
// resolution stored in meters
activePlane.resolutionX = Double.parseDouble(value) * 1000000;
}
else if ("ImageResolutionY".equals(currentName)) {
// resolution stored in meters
activePlane.resolutionY = Double.parseDouble(value) * 1000000;
}
else if ("PositionX".equals(currentName)) {
// position stored in meters
final double meters = Double.parseDouble(value) * 1000000;
activePlane.positionX = new Length(meters, UNITS.REFERENCEFRAME);
}
else if ("PositionY".equals(currentName)) {
// position stored in meters
final double meters = Double.parseDouble(value) * 1000000;
activePlane.positionY = new Length(meters, UNITS.REFERENCEFRAME);
}
else if ("AbsPositionZ".equals(currentName)) {
// position stored in meters
final double meters = Double.parseDouble(value) * 1000000;
activePlane.positionZ = new Length(meters, UNITS.REFERENCEFRAME);
}
else if ("ObjectiveMagnification".equals(currentName)) {
activePlane.magnification = Double.parseDouble(value);
}
else if ("ObjectiveNA".equals(currentName)) {
activePlane.lensNA = Double.parseDouble(value);
}
else if ("MainEmissionWavelength".equals(currentName)) {
activePlane.emWavelength = Double.parseDouble(value);
}
else if ("MainExcitationWavelength".equals(currentName)) {
activePlane.exWavelength = Double.parseDouble(value);
}
}
currentName = null;
if (qName.equals("Image") && activePlane != null) {
planes.add(activePlane);
}
}
}
class Plane {
public String filename;
public int row;
public int col;
public int field;
public int x;
public int y;
public int z;
public int t;
public int c;
public String channelName;
public double resolutionX;
public double resolutionY;
public Length positionX;
public Length positionY;
public Length positionZ;
public double emWavelength;
public double exWavelength;
public double magnification;
public double lensNA;
}
// -- Helper methods --
}