/* * #%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.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import ome.units.UNITS; import ome.units.quantity.Length; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * Metadata structure for Prairie Technologies' TIFF-based format. * * @author Curtis Rueden */ public class PrairieMetadata { /** {@code <Sequence>} elements, keyed on each sequence's {@code cycle}. */ private final HashMap<Integer, Sequence> sequences = new HashMap<Integer, Sequence>(); /** Table of key/value pairs at the top level. */ private final ValueTable scanValues = new ValueTable(); /** The first actual {@code <Sequence>} element. */ private Sequence firstSequence; /** Minimum cycle value. */ private int cycleMin = Integer.MAX_VALUE; /** Maximum cycle value. */ private int cycleMax = Integer.MIN_VALUE; /** The date of the acquisition. */ private String date; /** The wait time of the acquisition. */ private Double waitTime; /** Set of active channel indices. */ private final HashSet<Integer> activeChannels = new HashSet<Integer>(); /** Key/value pairs from CFG and/or ENV files. */ private final ValueTable config = new ValueTable(); /** * Creates a new Prairie metadata by parsing the given XML, CFG and/or ENV * documents. * * @param xml The XML document to parse, or null if none available. * @param cfg The CFG document to parse, or null if none available. * @param env The ENV document to parse, or null if none available. */ public PrairieMetadata(final Document xml, final Document cfg, final Document env) { if (xml != null) parseXML(xml); if (cfg != null) parseCFG(cfg); if (env != null) parseENV(env); parseChannels(); } // -- PrairieMetadata methods -- /** Gets the {@code waitTime} recorded in the configuration. */ public Double getWaitTime() { return waitTime; } /** * Gets the list of active channel indices, in sorted order. * <p> * These indices correspond to the configuration's {@code channel} keys * flagged as {@code True}. * </p> */ public int[] getActiveChannels() { final int[] result = new int[activeChannels.size()]; int i = 0; for (int channelIndex : activeChannels) { result[i++] = channelIndex; } Arrays.sort(result); return result; } /** * Gets whether the stage position X coordinates are inverted (i.e., * left-to-right). */ public boolean isInvertX() { //return b(getConfig("xYStageXPositionIncreasesLeftToRight")); // HACK: It appears that this flag is not actually respected // in the tile layout, so we behave as though it is never set! return false; } /** * Gets whether the stage position Y coordinates are inverted (i.e., * bottom-to-top). */ public boolean isInvertY() { return b(value(getConfig("xYStageYPositionIncreasesBottomToTop"))); } /** Gets the {@code bitDepth} recorded in the configuration. */ public Integer getBitDepth() { return i(value(getConfig("bitDepth"))); } /** Gets the first {@code laserPower} recorded in the configuration. */ public Double getLaserPower() { return d(value(getConfig("laserPower"), 0)); } /** Gets the {@code value} of the given configuration {@code key}. */ public Value getConfig(final String key) { return config.get(key); } /** Gets the map of configuration key/value pairs. */ public ValueTable getConfig() { return config; } /** Gets the date of the acquisition. */ public String getDate() { return date; } /** * Gets the minimum cycle value. Matches the smallest {@code cycle} attribute * found, and hence will not necessarily equal {@code 1} (though in practice * it usually does). */ public int getCycleMin() { return cycleMin; } /** * Gets the maximum cycle value. Matches the largest {@code cycle} attribute * found, and hence will not necessarily equal {@code sequences#size()} * (though in practice it usually does). */ public int getCycleMax() { return cycleMax; } /** * Gets the number of recorded cycles. This value is equal to * {@link #getCycleMax()} - {@link #getCycleMin()} + 1. */ public int getCycleCount() { return cycleMax - cycleMin + 1; } /** Gets the first {@code Sequence}. */ public Sequence getFirstSequence() { return firstSequence; } /** Gets the {@code Sequence} at the given {@code cycle}. */ public Sequence getSequence(final int cycle) { return sequences.get(cycle); } /** Gets all {@code Sequences}, ordered by {@code cycle}. */ public ArrayList<Sequence> getSequences() { return valuesByKey(sequences); } /** Gets the {@code Frame} at the given ({@code cycle} and {@code index}). */ public Frame getFrame(final int cycle, final int index) { final Sequence sequence = getSequence(cycle); if (sequence == null) return null; return sequence.getFrame(index); } /** * Gets the {@code Frame} at the given ({@code cycle}, {@code index}, * {@code channel}). */ public PFile getFile(final int cycle, final int index, final int channel) { final Frame frame = getFrame(cycle, index); if (frame == null) return null; return frame.getFile(channel); } /** * Gets the {@code value} of the given {@code key}, at the top-level * {@code <PVScan>} element. */ public Value getValue(final String key) { return scanValues.get(key); } /** Gets the table of {@code PVScan} key/value pairs. */ public ValueTable getValues() { return scanValues; } // -- Helper methods -- /** Parses metadata from Prairie XML file. */ private void parseXML(final Document doc) { final Element pvScan = doc.getDocumentElement(); checkElement(pvScan, "PVScan"); // parse <PVStateShard> key/value block parsePVStateShard(pvScan, scanValues); // parse acquisition date date = attr(pvScan, "date"); // iterate over all Sequence elements final NodeList sequenceNodes = doc.getElementsByTagName("Sequence"); for (int s = 0; s < sequenceNodes.getLength(); s++) { final Element sequenceElement = el(sequenceNodes, s); if (sequenceElement == null) continue; final Sequence sequence = new Sequence(sequenceElement); if (firstSequence == null) firstSequence = sequence; final int cycle = sequence.getCycle(); if (cycle < cycleMin) cycleMin = cycle; if (cycle > cycleMax) cycleMax = cycle; sequences.put(cycle, sequence); } } /** * Parses metadata from Prairie CFG file. This file is only present for * Prairie datasets recorded prior to version 5.2. */ private void parseCFG(final Document doc) { checkElement(doc.getDocumentElement(), "PVConfig"); final NodeList waitNodes = doc.getElementsByTagName("PVTSeriesElementWait"); if (waitNodes.getLength() > 0) { final Element waitElement = el(waitNodes, 0); waitTime = d(attr(waitElement, "waitTime")); } parseKeys(doc.getDocumentElement(), config); } /** * Parses metadata from Prairie ENV file. This file is only present for * Prairie datasets recorded with version 5.2 or later. */ private void parseENV(final Document doc) { checkElement(doc.getDocumentElement(), "Environment"); parsePVStateShard(doc.getDocumentElement(), config); } /** * Parses {@code <Key>} elements beneath the given element, into the specified * table. These {@code <Key>} elements are only present in data from * PrairieView versions prior to 5.2. */ private void parseKeys(final Element el, final ValueTable table) { final NodeList keyNodes = el.getElementsByTagName("Key"); for (int k = 0; k < keyNodes.getLength(); k++) { final Element keyElement = el(keyNodes, k); if (keyElement == null) continue; final String key = attr(keyElement, "key"); final String value = attr(keyElement, "value"); final int underscore = key.indexOf("_"); if (underscore < 0) { // single key/value pair table.put(key, new ValueItem(value, null)); } else { // table of key/value pairs final String prefix = key.substring(0, underscore); final String index = key.substring(underscore + 1); if (!table.containsKey(prefix)) { table.put(prefix, new ValueTable()); } final ValueTable subTable = (ValueTable) table.get(prefix); final String[] tokens = value.split(","); if (tokens.length == 1) { // single value subTable.put(index, new ValueItem(value, null)); } else { // sub-table of values final ValueTable subSubTable = new ValueTable(); for (int i=0; i<tokens.length; i++) { subSubTable.put("" + i, new ValueItem(tokens[i], null)); } subTable.put(index, subSubTable); } } } } /** * Parses the {@code <PVStateShard>} element beneath the given element, into * the specified table. These {@code <PVStateShard>} elements are only present * in data from PrairieView versions 5.2 and later. */ private void parsePVStateShard(final Element el, final ValueTable table) { final Element pvStateShard = getFirstChild(el, "PVStateShard"); if (pvStateShard == null) return; final NodeList svNodes = el.getElementsByTagName("PVStateValue"); for (int k = 0; k < svNodes.getLength(); k++) { final Element keyElement = el(svNodes, k); if (keyElement == null) continue; final String key = attr(keyElement, "key"); final String value = attr(keyElement, "value"); if (value != null) { // E.g.: <PVStateValue key="linesPerFrame" value="186" /> table.put(key, new ValueItem(value, attr(keyElement, "description"))); continue; } // value is itself a table of values final ValueTable subTable = new ValueTable(); table.put(key, subTable); // process <IndexedValue> elements; e.g.: // <IndexedValue index="0" value="605" description="Ch1 High Voltage" /> final NodeList ivNodes = keyElement.getElementsByTagName("IndexedValue"); for (int i = 0; i < ivNodes.getLength(); i++) { final Element ivElement = el(ivNodes, i); if (ivElement == null) continue; final String index = attr(ivElement, "index"); if (index == null) continue; // invalid <IndexedValue> element final String iValue = attr(ivElement, "value"); final String iDescription = attr(ivElement, "description"); subTable.put(index, new ValueItem(iValue, iDescription)); } // process <SubindexedValue> elements; e.g.: // <SubindexedValues index="ZAxis"> // <SubindexedValue subindex="0" value="-9" description="Focus" /> // <SubindexedValue subindex="1" value="62.45" description="Piezo" /> // </SubindexedValues> final NodeList sivNodes = keyElement.getElementsByTagName("SubindexedValues"); for (int i = 0; i < sivNodes.getLength(); i++) { final Element sivElement = el(sivNodes, i); if (sivElement == null) continue; final String index = attr(sivElement, "index"); if (index == null) continue; // invalid <SubindexedValues> element final ValueTable subSubTable = new ValueTable(); subTable.put(index, subSubTable); // iterate over <SubindexedValue> children final NodeList subNodes = sivElement.getElementsByTagName("SubindexedValue"); for (int s = 0; s < subNodes.getLength(); s++) { final Element subElement = el(subNodes, s); final String subindex = attr(subElement, "subindex"); if (subindex == null) continue; // invalid <SubindexedValue> element final String sValue = attr(subElement, "value"); final String sDescription = attr(subElement, "description"); subSubTable.put(subindex, new ValueItem(sValue, sDescription)); } } } } /** * Parses details of the activated channels into the {@link #activeChannels} * data structure from the "channel" entry of the configuration. */ private void parseChannels() { final Value channels = config.get("channel"); if (!(channels instanceof ValueTable)) return; final ValueTable channelsTable = (ValueTable) channels; for (final String key : channelsTable.keySet()) { final Value value = channelsTable.get(key); // verify that the channel is active if (!b(value(value))) continue; // channel not active // parse the channel index (converting to a 1-based index!) final int channelIndex = i(key) + 1; // add the channel index to the active channels list activeChannels.add(channelIndex); } } /** * Checks that the given element has the specified name. * * @throws IllegalArgumentException if the name does not match. */ private void checkElement(final Element el, final String name) { if (!el.getNodeName().equals(name)) { throw new IllegalArgumentException("Not a " + name + " element"); } } /** Gets the first child element with the given name. */ private Element getFirstChild(final Element el, final String name) { // NB: Unfortunately, the Element interface has no API method to obtain // _only_ direct children with a given name; the getElementsByTagName // method returns _all_ descendant elements with the given name. final NodeList nodeList = el.getChildNodes(); for (int i = 0; i < nodeList.getLength(); i++) { final Element child = el(nodeList, i); if (child == null) continue; if (name.equals(child.getNodeName())) return child; } return null; } /** Gets the {@code index}th element from the given list of nodes. */ private Element el(final NodeList nodes, final int index) { final Node node = nodes.item(index); if (!(node instanceof Element)) return null; return (Element) node; } /** Gets the attribute value with the given name, or null if not defined. */ private String attr(final Element el, final String name) { return el.hasAttribute(name) ? el.getAttribute(name) : null; } /** Returns {@code value.value()}, or null if {@code value} is null. */ private String value(final Value value) { return value == null ? null : value.value(); } /** * Returns {@code value.get(key).value()}, or null if {@code value} or * {@code value.get(key)} is null. */ private String value(final Value value, final String key) { if (value == null) return null; final Value v = value.get(key); return v == null ? null : v.value(); } /** * Returns {@code value.get(index).value()}, or null if {@code value} or * {@code value.get(index)} is null. */ private String value(final Value value, final int index) { if (value == null) return null; final Value v = value.get(index); return v == null ? null : v.value(); } /** Converts the given string to a {@code boolean}. */ private boolean b(final String value) { return Boolean.parseBoolean(value); } /** * Converts the given string to a {@link Double}, or {@code null} if * incompatible. */ private Double d(final String value) { if (value == null) return null; try { return new Double(value); } catch (final NumberFormatException exc) { // TODO: log it return null; } } /** * Converts the given string to an {@link Integer}, or null if incompatible. */ private Integer i(final String value) { if (value == null) return null; try { return new Integer(value); } catch (final NumberFormatException exc) { // TODO: log it return null; } } /** * Gets the {@code i}th token of the given string, split according to the * specific regular expression. */ private String token(final String s, final String regex, final int i) { if (s == null) return null; final String[] tokens = s.split(regex); return tokens.length > i ? tokens[i] : null; } /** Gets the values of the given map, sorted by key. */ private <K extends Comparable<? super K>, V> ArrayList<V> valuesByKey( final Map<K, V> map) { final ArrayList<K> keys = new ArrayList<K>(map.size()); final ArrayList<V> values = new ArrayList<V>(map.size()); keys.addAll(map.keySet()); Collections.sort(keys); for (final K key : keys) { values.add(map.get(key)); } return values; } // -- Helper classes -- /** A Prairie {@code <Sequence>}. */ public class Sequence { /** * {@code <Frame>} elements beneath this {@code <Sequence>}, keyed on each * frame's {@code index}. */ private final HashMap<Integer, Frame> frames = new HashMap<Integer, Frame>(); /** Table of key/value pairs for this {@code <Sequence>}. */ private final ValueTable sequenceValues = new ValueTable(); /** The first actual {@code <Frame>} element for this {@code <Sequence>}. */ private Frame firstFrame; /** Minimum index value. */ private int indexMin = Integer.MAX_VALUE; /** Maximum index value. */ private int indexMax = Integer.MIN_VALUE; /** {@code type} attribute of this {@code <Sequence>}. */ private String type; /** {@code cycle} of this {@code <Sequence>}. */ private Integer cycle; /** {@code SpectralMode} of this {@code <Sequence>}. */ private boolean spectralMode; /** * Creates a new sequence by parsing the given {@code <Sequence>} element. */ public Sequence(final Element sequenceElement) { parse(sequenceElement); } /** Parses metadata from the given {@code Sequence} element. */ public void parse(final Element sequenceElement) { checkElement(sequenceElement, "Sequence"); // parse <PVStateShard> key/value block parsePVStateShard(sequenceElement, sequenceValues); type = attr(sequenceElement, "type"); cycle = i(attr(sequenceElement, "cycle")); if (cycle == null) { throw new IllegalArgumentException("Sequence missing cycle attribute"); } spectralMode = b(attr(sequenceElement, "SpectralMode")); // iterate over all Frame elements final NodeList frameNodes = sequenceElement.getElementsByTagName("Frame"); for (int f = 0; f < frameNodes.getLength(); f++) { final Element frameElement = el(frameNodes, f); if (frameElement == null) continue; final Frame frame = new Frame(this, frameElement); if (firstFrame == null) firstFrame = frame; final int index = frame.getIndex(); if (index < indexMin) indexMin = index; if (index > indexMax) indexMax = index; frames.put(index, frame); } } /** Gets the {@code type} associated with this {@code Sequence}. */ public String getType() { return type; } /** * Gets whether this {@code Sequence} should be considered a time series. */ public boolean isTimeSeries() { return "TSeries Timed Element".equals(type); } /** Gets the {@code cycle} associated with this {@code Sequence}. */ public int getCycle() { return cycle; } public boolean isSpectralMode() { return spectralMode; } /** * Gets the minimum index value. Matches the smallest {@code index} * attribute found, and hence will not necessarily equal {@code 1} (though * in practice it usually does). */ public int getIndexMin() { return indexMin; } /** * Gets the maximum index value. Matches the largest {@code index} attribute * found, and hence will not necessarily equal {@code frames#size()} (though * in practice it usually does). */ public int getIndexMax() { return indexMax; } /** * Gets the number of recorded indices at this {@code Sequence}. This value * is equal to {@link #getIndexMax()} - {@link #getIndexMin()} + 1. */ public int getIndexCount() { return indexMax - indexMin + 1; } /** Gets the first {@code Frame} of the {@code Sequence}. */ public Frame getFirstFrame() { return firstFrame; } /** Gets the {@code Frame} with the given {@code index}. */ public Frame getFrame(final int index) { return frames.get(index); } /** * Gets the {@code Frame} at the given ({@code cycle}, {@code index}, * {@code channel}). */ public PFile getFile(final int index, final int channel) { final Frame frame = getFrame(index); if (frame == null) return null; return frame.getFile(channel); } /** * Gets the {@code value} of the given {@code key}, beneath this * {@code Sequence}, inferring the value from the parent {@code <PVScan>} * section as needed. */ public Value getValue(final String key) { if (sequenceValues.containsKey(key)) return sequenceValues.get(key); return PrairieMetadata.this.getValue(key); } /** Gets the table of {@code Frame} key/value pairs. */ public ValueTable getValues() { return sequenceValues; } } /** A Prairie {@code <Frame>}, beneath a {@code <Sequence>}. */ public class Frame { /** The {@code <Sequence>} containing this {@code <Frame>}. */ private Sequence sequence; /** * {@code <File>} elements beneath this {@code <Frame>}, keyed on each * file's {@code channel}. */ private final HashMap<Integer, PFile> files = new HashMap<Integer, PFile>(); /** Table of key/value pairs for this {@code <Frame>}. */ private final ValueTable frameValues = new ValueTable(); /** The first actual {@code <File>} element for this {@code <Frame>}. */ private PFile firstFile; /** {@code relativeTime} attribute of this {@code <Frame>}. */ private Double relativeTime; /** {@code absoluteTime} attribute of this {@code <Frame>}. */ private Double absoluteTime; /** {@code index} of this {@code <Frame>}. */ private Integer index; /** Creates a new frame by parsing the given {@code <Frame>} element. */ public Frame(final Sequence sequence, final Element frameElement) { this.sequence = sequence; parse(frameElement); } // -- Frame methods -- /** Gets the {@code <Sequence>} containing this {@code <Frame>}. */ public Sequence getSequence() { return sequence; } /** Parses metadata from the given {@code Frame} element. */ public void parse(final Element frameElement) { checkElement(frameElement, "Frame"); // parse <PVStateShard> key/value block parsePVStateShard(frameElement, frameValues); relativeTime = d(attr(frameElement, "relativeTime")); absoluteTime = d(attr(frameElement, "absoluteTime")); index = i(attr(frameElement, "index")); if (index == null) { throw new IllegalArgumentException("Frame missing index attribute"); } // iterate over all File elements final NodeList fileNodes = frameElement.getElementsByTagName("File"); for (int f = 0; f < fileNodes.getLength(); f++) { final Element fileElement = el(fileNodes, f); if (fileElement == null) continue; final PFile file = new PFile(this, fileElement); if (firstFile == null) firstFile = file; final int channel = file.getChannel(); files.put(channel, file); } parseKeys(frameElement, frameValues); } /** Gets the {@code relativeTime} associated with this {@code Frame}. */ public double getRelativeTime() { return relativeTime; } /** Gets the {@code absoluteTime} associated with this {@code Frame}. */ public double getAbsoluteTime() { return absoluteTime; } /** Gets the {@code index} associated with this {@code Frame}. */ public int getIndex() { return index; } /** Gets the first {@code File} of the {@code Sequence}. */ public PFile getFirstFile() { return firstFile; } /** Gets the {@code File} with the given {@code channel}. */ public PFile getFile(final int channel) { return files.get(channel); } /** Gets the objective lens string for this {@code Frame}. */ public String getObjectiveLens() { return value(getValue("objectiveLens")); } /** Extracts the objective manufacturer from the objective lens string. */ public String getObjectiveManufacturer() { return token(getObjectiveLens(), " ", 0); } /** Extracts the magnification from the objective lens string. */ public Double getMagnification() { return d(token(getObjectiveLens(), " ", 1)); } /** Extracts the immersion from the objective lens string. */ public String getImmersion() { return token(getObjectiveLens(), " ", 2); } /** Gets the numerical aperture of the lens for this {@code Frame}. */ public Double getObjectiveLensNA() { return d(value(getValue("objectiveLensNA"))); } /** Gets the pixels per line for this {@code Frame}. */ public Integer getPixelsPerLine() { return i(value(getValue("pixelsPerLine"))); } /** Gets the lines per frame for this {@code Frame}. */ public Integer getLinesPerFrame() { return i(value(getValue("linesPerFrame"))); } /** * Convert a position number to a length in the microscope reference frame. * @param position a position number, may be {@code null} * @param isInvert if the number's sign should be flipped * @return a length corresponding to the number, may be {@code null} */ private Length toLength(Double position, boolean isInvert) { if (position == null) { return null; } if (isInvert) { position = -position; } return new Length(position, UNITS.REFERENCEFRAME); } /** Gets the X stage position associated with this {@code Frame}. */ public Length getPositionX() { final Double posX = d(value(getValue("positionCurrent"), "XAxis")); return toLength(posX, isInvertX()); } /** Gets the Y stage position associated with this {@code Frame}. */ public Length getPositionY() { final Double posY = d(value(getValue("positionCurrent"), "YAxis")); return toLength(posY, isInvertY()); } /** Gets the Z stage position associated with this {@code Frame}. */ public Length getPositionZ() { final Double posZ = d(value(getValue("positionCurrent"), "ZAxis")); return toLength(posZ, false); } /** Gets the optical zoom associated with this {@code Frame}. */ public Double getOpticalZoom() { return d(value(getValue("opticalZoom"))); } /** Gets the microns per pixel along X for this {@code Frame}. */ public Double getMicronsPerPixelX() { return d(value(getValue("micronsPerPixel"), "XAxis")); } /** Gets the microns per pixel along Y for this {@code Frame}. */ public Double getMicronsPerPixelY() { return d(value(getValue("micronsPerPixel"), "YAxis")); } /** * Gets the {@code c}th offset for this {@code Frame}. * * @param c The 0-based(!) channel index for which to obtain the offset. */ public Double getOffset(final int c) { return d(value(getValue("pmtOffset"), c)); } /** * Gets the {@code c}th gain for this {@code Frame}. * * @param c The 0-based(!) channel index for which to obtain the gain. */ public Double getGain(final int c) { return d(value(getValue("pmtGain"), c)); } /** Gets the imaging device associated with this {@code Frame}. */ public String getImagingDevice() { return value(getValue("imagingDevice")); } /** * Gets the {@code value} of the given {@code key}, beneath this * {@code Frame}, inferring the value from the parent {@code <Sequence>} * or grandparent {@code <PVScan>} section as needed. */ public Value getValue(final String key) { if (frameValues.containsKey(key)) return frameValues.get(key); return getSequence().getValue(key); } /** Gets the table of {@code Frame} key/value pairs. */ public ValueTable getValues() { return frameValues; } } /** * A Prairie {@code <File>} beneath a {@code <Frame>}. It is called * {@code PFile} rather than {@code File} to avoid confusion with the * {@link java.io.File} class. */ public class PFile { /** The {@code <Frame>} containing this {@code <File>}. */ private Frame frame; /** {@code channel} of this {@code <File>}. */ private Integer channel; /** {@code channelName} attribute of this {@code <File>}. */ private String channelName; /** {@code filename} attribute of this {@code <File>}. */ private String filename; /** {@code wavelengthMin} attribute of this {@code <File>}. */ private Double waveMin; /** {@code wavelengthMax} attribute of this {@code <File>}. */ private Double waveMax; /** Creates a new file by parsing the given {@code <File>} element. */ public PFile(final Frame frame, final Element fileElement) { this.frame = frame; parse(fileElement); } // -- PFile methods -- /** Gets the {@code <Frame>} containing this {@code <File>}. */ public Frame getFrame() { return frame; } /** Parses metadata from the given {@code File} element. */ public void parse(final Element fileElement) { checkElement(fileElement, "File"); channel = i(attr(fileElement, "channel")); if (channel == null) { throw new IllegalArgumentException("File missing channel attribute"); } activeChannels.add(channel); channelName = attr(fileElement, "channelName"); filename = attr(fileElement, "filename"); waveMin = d(attr(fileElement, "wavelengthMin")); waveMax = d(attr(fileElement, "wavelengthMax")); } /** Gets the {@code channel} associated with this {@code File}. */ public int getChannel() { return channel; } /** Gets the {@code channelName} associated with this {@code File}. */ public String getChannelName() { return channelName; } /** Gets the {@code filename} associated with this {@code File}. */ public String getFilename() { return filename; } /** Gets the {@code wavelengthMin} associated with this {@code File}. */ public Double getWavelengthMin() { return waveMin; } /** Gets the {@code wavelengthMax} associated with this {@code File}. */ public Double getWavelengthMax() { return waveMax; } } /** * A value in a Prairie metadata dictionary. * <p> * Prior to PrairieView 5.2, these were expressed as {@code <Key>} elements: * </p> * * <pre> * <Key key="linesPerFrame" permissions="Read, Write, Save" value="186" /> * <Key key="pmtGain_0" permissions="Write, Save" value="605" /> * <Key key="pmtGain_1" permissions="Write, Save" value="604" /> * <Key key="pmtGain_2" permissions="Write, Save" value="0" /> * <Key key="positionCurrent_XAxis" permissions="Write, Save" value="0.95" /> * <Key key="positionCurrent_YAxis" permissions="Write, Save" value="-4.45" /> * <Key key="positionCurrent_ZAxis" permissions="Write, Save" value="-9,62.45" /> * </pre> * <p> * From 5.2 onwards, they are @{code <PVStateValue>} elements: * </p> * * <pre> * <PVStateValue key="linesPerFrame" value="186" /> * <PVStateValue key="pmtGain"> * <IndexedValue index="0" value="605" description="Ch1 High Voltage" /> * <IndexedValue index="1" value="604" description="Ch2 High Voltage" /> * <IndexedValue index="2" value="0" description="Ch3 High Voltage" /> * </PVStateValue> * <PVStateValue key="positionCurrent"> * <SubindexedValues index="XAxis"> * <SubindexedValue subindex="0" value="0.95" /> * </SubindexedValues> * <SubindexedValues index="YAxis"> * <SubindexedValue subindex="0" value="-4.45" /> * </SubindexedValues> * <SubindexedValues index="ZAxis"> * <SubindexedValue subindex="0" value="-9" description="Focus" /> * <SubindexedValue subindex="1" value="62.45" description="Piezo" /> * </SubindexedValues> * </PVStateValue> * </pre> */ public static interface Value { boolean isTable(); Value get(Object key); Value get(int index); String value(); String description(); } /** * A leaf value with an actual {@link #value()} as well as an optional * {@link #description()}. */ public static class ValueItem implements Value { private String value; private String description; public ValueItem(final String value, final String description) { this.value = value; this.description = description; } @Override public boolean isTable() { return false; } @Override public Value get(final Object key) { return null; } @Override public Value get(final int index) { return null; } @Override public String value() { return value; } @Override public String description() { return description; } @Override public String toString() { return value(); } } /** * A table of values. Each value may be either a leaf item ({@link ValueItem}) * or a sub-table ({@link ValueTable}). */ public static class ValueTable extends HashMap<String, Value> implements Value { @Override public boolean isTable() { return true; } @Override public Value get(int index) { return get("" + index); } @Override public String value() { // NB: For tables with exactly one entry, we return the value // of the entry directly, when the table's value is requested. // This works around an ambiguity within the pre-5.2 schema, // which made it impossible to distinguish between two cases. // Consider the following pre-5.2 XML fragment: // // <Key key="positionCurrent_XAxis" value="0.95" /> // // In terms of the 5.2+ schema, there are two potential ways // to interpret this information: // // <PVStateValue key="positionCurrent"> // <SubindexedValues index="XAxis"> // <SubindexedValue subindex="0" value="0.95" /> // </SubindexedValues> // </PVStateValue> // // And: // // <PVStateValue key="positionCurrent"> // <IndexedValue index="XAxis" value="0.95" /> // </PVStateValue> // // In order to maintain consistency when consuming such fields, // we allow "short circuiting" single table indices. So in the // above case, the following statements are equivalent: // // table.get("positionCurrent").get("XAxis").get(0).value(); // table.get("positionCurrent").get("XAxis").value(); // table.get("positionCurrent").value(); return size() == 1 ? values().iterator().next().value() : null; } @Override public String description() { return null; } } }