/* * #%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 javax.xml.parsers.ParserConfigurationException; import loci.common.Constants; import loci.common.DateTools; import loci.common.Location; import loci.common.RandomAccessInputStream; 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.in.PrairieMetadata.Frame; import loci.formats.in.PrairieMetadata.PFile; import loci.formats.in.PrairieMetadata.Sequence; import loci.formats.in.PrairieMetadata.ValueTable; import loci.formats.meta.MetadataStore; import loci.formats.tiff.IFD; import loci.formats.tiff.TiffParser; import ome.xml.model.primitives.PositiveFloat; import ome.xml.model.primitives.Timestamp; import ome.units.quantity.Length; import ome.units.quantity.Power; import ome.units.quantity.Time; import ome.units.UNITS; import org.w3c.dom.Document; import org.xml.sax.SAXException; /** * PrairieReader is the file format reader for * Prairie Technologies' TIFF variant. * * @author Curtis Rueden * @author Melissa Linkert */ public class PrairieReader extends FormatReader { // -- Constants -- public static final String[] CFG_SUFFIX = {"cfg"}; public static final String[] ENV_SUFFIX = {"env"}; public static final String[] XML_SUFFIX = {"xml"}; public static final String[] PRAIRIE_SUFFIXES = {"cfg", "env", "xml"}; // Private tags present in Prairie TIFF files // IMPORTANT NOTE: these are the same as Metamorph's private tags - therefore, // it is likely that Prairie TIFF files will be incorrectly // identified unless the XML or CFG file is specified private static final int PRAIRIE_TAG_1 = 33628; private static final int PRAIRIE_TAG_2 = 33629; private static final int PRAIRIE_TAG_3 = 33630; private static final String DATE_FORMAT = "MM/dd/yyyy h:mm:ss a"; // -- Fields -- /** Helper reader for opening images. */ private TiffReader tiff; /** The associated XML files. */ private Location xmlFile, cfgFile, envFile; /** Format-specific metadata. */ private PrairieMetadata meta; /** List of Prairie metadata {@code Sequence}s, ordered by cycle. */ private ArrayList<Sequence> sequences; /** List of active channels. */ private int[] channels; /** * Whether a series uses {@code Frame}s as time points rather than focal * planes (i.e., sizeZ and sizeT values inverted). * <p> * This situation occurs when the series's first {@code Sequence} is labeled * as a "TSeries" (i.e., {@link Sequence#isTimeSeries()} returns true), but * there is only one {@code Sequence}. * </p> * <p> * The array length equals the number of series; i.e., it is a parallel array * to {@link #core}. * </p> */ private boolean[] framesAreTime; /** * Flag indicating that the reader is operating in a mode where grouping of * files is disallowed. In the case of Prairie, this happens if a TIFF file is * passed to {@link #setId} while {@link #isGroupFiles()} is {@code false}. */ private boolean singleTiffMode; // -- Constructor -- /** Constructs a new Prairie TIFF reader. */ public PrairieReader() { super("Prairie TIFF", new String[] {"tif", "tiff", "cfg", "env", "xml"}); domains = new String[] {FormatTools.LM_DOMAIN}; hasCompanionFiles = true; datasetDescription = "One .xml file, one .cfg file, and one or more " + ".tif/.tiff files"; } // -- IFormatReader API methods -- @Override public boolean isSingleFile(String id) throws FormatException, IOException { return false; } @Override public boolean isThisType(String name, boolean open) { if (!open) return false; // not allowed to touch the file system Location file = new Location(name).getAbsoluteFile(); Location parent = file.getParentFile(); String prefix = file.getName(); if (prefix.indexOf(".") != -1) { prefix = prefix.substring(0, prefix.lastIndexOf(".")); } if (checkSuffix(name, CFG_SUFFIX)) { if (prefix.lastIndexOf("Config") == -1) return false; prefix = prefix.substring(0, prefix.lastIndexOf("Config")); } // check for appropriately named XML file Location xml = new Location(parent, prefix + ".xml"); while (!xml.exists() && prefix.indexOf("_") != -1) { prefix = prefix.substring(0, prefix.lastIndexOf("_")); xml = new Location(parent, prefix + ".xml"); } boolean validXML = false; try { RandomAccessInputStream xmlStream = new RandomAccessInputStream(xml.getAbsolutePath()); validXML = isThisType(xmlStream); xmlStream.close(); } catch (IOException e) { LOGGER.trace("Failed to check XML file's type", e); } return xml.exists() && super.isThisType(name, false) && validXML; } @Override public boolean isThisType(RandomAccessInputStream stream) throws IOException { final int blockLen = (int) Math.min(1048608, stream.length()); if (!FormatTools.validStream(stream, blockLen, false)) return false; String s = stream.readString(blockLen); if (s.indexOf("xml") != -1 && s.indexOf("PV") != -1) return true; TiffParser tp = new TiffParser(stream); IFD ifd = tp.getFirstIFD(); if (ifd == null) return false; String software = null; try { software = ifd.getIFDStringValue(IFD.SOFTWARE); } catch (FormatException exc) { return false; // no software tag, or tag is wrong type } if (software == null) return false; if (software.indexOf("Prairie") < 0) return false; // not Prairie software return ifd.containsKey(new Integer(PRAIRIE_TAG_1)) && ifd.containsKey(new Integer(PRAIRIE_TAG_2)) && ifd.containsKey(new Integer(PRAIRIE_TAG_3)); } @Override public int fileGroupOption(String id) throws FormatException, IOException { return FormatTools.MUST_GROUP; } @Override public String[] getSeriesUsedFiles(boolean noPixels) { FormatTools.assertId(currentId, true, 1); if (singleTiffMode) return tiff.getSeriesUsedFiles(noPixels); // add metadata files to the used files list final ArrayList<String> usedFiles = new ArrayList<String>(); if (xmlFile != null) usedFiles.add(xmlFile.getAbsolutePath()); if (cfgFile != null) usedFiles.add(cfgFile.getAbsolutePath()); if (envFile != null) usedFiles.add(envFile.getAbsolutePath()); if (!noPixels) { // add TIFF files to the used files list final int s = getSeries(); for (int t = 0; t < getSizeT(); t++) { final Sequence sequence = sequence(t, s); for (int z = 0; z < getSizeZ(); z++) { final int index = frameIndex(sequence, z, t, s); final Frame frame = sequence.getFrame(index); if (frame == null) { warnFrame(sequence, index); continue; } for (int c = 0; c < getSizeC(); c++) { final int channel = channels[c]; final PFile file = frame.getFile(channel); if (file == null) { warnFile(sequence, index, channel); continue; } final String filename = file.getFilename(); if (filename == null) { warnFilename(sequence, index, channel); continue; } usedFiles.add(getPath(file)); } } } } return usedFiles.toArray(new String[usedFiles.size()]); } @Override public int getOptimalTileWidth() { FormatTools.assertId(currentId, true, 1); return tiff.getOptimalTileWidth(); } @Override public int getOptimalTileHeight() { FormatTools.assertId(currentId, true, 1); return tiff.getOptimalTileHeight(); } @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 (singleTiffMode) return tiff.openBytes(no, buf, x, y, w, h); // convert 1D index to (sequence, index, channel) coordinates. final int[] zct = getZCTCoords(no); final int z = zct[0], c = zct[1], t = zct[2]; final Sequence sequence = sequence(t, getSeries()); final int index = frameIndex(sequence, z, t, getSeries()); final Frame frame = sequence.getFrame(index); if (frame == null) { warnFrame(sequence, index); return blank(buf); } final int channel = channels[c]; final PFile file = frame.getFile(channel); if (file == null) { warnFile(sequence, index, channel); return blank(buf); } tiff.setId(getPath(file)); return tiff.openBytes(0, buf, x, y, w, h); } @Override public void close(boolean fileOnly) throws IOException { super.close(fileOnly); if (tiff != null) tiff.close(fileOnly); if (!fileOnly) { xmlFile = cfgFile = envFile = null; tiff = null; meta = null; sequences = null; channels = null; framesAreTime = null; singleTiffMode = false; } } // -- Internal FormatReader API methods -- @Override protected void initFile(String id) throws FormatException, IOException { super.initFile(id); tiff = new TiffReader(); if (checkSuffix(id, XML_SUFFIX)) { xmlFile = new Location(id); findMetadataFiles(); } else if (checkSuffix(id, CFG_SUFFIX)) { cfgFile = new Location(id); findMetadataFiles(); } else if (checkSuffix(id, ENV_SUFFIX)) { envFile = new Location(id); findMetadataFiles(); } else { // we have been given a TIFF file if (isGroupFiles()) { findMetadataFiles(); } else { // NB: File grouping is not allowed, so we enter a special mode, // which delegates to the TIFF reader for everything. singleTiffMode = true; tiff.setId(id); return; } } currentId = xmlFile.getAbsolutePath(); parsePrairieMetadata(); populateCoreMetadata(); populateOriginalMetadata(); populateOMEMetadata(); } // -- Helper methods -- private void findMetadataFiles() { LOGGER.info("Finding metadata files"); if (xmlFile == null) xmlFile = find(XML_SUFFIX); if (cfgFile == null) cfgFile = find(CFG_SUFFIX); if (envFile == null) envFile = find(ENV_SUFFIX); } /** * This step parses the Prairie metadata files into the Prairie-specific * metadata structure, {@link #meta}. */ private void parsePrairieMetadata() throws FormatException, IOException { LOGGER.info("Parsing Prairie metadata"); final Document xml, cfg, env; try { xml = parseDOM(xmlFile); cfg = parseDOM(cfgFile); env = parseDOM(envFile); } catch (ParserConfigurationException exc) { throw new FormatException(exc); } catch (SAXException exc) { throw new FormatException(exc); } meta = new PrairieMetadata(xml, cfg, env); sequences = meta.getSequences(); channels = meta.getActiveChannels(); if (channels == null || channels.length == 0) { throw new FormatException("No active channels found"); } } /** * This step populates the {@link CoreMetadata} by extracting relevant values * from the parsed {@link #meta} structure. */ private void populateCoreMetadata() throws FormatException, IOException { LOGGER.info("Populating core metadata"); // NB: Both stage positions and time points are rasterized into the list // of Sequences. So by definition: sequenceCount = sizeT * seriesCount. final int sequenceCount = sequences.size(); final int sizeT = computeSizeT(sequenceCount); final int seriesCount = sequenceCount / sizeT; final Integer bitDepth = meta.getBitDepth(); int bpp = bitDepth == null ? -1 : bitDepth; core.clear(); framesAreTime = new boolean[seriesCount]; for (int s = 0; s < seriesCount; s++) { final Sequence sequence = sequence(0, s, seriesCount); final Frame frame = sequence.getFirstFrame(); final PFile file = frame == null ? null : frame.getFirstFile(); if (frame == null || file == null) { throw new FormatException("No metadata for series #" + s); } // NB: We initialize the TIFF reader with the first available file of the // series. For performance, we initialize only the first series, and then // assume that subsequent series have the same TIFF properties // (endianness, etc.). In our experience, all Prairie datasets conform to // this assumption, but if not, removing the "if (s == 0)" test here // should remedy any resultant inaccuracies in the metadata. if (s == 0) { tiff.setId(getPath(file)); if (bpp <= 0) bpp = tiff.getBitsPerPixel(); } final Integer linesPerFrame = frame.getLinesPerFrame(); final Integer pixelsPerLine = frame.getPixelsPerLine(); final int indexCount = sequence.getIndexCount(); final int sizeX = pixelsPerLine == null ? tiff.getSizeX() : pixelsPerLine; final int sizeY = linesPerFrame == null ? tiff.getSizeY() : linesPerFrame; framesAreTime[s] = sequence.isTimeSeries() && sizeT == 1; final CoreMetadata cm = new CoreMetadata(); cm.sizeX = sizeX; cm.sizeY = sizeY; cm.sizeZ = framesAreTime[s] ? 1 : indexCount; cm.sizeC = channels.length; cm.sizeT = framesAreTime[s] ? indexCount : sizeT; cm.pixelType = tiff.getPixelType(); cm.bitsPerPixel = bpp; cm.imageCount = cm.sizeZ * cm.sizeC * cm.sizeT; cm.dimensionOrder = "XYCZT"; cm.orderCertain = true; cm.rgb = false; cm.littleEndian = tiff.isLittleEndian(); cm.interleaved = false; cm.indexed = tiff.isIndexed(); cm.falseColor = false; core.add(cm); } } /** * This steps populates the original metadata table (the tables returned by * {@link #getGlobalMetadata()} and {@link #getSeriesMetadata()}). */ private void populateOriginalMetadata() { final boolean minimumMetadata = isMinimumMetadata(); if (minimumMetadata) return; // populate global metadata addGlobalMeta("cycleCount", meta.getCycleCount()); addGlobalMeta("date", meta.getDate()); addGlobalMeta("waitTime", meta.getWaitTime()); addGlobalMeta("sequenceCount", sequences.size()); final ValueTable config = meta.getConfig(); for (final String key : config.keySet()) { addGlobalMeta(key, config.get(key).toString()); } addGlobalMeta("meta", meta); // populate series metadata final int seriesCount = getSeriesCount(); for (int s = 0; s < seriesCount; s++) { setSeries(s); final Sequence sequence = sequence(s); addSeriesMeta("cycle", sequence.getCycle()); addSeriesMeta("indexCount", sequence.getIndexCount()); addSeriesMeta("type", sequence.getType()); } setSeries(0); } /** * This step populates the OME {@link MetadataStore} by extracting relevant * values from the parsed {@link #meta} structure. */ private void populateOMEMetadata() throws FormatException { LOGGER.info("Populating OME metadata"); // populate required Pixels metadata final boolean minimumMetadata = isMinimumMetadata(); MetadataStore store = makeFilterMetadata(); MetadataTools.populatePixels(store, this, !minimumMetadata); // populate required AcquisitionDate final String date = DateTools.formatDate(meta.getDate(), DATE_FORMAT); final Timestamp acquisitionDate = Timestamp.valueOf(date); final int seriesCount = getSeriesCount(); for (int s = 0; s < seriesCount; s++) { setSeries(s); if (date != null) store.setImageAcquisitionDate(acquisitionDate, s); } if (minimumMetadata) return; // create an Instrument final String instrumentID = MetadataTools.createLSID("Instrument", 0); store.setInstrumentID(instrumentID, 0); // populate Laser Power, if available final Double laserPower = meta.getLaserPower(); if (laserPower != null) { // create a Laser final String laserID = MetadataTools.createLSID("LightSource", 0, 0); store.setLaserID(laserID, 0, 0); store.setLaserPower(new Power(laserPower, UNITS.MW), 0, 0); } String objectiveID = null; for (int s = 0; s < seriesCount; s++) { setSeries(s); final Sequence sequence = sequence(s); final Frame firstFrame = sequence.getFirstFrame(); // link Instrument and Image store.setImageInstrumentRef(instrumentID, s); // populate PhysicalSizeX final PositiveFloat physicalSizeX = pf(firstFrame.getMicronsPerPixelX(), "PhysicalSizeX"); if (physicalSizeX != null) { store.setPixelsPhysicalSizeX(FormatTools.createLength(physicalSizeX, UNITS.MICROM), s); } // populate PhysicalSizeY final PositiveFloat physicalSizeY = pf(firstFrame.getMicronsPerPixelY(), "PhysicalSizeY"); if (physicalSizeY != null) { store.setPixelsPhysicalSizeY(FormatTools.createLength(physicalSizeY, UNITS.MICROM), s); } // populate TimeIncrement final Double waitTime = meta.getWaitTime(); if (waitTime != null) store.setPixelsTimeIncrement(new Time(waitTime, UNITS.S), s); final String[] detectorIDs = new String[channels.length]; for (int c = 0; c < channels.length; c++) { final int channel = channels[c]; final PFile file = firstFrame.getFile(channel); // populate channel name final String channelName = file == null ? null : file.getChannelName(); if (channelName != null) store.setChannelName(channelName, s, c); // populate emission wavelength if (file != null) { final Double waveMin = file.getWavelengthMin(); final Double waveMax = file.getWavelengthMax(); if (waveMin != null && waveMax != null) { final double waveAvg = (waveMin + waveMax) / 2; final Length wavelength = FormatTools.getEmissionWavelength(waveAvg); store.setChannelEmissionWavelength(wavelength, s, c); } } if (detectorIDs[c] == null) { // create a Detector for this channel detectorIDs[c] = MetadataTools.createLSID("Detector", 0, c); store.setDetectorID(detectorIDs[c], 0, c); store.setDetectorType(getDetectorType("Other"), 0, c); // NB: Ideally we would populate the detector zoom differently for // each Image, rather than globally for the Detector, but // unfortunately it is a property of Detector, not DetectorSettings. final Double zoom = firstFrame.getOpticalZoom(); if (zoom != null) store.setDetectorZoom(zoom, 0, c); } // link DetectorSettings and Detector store.setDetectorSettingsID(detectorIDs[c], s, c); // populate Offset final Double offset = firstFrame.getOffset(c); if (offset != null) store.setDetectorSettingsOffset(offset, s, c); // populate Gain final Double gain = firstFrame.getGain(c); if (gain != null) store.setDetectorSettingsGain(gain, s, c); } if (objectiveID == null) { // create an Objective objectiveID = MetadataTools.createLSID("Objective", 0, 0); store.setObjectiveID(objectiveID, 0, 0); store.setObjectiveCorrection(getCorrection("Other"), 0, 0); // populate Objective NominalMagnification final Double magnification = firstFrame.getMagnification(); if (magnification != null) { store.setObjectiveNominalMagnification(magnification, 0, 0); } // populate Objective Manufacturer final String objectiveManufacturer = firstFrame.getObjectiveManufacturer(); store.setObjectiveManufacturer(objectiveManufacturer, 0, 0); // populate Objective Immersion final String immersion = firstFrame.getImmersion(); store.setObjectiveImmersion(getImmersion(immersion), 0, 0); // populate Objective LensNA final Double lensNA = firstFrame.getObjectiveLensNA(); if (lensNA != null) store.setObjectiveLensNA(lensNA, 0, 0); // populate Microscope Model final String microscopeModel = firstFrame.getImagingDevice(); store.setMicroscopeModel(microscopeModel, 0); } // link ObjectiveSettings and Objective store.setObjectiveSettingsID(objectiveID, s); // populate stage position coordinates for (int t = 0; t < getSizeT(); t++) { final Sequence tSequence = sequence(t, s); for (int z = 0; z < getSizeZ(); z++) { final int index = frameIndex(tSequence, z, t, s); final Frame zFrame = tSequence.getFrame(index); if (zFrame == null) { warnFrame(sequence, index); continue; } final Length posX = zFrame.getPositionX(); final Length posY = zFrame.getPositionY(); final Length posZ = zFrame.getPositionZ(); final Double deltaT = zFrame.getRelativeTime(); for (int c = 0; c < getSizeC(); c++) { final int i = getIndex(z, c, t); if (posX != null) store.setPlanePositionX(posX, s, i); if (posY != null) store.setPlanePositionY(posY, s, i); if (posZ != null) store.setPlanePositionZ(posZ, s, i); if (deltaT != null) store.setPlaneDeltaT(new Time(deltaT, UNITS.S), s, i); } } } } setSeries(0); } /** Gets whether to populate only the minimum required metadata. */ private boolean isMinimumMetadata() { return getMetadataOptions().getMetadataLevel() == MetadataLevel.MINIMUM; } /** Parses a {@link Document} from the data in the given file. */ private Document parseDOM(final Location file) throws ParserConfigurationException, SAXException, IOException { if (file == null) return null; // NB: The simplest approach here would be to call XMLTools.parseDOM(file) // directly, but we cannot do that because Prairie XML files are technically // invalid and must be preprocessed in order for Java to parse them. // // Specifically, Prairie XML files describe themselves as // <?xml version="1.0" encoding="utf-8"?> // // but some of them contain invalid characters in the XML 1.0 specification. // One way to supposedly hack around this is to manually adjust the XML // version to 1.1, which is a superset of 1.0 with support for an expanded // character set. We tried it, but unfortunately the XML parsing gets // mangled (e.g., XML attributes have invalid values). // // So we hack around it another way: by filtering out all invalid characters // manually, so the data becomes valid XML version 1.0. // // For details, see: // http://stackoverflow.com/questions/2997255 // read entire XML document into a giant byte array final byte[] buf = new byte[(int) file.length()]; final RandomAccessInputStream is = new RandomAccessInputStream(file.getAbsolutePath()); is.readFully(buf); is.close(); // filter out invalid characters from the XML final String xml = XMLTools.sanitizeXML(new String(buf, Constants.ENCODING)); return XMLTools.parseDOM(xml); } /** Emits a warning about a missing {@code <Frame>}. */ private void warnFrame(final Sequence sequence, final int index) { LOGGER.warn("No Frame at cycle #{}, index #{}", sequence.getCycle(), index); } /** Emits a warning about a missing {@code <File>}. */ private void warnFile(final Sequence sequence, final int index, final int channel) { LOGGER.warn("No File at cycle #" + sequence.getCycle() + ", index #{}, channel #{}", index, channel); } /** Emits a warning about a {@code <File>}'s missing {@code filename}. */ private void warnFilename(final Sequence sequence, final int index, final int channel) { LOGGER.warn("File at cycle #" + sequence.getCycle() + ", index #{}, channel #{} has null filename", index, channel); } /** Gets the absolute path to the filename of the given {@link PFile}. */ private String getPath(final PFile file) { final Location f = new Location(xmlFile.getParent(), file.getFilename()); return f.getAbsolutePath(); } /** Blanks out and returns the given buffer. */ private byte[] blank(final byte[] buf) { // missing data; return empty plane Arrays.fill(buf, (byte) 0); return buf; } /** * Converts the given {@code double} to a {@link PositiveFloat}, or * {@code null} if incompatible. */ private PositiveFloat pf(final Double value, final String name) { if (value == null) return null; try { return new PositiveFloat(value); } catch (IllegalArgumentException e) { LOGGER.debug("Expected positive value for {}; got {}", name, value); } return null; } /** Finds the first file with one of the given suffixes. */ private Location find(final String[] suffix) { final Location file = new Location(currentId).getAbsoluteFile(); final Location parent = file.getParentFile(); final String[] listing = parent.list(); for (final String name : listing) { if (checkSuffix(name, suffix)) { return new Location(parent, name); } } return null; } /** * Scans the parsed metadata to determine the number of actual time points * versus the number of actual stage positions. The Prairie file format makes * no distinction between the two, referring to both as "Sequences", so we * must compare XYZ stage positions to differentiate them. */ private int computeSizeT(final int sequenceCount) { // NB: Guess at different possible "spans" for the rasterization. for (int sizeP = 1; sizeP <= sequenceCount; sizeP++) { if (sequenceCount % sizeP != 0) continue; // not a valid combo final int sizeT = sequenceCount / sizeP; if (positionsMatch(sizeT, sizeP)) return sizeT; } return 1; } /** Verifies that stage coordinates match for all (P, Z) across time. */ private boolean positionsMatch(int sizeT, int sizeP) { // NB: Rasterization order is XYCZpT, where p is the stage position. for (int p = 0; p < sizeP; p++) { final Sequence initialSequence = sequence(0, p, sizeP); final int indexMin = initialSequence.getIndexMin(); final int indexCount = initialSequence.getIndexCount(); for (int z = 0; z < indexCount; z++) { final int index = z + indexMin; final Frame initialFrame = initialSequence.getFrame(index); if (initialFrame == null) { warnFrame(initialSequence, index); break; } // obtain the initial XYZ stage coordinates for this position final Length xInitial = initialFrame.getPositionX(); final Length yInitial = initialFrame.getPositionY(); final Length zInitial = initialFrame.getPositionZ(); // verify that the initial coordinates match all subsequent time points for (int t = 1; t < sizeT; t++) { final Sequence sequence = sequence(t, p, sizeP); final Frame frame = sequence.getFrame(index); if (frame == null) { warnFrame(sequence, index); continue; } final Length xPos = frame.getPositionX(); final Length yPos = frame.getPositionY(); final Length zPos = frame.getPositionZ(); if (!equal(xPos, xInitial) || !equal(yPos, yInitial) || !equal(zPos, zInitial)) { return false; } } } } return true; } /** * Gets the first sequence associated with the given series. * * @param s The series (i.e., stage position). * @return The first associated {@code Sequence}. */ private Sequence sequence(final int s) { return sequence(0, s); } /** * Gets the sequence associated with the given series and time point. * * @param t The time point. * @param s The series (i.e., stage position). * @return The associated {@code Sequence}. */ private Sequence sequence(final int t, final int s) { final int actualT = framesAreTime[s] ? 0 : t; return sequence(actualT, s, getSeriesCount()); } /** * Gets the sequence associated with the given time point and stage position. * * @param t The time point. * @param p The stage position. * @param sizeP The number of stage positions. * @return The associated {@code Sequence}. */ private Sequence sequence(final int t, final int p, final int sizeP) { return sequences.get(sizeP * t + p); } /** * Gets the frame index associated with the given (Z, T) position of the * specified series. * * @param sequence The sequence from which to extract the frame. * @param z The focal plane. * @param t The time point. * @param s The series (i.e., stage position). * @return The frame index which can be passed to {@link Sequence#getFrame}. */ private int frameIndex(final Sequence sequence, int z, int t, int s) { return (framesAreTime[s] ? t : z) + sequence.getIndexMin(); } /** Determines whether the two {@link Length} values are equal. */ private static boolean equal(final Length xPos, final Length xInitial) { if (xPos == null && xInitial == null) return true; if (xPos == null) return false; return xPos.equals(xInitial); } }