loci.formats.in.LIFReader.java Source code

Java tutorial

Introduction

Here is the source code for loci.formats.in.LIFReader.java

Source

/*
 * #%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.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import loci.common.DataTools;
import loci.common.DateTools;
import loci.common.RandomAccessInputStream;
import loci.common.services.DependencyException;
import loci.common.services.ServiceException;
import loci.common.services.ServiceFactory;
import loci.common.xml.XMLTools;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
import loci.formats.FormatReader;
import loci.formats.FormatTools;
import loci.formats.ImageTools;
import loci.formats.MetadataTools;
import loci.formats.meta.MetadataStore;
import loci.formats.services.OMEXMLService;
import ome.xml.model.enums.DetectorType;
import ome.xml.model.enums.LaserMedium;
import ome.xml.model.enums.LaserType;
import ome.xml.model.primitives.Color;
import ome.xml.model.primitives.PercentFraction;
import ome.xml.model.primitives.PositiveFloat;
import ome.xml.model.primitives.Timestamp;
import ome.units.quantity.Length;
import ome.units.quantity.Time;
import ome.units.UNITS;

import org.xml.sax.SAXException;
import org.apache.commons.lang.StringUtils;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;

/**
 * LIFReader is the file format reader for Leica LIF files.
 *
 * @author Melissa Linkert melissa at glencoesoftware.com
 */
public class LIFReader extends FormatReader {

    // -- Constants --

    public static final byte LIF_MAGIC_BYTE = 0x70;
    public static final byte LIF_MEMORY_BYTE = 0x2a;

    /** The encoding used in this file.*/
    private static final String ENCODING = "ISO-8859-1";

    private static final ImmutableMap<String, Integer> CHANNEL_PRIORITIES = createChannelPriorities();

    private static ImmutableMap<String, Integer> createChannelPriorities() {
        final Builder<String, Integer> h = ImmutableMap.builder();

        h.put("red", 0);
        h.put("green", 1);
        h.put("blue", 2);
        h.put("cyan", 3);
        h.put("magenta", 4);
        h.put("yellow", 5);
        h.put("black", 6);
        h.put("gray", 7);
        h.put("", 8);

        return h.build();
    }

    // -- Fields --

    /** Offsets to memory blocks, paired with their corresponding description. */
    private List<Long> offsets;

    private int[][] realChannel;
    private int lastChannel = 0;

    private List<String> lutNames = new ArrayList<String>();
    private List<Double> physicalSizeXs = new ArrayList<Double>();
    private List<Double> physicalSizeYs = new ArrayList<Double>();
    private List<Length> fieldPosX = new ArrayList<Length>();
    private List<Length> fieldPosY = new ArrayList<Length>();

    private String[] descriptions, microscopeModels, serialNumber;
    private Double[] pinholes, zooms, zSteps, tSteps, lensNA;
    private Double[][] expTimes, gains, detectorOffsets;
    private String[][] channelNames;
    private List[] detectorModels;
    private Double[][] exWaves;
    private List[] activeDetector;
    private HashMap[] detectorIndexes;

    private String[] immersions, corrections, objectiveModels;
    private Double[] magnification;
    private Length[] posX, posY, posZ;
    private Double[] refractiveIndex;
    private List[] cutIns, cutOuts, filterModels;
    private double[][] timestamps;
    private List[] laserWavelength, laserIntensity, laserActive, laserFrap;

    private ROI[][] imageROIs;
    private boolean alternateCenter = false;
    private String[] imageNames;
    private double[] acquiredDate;

    private int[] tileCount;
    private long endPointer;

    // -- Constructor --

    /** Constructs a new Leica LIF reader. */
    public LIFReader() {
        super("Leica Image File Format", "lif");
        suffixNecessary = false;
        domains = new String[] { FormatTools.LM_DOMAIN };
    }

    // -- IFormatReader API methods --

    /* @see loci.formats.IFormatReader#getOptimalTileHeight() */
    @Override
    public int getOptimalTileHeight() {
        FormatTools.assertId(currentId, true, 1);
        return getSizeY();
    }

    /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */
    @Override
    public boolean isThisType(RandomAccessInputStream stream) throws IOException {
        final int blockLen = 1;
        if (!FormatTools.validStream(stream, blockLen, true))
            return false;
        return stream.read() == LIF_MAGIC_BYTE;
    }

    /* @see loci.formats.IFormatReader#get8BitLookupTable() */
    @Override
    public byte[][] get8BitLookupTable() {
        FormatTools.assertId(currentId, true, 1);
        if (getPixelType() != FormatTools.UINT8 || !isIndexed())
            return null;

        if (lastChannel < 0 || lastChannel >= 9) {
            return null;
        }

        byte[][] lut = new byte[3][256];
        for (int i = 0; i < 256; i++) {
            switch (lastChannel) {
            case 0:
                // red
                lut[0][i] = (byte) (i & 0xff);
                break;
            case 1:
                // green
                lut[1][i] = (byte) (i & 0xff);
                break;
            case 2:
                // blue
                lut[2][i] = (byte) (i & 0xff);
                break;
            case 3:
                // cyan
                lut[1][i] = (byte) (i & 0xff);
                lut[2][i] = (byte) (i & 0xff);
                break;
            case 4:
                // magenta
                lut[0][i] = (byte) (i & 0xff);
                lut[2][i] = (byte) (i & 0xff);
                break;
            case 5:
                // yellow
                lut[0][i] = (byte) (i & 0xff);
                lut[1][i] = (byte) (i & 0xff);
                break;
            default:
                // gray
                lut[0][i] = (byte) (i & 0xff);
                lut[1][i] = (byte) (i & 0xff);
                lut[2][i] = (byte) (i & 0xff);
            }
        }
        return lut;
    }

    /* @see loci.formats.IFormatReader#get16BitLookupTable() */
    @Override
    public short[][] get16BitLookupTable() {
        FormatTools.assertId(currentId, true, 1);
        if (getPixelType() != FormatTools.UINT16 || !isIndexed())
            return null;

        if (lastChannel < 0 || lastChannel >= 9) {
            return null;
        }

        short[][] lut = new short[3][65536];
        for (int i = 0; i < 65536; i++) {
            switch (lastChannel) {
            case 0:
                // red
                lut[0][i] = (short) (i & 0xffff);
                break;
            case 1:
                // green
                lut[1][i] = (short) (i & 0xffff);
                break;
            case 2:
                // blue
                lut[2][i] = (short) (i & 0xffff);
                break;
            case 3:
                // cyan
                lut[1][i] = (short) (i & 0xffff);
                lut[2][i] = (short) (i & 0xffff);
                break;
            case 4:
                // magenta
                lut[0][i] = (short) (i & 0xffff);
                lut[2][i] = (short) (i & 0xffff);
                break;
            case 5:
                // yellow
                lut[0][i] = (short) (i & 0xffff);
                lut[1][i] = (short) (i & 0xffff);
                break;
            default:
                // gray
                lut[0][i] = (short) (i & 0xffff);
                lut[1][i] = (short) (i & 0xffff);
                lut[2][i] = (short) (i & 0xffff);
            }
        }
        return lut;
    }

    /**
     * @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 (!isRGB()) {
            int[] pos = getZCTCoords(no);
            lastChannel = realChannel[getTileIndex(series)][pos[1]];
        }

        int index = getTileIndex(series);
        if (index >= offsets.size()) {
            // truncated file; imitate LAS AF and return black planes
            Arrays.fill(buf, (byte) 0);
            return buf;
        }

        long offset = offsets.get(index).longValue();
        int bytes = FormatTools.getBytesPerPixel(getPixelType());
        int bpp = bytes * getRGBChannelCount();

        long planeSize = (long) getSizeX() * getSizeY() * bpp;
        long nextOffset = index + 1 < offsets.size() ? offsets.get(index + 1).longValue() : endPointer;
        int bytesToSkip = (int) (nextOffset - offset - planeSize * getImageCount());
        bytesToSkip /= getSizeY();
        if ((getSizeX() % 4) == 0)
            bytesToSkip = 0;

        if (offset + (planeSize + bytesToSkip * getSizeY()) * no >= in.length()) {
            // truncated file; imitate LAS AF and return black planes
            Arrays.fill(buf, (byte) 0);
            return buf;
        }

        in.seek(offset + planeSize * no);

        int tile = series;
        for (int i = 0; i < index; i++) {
            tile -= tileCount[i];
        }

        in.skipBytes((int) (tile * planeSize * getImageCount()));
        in.skipBytes(bytesToSkip * getSizeY() * no);

        if (bytesToSkip == 0) {
            readPlane(in, x, y, w, h, buf);
        } else {
            in.skipBytes(y * (getSizeX() * bpp + bytesToSkip));
            for (int row = 0; row < h; row++) {
                in.skipBytes(x * bpp);
                in.read(buf, row * w * bpp, w * bpp);
                in.skipBytes(bpp * (getSizeX() - w - x) + bytesToSkip);
            }
        }

        // color planes are stored in BGR order
        if (getRGBChannelCount() == 3) {
            ImageTools.bgrToRgb(buf, isInterleaved(), bytes, getRGBChannelCount());
        }

        return buf;
    }

    /* @see loci.formats.IFormatReader#close(boolean) */
    @Override
    public void close(boolean fileOnly) throws IOException {
        super.close(fileOnly);
        if (!fileOnly) {
            offsets = null;
            realChannel = null;
            lastChannel = 0;
            lutNames.clear();
            physicalSizeXs.clear();
            physicalSizeYs.clear();
            descriptions = microscopeModels = serialNumber = null;
            pinholes = zooms = lensNA = null;
            zSteps = tSteps = null;
            expTimes = gains = null;
            detectorOffsets = null;
            channelNames = null;
            detectorModels = null;
            exWaves = null;
            activeDetector = null;
            immersions = corrections = null;
            magnification = null;
            objectiveModels = null;
            posX = posY = posZ = null;
            refractiveIndex = null;
            cutIns = cutOuts = filterModels = null;
            timestamps = null;
            laserWavelength = laserIntensity = laserActive = laserFrap = null;
            imageROIs = null;
            alternateCenter = false;
            imageNames = null;
            acquiredDate = null;
            detectorIndexes = null;
            tileCount = null;
            fieldPosX.clear();
            fieldPosY.clear();
            endPointer = 0;
        }
    }

    // -- Internal FormatReader API methods --

    /* @see loci.formats.FormatReader#initFile(String) */
    @Override
    protected void initFile(String id) throws FormatException, IOException {
        super.initFile(id);
        in = new RandomAccessInputStream(id);
        in.setEncoding(ENCODING);
        offsets = new ArrayList<Long>();

        in.order(true);

        // read the header

        LOGGER.info("Reading header");

        byte checkOne = in.readByte();
        in.skipBytes(2);
        byte checkTwo = in.readByte();
        if (checkOne != LIF_MAGIC_BYTE && checkTwo != LIF_MAGIC_BYTE) {
            throw new FormatException(id + " is not a valid Leica LIF file");
        }

        in.skipBytes(4);

        // read and parse the XML description

        if (in.read() != LIF_MEMORY_BYTE) {
            throw new FormatException("Invalid XML description");
        }

        // number of Unicode characters in the XML block
        int nc = in.readInt();
        String xml = DataTools.stripString(in.readString(nc * 2));

        LOGGER.info("Finding image offsets");

        while (in.getFilePointer() < in.length()) {
            LOGGER.debug("Looking for a block at {}; {} blocks read", in.getFilePointer(), offsets.size());
            int check = in.readInt();
            if (check != LIF_MAGIC_BYTE) {
                if (check == 0 && offsets.size() > 0) {
                    // newer .lif file; the remainder of the file is all 0s
                    endPointer = in.getFilePointer();
                    break;
                }
                throw new FormatException(
                        "Invalid Memory Block: found magic bytes " + check + ", expected " + LIF_MAGIC_BYTE);
            }

            in.skipBytes(4);
            check = in.read();
            if (check != LIF_MEMORY_BYTE) {
                throw new FormatException("Invalid Memory Description: found magic " + "byte " + check
                        + ", expected " + LIF_MEMORY_BYTE);
            }

            long blockLength = in.readInt();
            if (in.read() != LIF_MEMORY_BYTE) {
                in.seek(in.getFilePointer() - 5);
                blockLength = in.readLong();
                check = in.read();
                if (check != LIF_MEMORY_BYTE) {
                    throw new FormatException("Invalid Memory Description: found magic " + "byte " + check
                            + ", expected " + LIF_MEMORY_BYTE);
                }
            }

            int descrLength = in.readInt() * 2;

            if (blockLength > 0) {
                offsets.add(in.getFilePointer() + descrLength);
            }

            in.seek(in.getFilePointer() + descrLength + blockLength);
        }
        initMetadata(xml);
        xml = null;

        if (endPointer == 0) {
            endPointer = in.length();
        }

        // correct offsets, if necessary
        if (offsets.size() > getSeriesCount()) {
            Long[] storedOffsets = offsets.toArray(new Long[offsets.size()]);
            offsets.clear();
            int index = 0;
            for (int i = 0; i < getSeriesCount(); i++) {
                setSeries(i);
                long nBytes = (long) FormatTools.getPlaneSize(this) * getImageCount();
                long start = storedOffsets[index];
                long end = index == storedOffsets.length - 1 ? in.length() : storedOffsets[index + 1];
                while (end - start < nBytes && ((end - start) / nBytes) != 1) {
                    index++;
                    start = storedOffsets[index];
                    end = index == storedOffsets.length - 1 ? in.length() : storedOffsets[index + 1];
                }
                offsets.add(storedOffsets[index]);
                index++;
            }
            setSeries(0);
        }
    }

    // -- Helper methods --

    /** Parses a string of XML and puts the values in a Hashtable. */
    private void initMetadata(String xml) throws FormatException, IOException {
        try {
            ServiceFactory factory = new ServiceFactory();
            OMEXMLService service = factory.getInstance(OMEXMLService.class);
            service.createOMEXMLMetadata();
        } catch (DependencyException exc) {
            throw new FormatException("Could not create OME-XML store.", exc);
        } catch (ServiceException exc) {
            throw new FormatException("Could not create OME-XML store.", exc);
        }
        MetadataStore store = makeFilterMetadata();

        // the XML blocks stored in a LIF file are invalid,
        // because they don't have a root node

        xml = "<?xml version=\"1.0\" encoding=\"" + ENCODING + "\"?><LEICA>" + xml + "</LEICA>";

        xml = XMLTools.sanitizeXML(xml);

        translateMetadata(getMetadataRoot(xml));

        for (int i = 0; i < imageNames.length; i++) {
            setSeries(i);
            addSeriesMeta("Image name", imageNames[i]);
        }
        setSeries(0);

        // set up mapping to rearrange channels
        // for instance, the green channel may be #0, and the red channel may be #1
        realChannel = new int[tileCount.length][];
        int nextLut = 0;

        for (int i = 0; i < core.size(); i++) {
            int index = getTileIndex(i);
            if (realChannel[index] != null) {
                continue;
            }
            CoreMetadata ms = core.get(i);
            realChannel[index] = new int[ms.sizeC];

            for (int q = 0; q < ms.sizeC; q++) {
                String lut = "";
                if (nextLut < lutNames.size()) {
                    lut = lutNames.get(nextLut++).toLowerCase();
                }
                if (!CHANNEL_PRIORITIES.containsKey(lut))
                    lut = "";
                realChannel[index][q] = CHANNEL_PRIORITIES.get(lut).intValue();
            }

            int[] sorted = new int[ms.sizeC];
            Arrays.fill(sorted, -1);

            for (int q = 0; q < sorted.length; q++) {
                int min = Integer.MAX_VALUE;
                int minIndex = -1;
                for (int n = 0; n < ms.sizeC; n++) {
                    if (realChannel[index][n] < min && !DataTools.containsValue(sorted, n)) {
                        min = realChannel[index][n];
                        minIndex = n;
                    }
                }

                sorted[q] = minIndex;
            }
        }

        MetadataTools.populatePixels(store, this, true, false);

        int roiCount = 0;
        for (int i = 0; i < getSeriesCount(); i++) {
            setSeries(i);

            String instrumentID = MetadataTools.createLSID("Instrument", i);
            store.setInstrumentID(instrumentID, i);

            int index = getTileIndex(i);

            store.setMicroscopeModel(microscopeModels[index], i);
            store.setMicroscopeType(getMicroscopeType("Other"), i);

            String objectiveID = MetadataTools.createLSID("Objective", i, 0);
            store.setObjectiveID(objectiveID, i, 0);
            store.setObjectiveLensNA(lensNA[index], i, 0);
            store.setObjectiveSerialNumber(serialNumber[index], i, 0);
            if (magnification[index] != null) {
                store.setObjectiveNominalMagnification(magnification[index], i, 0);
            }
            store.setObjectiveImmersion(getImmersion(immersions[index]), i, 0);
            store.setObjectiveCorrection(getCorrection(corrections[index]), i, 0);
            store.setObjectiveModel(objectiveModels[index], i, 0);

            if (cutIns[index] != null && filterModels[index] != null) {
                int channel = 0;
                if (cutIns[index].size() >= filterModels[index].size() * 2) {
                    int diff = cutIns[index].size() - filterModels[index].size();
                    for (int q = 0; q < diff; q++) {
                        cutIns[index].remove(filterModels[index].size());
                    }
                }
                for (int filter = 0; filter < cutIns[index].size(); filter++) {
                    String filterID = MetadataTools.createLSID("Filter", i, filter);
                    store.setFilterID(filterID, i, filter);
                    if (filterModels[index] != null && filter < filterModels[index].size()) {
                        store.setFilterModel((String) filterModels[index].get(filter), i, filter);
                    }
                    store.setTransmittanceRangeCutIn((Length) cutIns[index].get(filter), i, filter);
                    store.setTransmittanceRangeCutOut((Length) cutOuts[index].get(filter), i, filter);
                }
            }

            final List<Double> lasers = laserWavelength[index];
            final List<Double> laserIntensities = laserIntensity[index];

            final List<Boolean> active = laserActive[index];
            final List<Boolean> frap = laserFrap[index];
            int nextChannel = 0;

            if (lasers != null) {
                int laserIndex = 0;
                while (laserIndex < lasers.size()) {
                    if ((Double) lasers.get(laserIndex) == 0) {
                        lasers.remove(laserIndex);
                    } else {
                        laserIndex++;
                    }
                }

                for (int laser = 0; laser < lasers.size(); laser++) {
                    String id = MetadataTools.createLSID("LightSource", i, laser);
                    store.setLaserID(id, i, laser);
                    store.setLaserType(LaserType.OTHER, i, laser);
                    store.setLaserLaserMedium(LaserMedium.OTHER, i, laser);
                    Double wavelength = (Double) lasers.get(laser);
                    Length wave = FormatTools.getWavelength(wavelength);
                    if (wave != null) {
                        store.setLaserWavelength(wave, i, laser);
                    }
                }

                Set<Integer> ignoredChannels = new HashSet<Integer>();
                final List<Integer> validIntensities = new ArrayList<Integer>();
                int size = lasers.size();
                int channel = 0;
                Set<Integer> channels = new HashSet<Integer>();

                for (int laser = 0; laser < laserIntensities.size(); laser++) {
                    double intensity = (Double) laserIntensities.get(laser);
                    channel = laser / size;
                    if (intensity < 100) {
                        validIntensities.add(laser);
                        channels.add(channel);
                    }
                    ignoredChannels.add(channel);
                }
                //remove channels w/o valid intensities
                ignoredChannels.removeAll(channels);
                //remove entries if channel has 2 wavelengths
                //e.g. 30% 458 70% 633
                int s = validIntensities.size();

                int jj;
                Set<Integer> toRemove = new HashSet<Integer>();

                int as = active.size();
                for (int j = 0; j < s; j++) {
                    if (j < as && !(Boolean) active.get(j)) {
                        toRemove.add(validIntensities.get(j));
                    }
                    jj = j + 1;
                    if (jj < s) {
                        int v = validIntensities.get(j) / size;
                        int vv = validIntensities.get(jj) / size;
                        if (vv == v) {//do not consider that channel.
                            toRemove.add(validIntensities.get(j));
                            toRemove.add(validIntensities.get(jj));
                            ignoredChannels.add(j);
                        }
                    }
                }
                if (toRemove.size() > 0) {
                    validIntensities.removeAll(toRemove);
                }

                boolean noNames = true;
                if (channelNames[index] != null) {
                    for (String name : channelNames[index]) {
                        if (name != null && !name.equals("")) {
                            noNames = false;
                            break;
                        }
                    }
                }
                if (!noNames && frap != null) { //only use name for frap.
                    for (int k = 0; k < frap.size(); k++) {
                        if (!frap.get(k)) {
                            noNames = true;
                            break;
                        }
                    }
                }

                int nextFilter = 0;
                //int nextFilter = cutIns[i].size() - getEffectiveSizeC();
                for (int k = 0; k < validIntensities.size(); k++, nextChannel++) {
                    int laserArrayIndex = validIntensities.get(k);
                    double intensity = (Double) laserIntensities.get(laserArrayIndex);
                    int laser = laserArrayIndex % lasers.size();
                    Double wavelength = (Double) lasers.get(laser);
                    if (wavelength != 0) {
                        while (ignoredChannels.contains(nextChannel)) {
                            nextChannel++;
                        }
                        while (channelNames != null && nextChannel < getEffectiveSizeC()
                                && channelNames[index] != null && ((channelNames[index][nextChannel] == null
                                        || channelNames[index][nextChannel].equals("")) && !noNames)) {
                            nextChannel++;
                        }
                        if (nextChannel < getEffectiveSizeC()) {
                            String id = MetadataTools.createLSID("LightSource", i, laser);
                            store.setChannelLightSourceSettingsID(id, i, nextChannel);
                            store.setChannelLightSourceSettingsAttenuation(
                                    new PercentFraction((float) intensity / 100f), i, nextChannel);

                            Length ex = FormatTools.getExcitationWavelength(wavelength);
                            if (ex != null) {
                                store.setChannelExcitationWavelength(ex, i, nextChannel);
                            }

                            if (wavelength > 0) {
                                if (cutIns[index] == null || nextFilter >= cutIns[index].size()) {
                                    continue;
                                }
                                Double cutIn = ((Length) cutIns[index].get(nextFilter)).value(UNITS.NM)
                                        .doubleValue();
                                while (cutIn - wavelength > 20) {
                                    nextFilter++;
                                    if (nextFilter < cutIns[index].size()) {
                                        cutIn = ((Length) cutIns[index].get(nextFilter)).value(UNITS.NM)
                                                .doubleValue();
                                    } else {
                                        break;
                                    }
                                }
                                if (nextFilter < cutIns[index].size()) {
                                    String fid = MetadataTools.createLSID("Filter", i, nextFilter);
                                    //store.setLightPathEmissionFilterRef(fid, i, nextChannel, 0);
                                    nextFilter++;
                                }
                            }
                        }
                    }
                }
            }

            store.setImageInstrumentRef(instrumentID, i);
            store.setObjectiveSettingsID(objectiveID, i);
            store.setObjectiveSettingsRefractiveIndex(refractiveIndex[index], i);

            store.setImageDescription(descriptions[index], i);
            if (acquiredDate[index] > 0) {
                store.setImageAcquisitionDate(new Timestamp(DateTools.convertDate(
                        (long) (acquiredDate[index] * 1000), DateTools.COBOL, DateTools.ISO8601_FORMAT, true)), i);
            }
            store.setImageName(imageNames[index].trim(), i);

            Length sizeX = FormatTools.getPhysicalSizeX(physicalSizeXs.get(index));
            Length sizeY = FormatTools.getPhysicalSizeY(physicalSizeYs.get(index));
            Length sizeZ = FormatTools.getPhysicalSizeZ(zSteps[index]);

            if (sizeX != null) {
                store.setPixelsPhysicalSizeX(sizeX, i);
            }
            if (sizeY != null) {
                store.setPixelsPhysicalSizeY(sizeY, i);
            }
            if (sizeZ != null) {
                store.setPixelsPhysicalSizeZ(sizeZ, i);
            }
            if (tSteps[index] != null) {
                store.setPixelsTimeIncrement(new Time(tSteps[index], UNITS.S), i);
            }

            final List<String> detectors = detectorModels[index];
            if (detectors != null) {
                nextChannel = 0;
                int start = detectors.size() - getEffectiveSizeC();
                if (start < 0) {
                    start = 0;
                }
                for (int detector = start; detector < detectors.size(); detector++) {
                    int dIndex = detector - start;
                    String detectorID = MetadataTools.createLSID("Detector", i, dIndex);
                    store.setDetectorID(detectorID, i, dIndex);
                    store.setDetectorModel((String) detectors.get(detector), i, dIndex);

                    store.setDetectorZoom(zooms[index], i, dIndex);
                    store.setDetectorType(DetectorType.PMT, i, dIndex);

                    if (activeDetector[index] != null) {
                        int detectorIndex = activeDetector[index].size() - getEffectiveSizeC() + dIndex;
                        if (detectorIndex >= 0 && detectorIndex < activeDetector[index].size()
                                && (Boolean) activeDetector[index].get(detectorIndex)
                                && detectorOffsets[index] != null && nextChannel < detectorOffsets[index].length) {
                            store.setDetectorOffset(detectorOffsets[index][nextChannel++], i, dIndex);
                        }
                    }
                }
            }

            final List<Boolean> activeDetectors = activeDetector[index];
            int firstDetector = activeDetectors == null ? 0 : activeDetectors.size() - getEffectiveSizeC();
            int nextDetector = firstDetector;

            int nextFilter = 0;
            int nextFilterDetector = 0;

            if (activeDetectors != null && activeDetectors.size() > cutIns[index].size()
                    && (Boolean) activeDetectors.get(activeDetectors.size() - 1)
                    && (Boolean) activeDetectors.get(activeDetectors.size() - 2)) {
                nextFilterDetector = activeDetectors.size() - cutIns[index].size();

                if (cutIns[index].size() > filterModels[index].size()) {
                    nextFilterDetector += filterModels[index].size();
                    nextFilter += filterModels[index].size();
                }
            }

            for (int c = 0; c < getEffectiveSizeC(); c++) {
                if (activeDetectors != null) {
                    while (nextDetector >= 0 && nextDetector < activeDetectors.size()
                            && !(Boolean) activeDetectors.get(nextDetector)) {
                        nextDetector++;
                    }
                    if (nextDetector < activeDetectors.size() && detectors != null
                            && nextDetector - firstDetector < detectors.size()) {
                        String detectorID = MetadataTools.createLSID("Detector", i, nextDetector - firstDetector);
                        store.setDetectorSettingsID(detectorID, i, c);
                        nextDetector++;

                        if (detectorOffsets[index] != null && c < detectorOffsets[index].length) {
                            store.setDetectorSettingsOffset(detectorOffsets[index][c], i, c);
                        }

                        if (gains[index] != null) {
                            store.setDetectorSettingsGain(gains[index][c], i, c);
                        }
                    }
                }

                if (channelNames[index] != null) {
                    store.setChannelName(channelNames[index][c], i, c);
                }
                if (pinholes[index] != null) {
                    store.setChannelPinholeSize(new Length(pinholes[index], UNITS.MICROM), i, c);
                }
                if (exWaves[index] != null) {
                    if (exWaves[index][c] != null && exWaves[index][c] > 1) {
                        Length ex = FormatTools.getExcitationWavelength(exWaves[index][c]);
                        if (ex != null) {
                            store.setChannelExcitationWavelength(ex, i, c);
                        }
                    }
                }

                // channel coloring is implicit if the image is stored as RGB
                Color channelColor = getChannelColor(realChannel[index][c]);
                if (!isRGB()) {
                    store.setChannelColor(channelColor, i, c);
                }

                if (channelColor.getValue() != -1 && nextFilter >= 0) {
                    if (nextDetector - firstDetector != getSizeC() && cutIns[index] != null
                            && nextDetector >= cutIns[index].size()) {
                        while (nextFilterDetector < firstDetector) {
                            String filterID = MetadataTools.createLSID("Filter", i, nextFilter);
                            store.setFilterID(filterID, i, nextFilter);

                            nextFilterDetector++;
                            nextFilter++;
                        }
                    }
                    while (activeDetectors != null && nextFilterDetector < activeDetectors.size()
                            && !(Boolean) activeDetectors.get(nextFilterDetector)) {
                        String filterID = MetadataTools.createLSID("Filter", i, nextFilter);
                        store.setFilterID(filterID, i, nextFilter);
                        nextFilterDetector++;
                        nextFilter++;
                    }
                    String filterID = MetadataTools.createLSID("Filter", i, nextFilter);
                    store.setFilterID(filterID, i, nextFilter);
                    store.setLightPathEmissionFilterRef(filterID, i, c, 0);
                    nextFilterDetector++;
                    nextFilter++;
                }
            }

            for (int image = 0; image < getImageCount(); image++) {
                Length xPos = posX[index];
                Length yPos = posY[index];
                if (i < fieldPosX.size() && fieldPosX.get(i) != null) {
                    xPos = fieldPosX.get(i);
                }
                if (i < fieldPosY.size() && fieldPosY.get(i) != null) {
                    yPos = fieldPosY.get(i);
                }
                if (xPos != null) {
                    store.setPlanePositionX(xPos, i, image);
                }
                if (yPos != null) {
                    store.setPlanePositionY(yPos, i, image);
                }
                store.setPlanePositionZ(posZ[index], i, image);
                if (timestamps[index] != null) {
                    double timestamp = timestamps[index][image];
                    if (timestamps[index][0] == acquiredDate[index]) {
                        timestamp -= acquiredDate[index];
                    } else if (timestamp == acquiredDate[index] && image > 0) {
                        timestamp = timestamps[index][0];
                    }
                    store.setPlaneDeltaT(new Time(timestamp, UNITS.S), i, image);
                }

                if (expTimes[index] != null) {
                    int c = getZCTCoords(image)[1];
                    if (expTimes[index][c] != null) {
                        store.setPlaneExposureTime(new Time(expTimes[index][c], UNITS.S), i, image);
                    }
                }
            }

            if (imageROIs[index] != null) {
                for (int roi = 0; roi < imageROIs[index].length; roi++) {
                    if (imageROIs[index][roi] != null) {
                        imageROIs[index][roi].storeROI(store, i, roiCount++, roi);
                    }
                }
            }
        }
    }

    private Element getMetadataRoot(String xml) throws FormatException, IOException {
        ByteArrayInputStream s = null;
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder parser = factory.newDocumentBuilder();
            s = new ByteArrayInputStream(xml.getBytes(ENCODING));
            return parser.parse(s).getDocumentElement();
        } catch (ParserConfigurationException e) {
            throw new FormatException(e);
        } catch (SAXException e) {
            throw new FormatException(e);
        } finally {
            if (s != null)
                s.close();
        }
    }

    private void translateMetadata(Element root) throws FormatException {
        Element realRoot = (Element) root.getChildNodes().item(0);

        NodeList toPrune = getNodes(realRoot, "LDM_Block_Sequential_Master");
        if (toPrune != null) {
            for (int i = 0; i < toPrune.getLength(); i++) {
                Element prune = (Element) toPrune.item(i);
                Element parent = (Element) prune.getParentNode();
                parent.removeChild(prune);
            }
        }

        NodeList images = getNodes(realRoot, "Image");
        List<Element> imageNodes = new ArrayList<Element>();
        Long[] oldOffsets = null;
        if (images.getLength() > offsets.size()) {
            oldOffsets = offsets.toArray(new Long[offsets.size()]);
            offsets.clear();
        }

        int nextOffset = 0;
        for (int i = 0; i < images.getLength(); i++) {
            Element image = (Element) images.item(i);
            Element grandparent = (Element) image.getParentNode();
            if (grandparent == null) {
                continue;
            }
            grandparent = (Element) grandparent.getParentNode();
            if (grandparent == null) {
                continue;
            }
            if (!"ProcessingHistory".equals(grandparent.getNodeName())) {
                // image is being referenced from an event list
                imageNodes.add(image);
                if (oldOffsets != null && nextOffset < oldOffsets.length) {
                    offsets.add(oldOffsets[nextOffset]);
                }
            }
            grandparent = (Element) grandparent.getParentNode();
            if (grandparent == null) {
                continue;
            }
            grandparent = (Element) grandparent.getParentNode();
            if (grandparent != null) {
                if (!"Image".equals(grandparent.getNodeName())) {
                    nextOffset++;
                }
            }
        }

        tileCount = new int[imageNodes.size()];
        Arrays.fill(tileCount, 1);
        core = new ArrayList<CoreMetadata>(imageNodes.size());
        acquiredDate = new double[imageNodes.size()];
        descriptions = new String[imageNodes.size()];
        laserWavelength = new List[imageNodes.size()];
        laserIntensity = new List[imageNodes.size()];
        laserActive = new List[imageNodes.size()];
        laserFrap = new List[imageNodes.size()];
        timestamps = new double[imageNodes.size()][];
        activeDetector = new List[imageNodes.size()];
        serialNumber = new String[imageNodes.size()];
        lensNA = new Double[imageNodes.size()];
        magnification = new Double[imageNodes.size()];
        immersions = new String[imageNodes.size()];
        corrections = new String[imageNodes.size()];
        objectiveModels = new String[imageNodes.size()];
        posX = new Length[imageNodes.size()];
        posY = new Length[imageNodes.size()];
        posZ = new Length[imageNodes.size()];
        refractiveIndex = new Double[imageNodes.size()];
        cutIns = new List[imageNodes.size()];
        cutOuts = new List[imageNodes.size()];
        filterModels = new List[imageNodes.size()];
        microscopeModels = new String[imageNodes.size()];
        detectorModels = new List[imageNodes.size()];
        detectorIndexes = new HashMap[imageNodes.size()];
        zSteps = new Double[imageNodes.size()];
        tSteps = new Double[imageNodes.size()];
        pinholes = new Double[imageNodes.size()];
        zooms = new Double[imageNodes.size()];

        expTimes = new Double[imageNodes.size()][];
        gains = new Double[imageNodes.size()][];
        detectorOffsets = new Double[imageNodes.size()][];
        channelNames = new String[imageNodes.size()][];
        exWaves = new Double[imageNodes.size()][];
        imageROIs = new ROI[imageNodes.size()][];
        imageNames = new String[imageNodes.size()];

        core.clear();
        for (int i = 0; i < imageNodes.size(); i++) {
            Element image = imageNodes.get(i);

            CoreMetadata ms = new CoreMetadata();
            core.add(ms);

            int index = core.size() - 1;
            setSeries(index);

            translateImageNames(image, index);
            translateImageNodes(image, index);
            translateAttachmentNodes(image, index);
            translateScannerSettings(image, index);
            translateFilterSettings(image, index);
            translateTimestamps(image, index);
            translateLaserLines(image, index);
            translateROIs(image, index);
            translateSingleROIs(image, index);
            translateDetectors(image, index);

            final Deque<String> nameStack = new ArrayDeque<String>();
            populateOriginalMetadata(image, nameStack);
            addUserCommentMeta(image, i);
        }
        setSeries(0);

        int totalSeries = 0;
        for (int count : tileCount) {
            totalSeries += count;
        }
        ArrayList<CoreMetadata> newCore = new ArrayList<CoreMetadata>();
        for (int i = 0; i < core.size(); i++) {
            for (int tile = 0; tile < tileCount[i]; tile++) {
                newCore.add(core.get(i));
            }
        }
        core = newCore;
    }

    private void populateOriginalMetadata(Element root, Deque<String> nameStack) {
        String name = root.getNodeName();
        if (root.hasAttributes() && !name.equals("Element") && !name.equals("Attachment")
                && !name.equals("LMSDataContainerHeader")) {
            nameStack.push(name);

            String suffix = root.getAttribute("Identifier");
            String value = root.getAttribute("Variant");
            if (suffix == null || suffix.trim().length() == 0) {
                suffix = root.getAttribute("Description");
            }
            StringBuffer key = new StringBuffer();
            final Iterator<String> nameStackIterator = nameStack.descendingIterator();
            while (nameStackIterator.hasNext()) {
                final String k = nameStackIterator.next();
                key.append(k);
                key.append("|");
            }
            if (suffix != null && value != null && suffix.length() > 0 && value.length() > 0
                    && !suffix.equals("HighInteger") && !suffix.equals("LowInteger")) {
                addSeriesMetaList(key.toString() + suffix, value);
            } else {
                NamedNodeMap attributes = root.getAttributes();
                for (int i = 0; i < attributes.getLength(); i++) {
                    Attr attr = (Attr) attributes.item(i);
                    if (!attr.getName().equals("HighInteger") && !attr.getName().equals("LowInteger")) {
                        addSeriesMeta(key.toString() + attr.getName(), attr.getValue());
                    }
                }
            }
        }

        NodeList children = root.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Object child = children.item(i);
            if (child instanceof Element) {
                populateOriginalMetadata((Element) child, nameStack);
            }
        }

        if (root.hasAttributes() && !name.equals("Element") && !name.equals("Attachment")
                && !name.equals("LMSDataContainerHeader")) {
            nameStack.pop();
        }
    }

    private void translateImageNames(Element imageNode, int image) {
        final List<String> names = new ArrayList<String>();
        Element parent = imageNode;
        while (true) {
            parent = (Element) parent.getParentNode();
            if (parent == null || parent.getNodeName().equals("LEICA")) {
                break;
            }
            if (parent.getNodeName().equals("Element")) {
                names.add(parent.getAttribute("Name"));
            }
        }
        imageNames[image] = "";
        for (int i = names.size() - 2; i >= 0; i--) {
            imageNames[image] += names.get(i);
            if (i > 0)
                imageNames[image] += "/";
        }
    }

    private void translateDetectors(Element imageNode, int image) throws FormatException {
        NodeList definitions = getNodes(imageNode, "ATLConfocalSettingDefinition");
        if (definitions == null)
            return;

        final List<String> channels = new ArrayList<String>();
        laserActive[image] = new ArrayList<Boolean>();
        int nextChannel = 0;
        for (int definition = 0; definition < definitions.getLength(); definition++) {
            Element definitionNode = (Element) definitions.item(definition);
            String parentName = definitionNode.getParentNode().getNodeName();
            boolean isMaster = parentName.endsWith("Master");
            NodeList detectors = getNodes(definitionNode, "Detector");
            if (detectors == null)
                return;
            int count = 0;
            for (int d = 0; d < detectors.getLength(); d++) {
                Element detector = (Element) detectors.item(d);
                NodeList multibands = null;
                if (!isMaster) {
                    multibands = getNodes(definitionNode, "MultiBand");
                }

                String v = detector.getAttribute("Gain");
                Double gain = v == null || v.trim().isEmpty() ? null : new Double(v.trim());
                v = detector.getAttribute("Offset");
                Double offset = v == null || v.trim().isEmpty() ? null : new Double(v.trim());

                boolean active = "1".equals(detector.getAttribute("IsActive"));
                String c = detector.getAttribute("Channel");
                int channel = (c == null || c.trim().length() == 0) ? 0 : Integer.parseInt(c);
                if (active) {
                    if (detectorIndexes[image] != null && detectorModels[image] != null) {
                        detectorModels[image].add(detectorIndexes[image].get(channel));
                    }

                    Element multiband = null;

                    if (multibands != null) {
                        for (int i = 0; i < multibands.getLength(); i++) {
                            Element mb = (Element) multibands.item(i);
                            if (channel == Integer.parseInt(mb.getAttribute("Channel"))) {
                                multiband = mb;
                                break;
                            }
                        }
                    }

                    if (multiband != null) {
                        String dye = multiband.getAttribute("DyeName");
                        if (!channels.contains(dye)) {
                            channels.add(dye);
                        }

                        double cutIn = new Double(multiband.getAttribute("LeftWorld"));
                        double cutOut = new Double(multiband.getAttribute("RightWorld"));
                        if ((int) cutIn > 0) {
                            if (cutIns[image] == null) {
                                cutIns[image] = new ArrayList<PositiveFloat>();
                            }
                            Length in = FormatTools.getCutIn((double) Math.round(cutIn));
                            if (in != null) {
                                cutIns[image].add(in);
                            }
                        }
                        if ((int) cutOut > 0) {
                            if (cutOuts[image] == null) {
                                cutOuts[image] = new ArrayList<PositiveFloat>();
                            }
                            Length out = FormatTools.getCutOut((double) Math.round(cutOut));
                            if (out != null) {
                                cutOuts[image].add(out);
                            }
                        }
                    } else {
                        channels.add("");
                    }

                    if (!isMaster) {
                        if (channel < nextChannel) {
                            nextChannel = 0;
                        }

                        if (nextChannel < getEffectiveSizeC()) {
                            if (gains[image] != null) {
                                gains[image][nextChannel] = gain;
                            }
                            if (detectorOffsets[image] != null) {
                                detectorOffsets[image][nextChannel] = offset;
                            }
                        }

                        nextChannel++;
                    }
                } else {
                    count++;
                }
                if (active && activeDetector[image] != null) {
                    activeDetector[image].add(active);
                }
            }
            //Store values to check if actually it is active.
            if (!isMaster) {
                laserActive[image].add(count < detectors.getLength());
            }
        }

        if (channels != null && channelNames[image] != null) {
            for (int i = 0; i < getEffectiveSizeC(); i++) {
                int index = i + channels.size() - getEffectiveSizeC();
                if (index >= 0 && index < channels.size()) {
                    if (channelNames[image][i] == null || channelNames[image][i].trim().isEmpty()) {
                        channelNames[image][i] = channels.get(index);
                    }
                }
            }
        }
    }

    private void translateROIs(Element imageNode, int image) throws FormatException {
        NodeList rois = getNodes(imageNode, "Annotation");
        if (rois == null)
            return;
        imageROIs[image] = new ROI[rois.getLength()];

        for (int r = 0; r < rois.getLength(); r++) {
            Element roiNode = (Element) rois.item(r);

            ROI roi = new ROI();

            String type = roiNode.getAttribute("type");
            if (type != null && !type.trim().isEmpty()) {
                roi.type = Integer.parseInt(type.trim());
            }
            String color = roiNode.getAttribute("color");
            if (color != null && !color.trim().isEmpty()) {
                roi.color = Long.parseLong(color.trim());
            }
            roi.name = roiNode.getAttribute("name");
            roi.fontName = roiNode.getAttribute("fontName");
            roi.fontSize = roiNode.getAttribute("fontSize");
            roi.transX = parseDouble(roiNode.getAttribute("transTransX"));
            roi.transY = parseDouble(roiNode.getAttribute("transTransY"));
            roi.scaleX = parseDouble(roiNode.getAttribute("transScalingX"));
            roi.scaleY = parseDouble(roiNode.getAttribute("transScalingY"));
            roi.rotation = parseDouble(roiNode.getAttribute("transRotation"));
            String linewidth = roiNode.getAttribute("linewidth");
            try {
                if (linewidth != null && !linewidth.trim().isEmpty()) {
                    roi.linewidth = Integer.parseInt(linewidth.trim());
                }
            } catch (NumberFormatException e) {
            }

            roi.text = roiNode.getAttribute("text");

            NodeList vertices = getNodes(roiNode, "Vertex");
            if (vertices == null) {
                continue;
            }

            for (int v = 0; v < vertices.getLength(); v++) {
                Element vertex = (Element) vertices.item(v);
                String xx = vertex.getAttribute("x");
                String yy = vertex.getAttribute("y");

                if (xx != null && !xx.trim().isEmpty()) {
                    roi.x.add(parseDouble(xx.trim()));
                }
                if (yy != null && !yy.trim().isEmpty()) {
                    roi.y.add(parseDouble(yy.trim()));
                }
            }
            imageROIs[image][r] = roi;

            if (getNodes(imageNode, "ROI") != null) {
                alternateCenter = true;
            }
        }
    }

    private void translateSingleROIs(Element imageNode, int image) throws FormatException {
        if (imageROIs[image] != null)
            return;
        NodeList children = getNodes(imageNode, "ROI");
        if (children == null)
            return;
        children = getNodes((Element) children.item(0), "Children");
        if (children == null)
            return;
        children = getNodes((Element) children.item(0), "Element");
        if (children == null)
            return;
        imageROIs[image] = new ROI[children.getLength()];

        for (int r = 0; r < children.getLength(); r++) {
            NodeList rois = getNodes((Element) children.item(r), "ROISingle");

            Element roiNode = (Element) rois.item(0);
            ROI roi = new ROI();

            String type = roiNode.getAttribute("RoiType");
            if (type != null && !type.trim().isEmpty()) {
                roi.type = Integer.parseInt(type.trim());
            }
            String color = roiNode.getAttribute("Color");
            if (color != null && !color.trim().isEmpty()) {
                roi.color = Long.parseLong(color.trim());
            }
            Element parent = (Element) roiNode.getParentNode();
            parent = (Element) parent.getParentNode();
            roi.name = parent.getAttribute("Name");

            NodeList vertices = getNodes(roiNode, "P");

            double sizeX = physicalSizeXs.get(image);
            double sizeY = physicalSizeYs.get(image);

            for (int v = 0; v < vertices.getLength(); v++) {
                Element vertex = (Element) vertices.item(v);
                String xx = vertex.getAttribute("X");
                String yy = vertex.getAttribute("Y");

                if (xx != null && !xx.trim().isEmpty()) {
                    roi.x.add(parseDouble(xx.trim()) / sizeX);
                }
                if (yy != null && !yy.trim().isEmpty()) {
                    roi.y.add(parseDouble(yy.trim()) / sizeY);
                }
            }

            Element transform = (Element) getNodes(roiNode, "Transformation").item(0);

            roi.rotation = parseDouble(transform.getAttribute("Rotation"));

            Element scaling = (Element) getNodes(transform, "Scaling").item(0);
            roi.scaleX = parseDouble(scaling.getAttribute("XScale"));
            roi.scaleY = parseDouble(scaling.getAttribute("YScale"));

            Element translation = (Element) getNodes(transform, "Translation").item(0);
            roi.transX = parseDouble(translation.getAttribute("X")) / sizeX;
            roi.transY = parseDouble(translation.getAttribute("Y")) / sizeY;

            imageROIs[image][r] = roi;
        }
    }

    private void translateLaserLines(Element imageNode, int image) throws FormatException {
        NodeList aotfLists = getNodes(imageNode, "AotfList");
        if (aotfLists == null || aotfLists.getLength() == 0)
            return;

        laserWavelength[image] = new ArrayList<Double>();
        laserIntensity[image] = new ArrayList<Double>();
        laserFrap[image] = new ArrayList<Boolean>();

        int baseIntensityIndex = 0;

        for (int channel = 0; channel < aotfLists.getLength(); channel++) {
            Element aotf = (Element) aotfLists.item(channel);
            NodeList laserLines = getNodes(aotf, "LaserLineSetting");
            if (laserLines == null)
                return;
            String gpName = aotf.getParentNode().getParentNode().getNodeName();
            //might need parent for attachment
            boolean isMaster = gpName.endsWith("Sequential_Master") || gpName.endsWith("Attachment");
            laserFrap[image].add(gpName.endsWith("FRAP_Master"));
            for (int laser = 0; laser < laserLines.getLength(); laser++) {
                Element laserLine = (Element) laserLines.item(laser);

                if (isMaster) {
                    continue;
                }
                String lineIndex = laserLine.getAttribute("LineIndex");
                String qual = laserLine.getAttribute("Qualifier");
                int index = lineIndex == null || lineIndex.trim().isEmpty() ? 0
                        : Integer.parseInt(lineIndex.trim());
                int qualifier = qual == null || qual.trim().isEmpty() ? 0 : Integer.parseInt(qual.trim());

                index += (2 - (qualifier / 10));
                if (index < 0) {
                    continue;
                    //index = 0;
                }

                String v = laserLine.getAttribute("LaserLine");
                Double wavelength = 0d;
                if (v != null && !v.trim().isEmpty()) {
                    wavelength = new Double(v.trim());
                }
                if (index < laserWavelength[image].size()) {
                    laserWavelength[image].set(index, wavelength);
                } else {
                    for (int i = laserWavelength[image].size(); i < index; i++) {
                        laserWavelength[image].add(Double.valueOf(0));
                    }
                    laserWavelength[image].add(wavelength);
                }

                String intensity = laserLine.getAttribute("IntensityDev");
                double realIntensity = intensity == null || intensity.trim().isEmpty() ? 0d
                        : new Double(intensity.trim());
                realIntensity = 100d - realIntensity;

                int realIndex = baseIntensityIndex + index;

                if (realIndex < laserIntensity[image].size()) {
                    laserIntensity[image].set(realIndex, realIntensity);
                } else {
                    while (realIndex < laserIntensity[image].size()) {
                        laserIntensity[image].add(100d);
                    }
                    laserIntensity[image].add(realIntensity);
                }
            }

            baseIntensityIndex += laserWavelength[image].size();
        }
    }

    private void translateTimestamps(Element imageNode, int image) throws FormatException {
        NodeList timestampNodes = getNodes(imageNode, "TimeStamp");
        if (timestampNodes == null)
            return;

        timestamps[image] = new double[getImageCount()];

        if (timestampNodes != null) {
            for (int stamp = 0; stamp < timestampNodes.getLength(); stamp++) {
                if (stamp < getImageCount()) {
                    Element timestamp = (Element) timestampNodes.item(stamp);
                    String stampHigh = timestamp.getAttribute("HighInteger");
                    String stampLow = timestamp.getAttribute("LowInteger");
                    long high = stampHigh == null || stampHigh.trim().isEmpty() ? 0
                            : Long.parseLong(stampHigh.trim());
                    long low = stampLow == null || stampHigh.trim().isEmpty() ? 0 : Long.parseLong(stampLow.trim());

                    long ms = DateTools.getMillisFromTicks(high, low);

                    timestamps[image][stamp] = ms / 1000.0;
                }
            }
        }
        acquiredDate[image] = timestamps[image][0];

        NodeList relTimestampNodes = getNodes(imageNode, "RelTimeStamp");
        if (relTimestampNodes != null) {
            for (int stamp = 0; stamp < relTimestampNodes.getLength(); stamp++) {
                if (stamp < getImageCount()) {
                    Element timestamp = (Element) relTimestampNodes.item(stamp);
                    timestamps[image][stamp] = new Double(timestamp.getAttribute("Time"));
                }
            }
        }
    }

    private void translateFilterSettings(Element imageNode, int image) throws FormatException {
        NodeList filterSettings = getNodes(imageNode, "FilterSettingRecord");
        if (filterSettings == null)
            return;

        activeDetector[image] = new ArrayList<Boolean>();
        cutIns[image] = new ArrayList<PositiveFloat>();
        cutOuts[image] = new ArrayList<PositiveFloat>();
        filterModels[image] = new ArrayList<String>();
        detectorIndexes[image] = new HashMap<Integer, String>();

        int nextChannel = 0;

        for (int i = 0; i < filterSettings.getLength(); i++) {
            Element filterSetting = (Element) filterSettings.item(i);

            String object = filterSetting.getAttribute("ObjectName");
            String attribute = filterSetting.getAttribute("Attribute");
            String objectClass = filterSetting.getAttribute("ClassName");
            String variant = filterSetting.getAttribute("Variant");
            String data = filterSetting.getAttribute("Data");

            if (attribute.equals("NumericalAperture")) {
                if (variant != null && !variant.trim().isEmpty()) {
                    lensNA[image] = new Double(variant.trim());
                }
            } else if (attribute.equals("OrderNumber")) {
                if (variant != null && !variant.trim().isEmpty()) {
                    serialNumber[image] = variant.trim();
                }
            } else if (objectClass.equals("CDetectionUnit")) {
                if (attribute.equals("State")) {
                    int channel = getChannelIndex(filterSetting);
                    if (channel < 0)
                        continue;

                    detectorIndexes[image].put(new Integer(data), object);
                    activeDetector[image].add("Active".equals(variant.trim()));
                }
            } else if (attribute.equals("Objective")) {
                StringTokenizer tokens = new StringTokenizer(variant, " ");
                boolean foundMag = false;
                StringBuffer model = new StringBuffer();
                while (!foundMag) {
                    String token = tokens.nextToken();
                    int x = token.indexOf("x");
                    if (x != -1) {
                        foundMag = true;

                        String na = token.substring(x + 1);

                        if (na != null && !na.trim().isEmpty()) {
                            lensNA[image] = new Double(na.trim());
                        }
                        na = token.substring(0, x);
                        if (na != null && !na.trim().isEmpty()) {
                            magnification[image] = new Double(na.trim());
                        }
                    } else {
                        model.append(token);
                        model.append(" ");
                    }
                }

                String immersion = "Other";
                if (tokens.hasMoreTokens()) {
                    immersion = tokens.nextToken();
                    if (immersion == null || immersion.trim().isEmpty()) {
                        immersion = "Other";
                    }
                }
                immersions[image] = immersion;

                String correction = "Other";
                if (tokens.hasMoreTokens()) {
                    correction = tokens.nextToken();
                    if (correction == null || correction.trim().isEmpty()) {
                        correction = "Other";
                    }
                }
                corrections[image] = correction;

                objectiveModels[image] = model.toString().trim();
            } else if (attribute.equals("RefractionIndex")) {
                if (variant != null && !variant.trim().isEmpty()) {
                    refractiveIndex[image] = new Double(variant.trim());
                }
            } else if (attribute.equals("XPos")) {
                if (variant != null && !variant.trim().isEmpty()) {
                    final Double number = Double.valueOf(variant.trim());
                    posX[image] = new Length(number, UNITS.REFERENCEFRAME);
                }
            } else if (attribute.equals("YPos")) {
                if (variant != null && !variant.trim().isEmpty()) {
                    final Double number = Double.valueOf(variant.trim());
                    posY[image] = new Length(number, UNITS.REFERENCEFRAME);
                }
            } else if (attribute.equals("ZPos")) {
                if (variant != null && !variant.trim().isEmpty()) {
                    final Double number = Double.valueOf(variant.trim());
                    posZ[image] = new Length(number, UNITS.REFERENCEFRAME);
                }
            } else if (objectClass.equals("CSpectrophotometerUnit")) {
                Double v = null;
                try {
                    v = Double.parseDouble(variant);
                } catch (NumberFormatException e) {
                }
                String description = filterSetting.getAttribute("Description");
                if (description.endsWith("(left)")) {
                    filterModels[image].add(object);
                    if (v != null && v > 0) {
                        Length in = FormatTools.getCutIn(v);
                        if (in != null) {
                            cutIns[image].add(in);
                        }
                    }
                } else if (description.endsWith("(right)")) {
                    if (v != null && v > 0) {
                        Length out = FormatTools.getCutOut(v);
                        if (out != null) {
                            cutOuts[image].add(out);
                        }
                    }
                } else if (attribute.equals("Stain")) {
                    if (nextChannel < channelNames[image].length) {
                        channelNames[image][nextChannel++] = variant;
                    }
                }
            }
        }
    }

    private void translateScannerSettings(Element imageNode, int image) throws FormatException {
        NodeList scannerSettings = getNodes(imageNode, "ScannerSettingRecord");
        if (scannerSettings == null)
            return;

        expTimes[image] = new Double[getEffectiveSizeC()];
        gains[image] = new Double[getEffectiveSizeC()];
        detectorOffsets[image] = new Double[getEffectiveSizeC()];
        channelNames[image] = new String[getEffectiveSizeC()];
        exWaves[image] = new Double[getEffectiveSizeC()];
        detectorModels[image] = new ArrayList<String>();

        for (int i = 0; i < scannerSettings.getLength(); i++) {
            Element scannerSetting = (Element) scannerSettings.item(i);
            String id = scannerSetting.getAttribute("Identifier");
            if (id == null)
                id = "";
            String suffix = scannerSetting.getAttribute("Identifier");
            String value = scannerSetting.getAttribute("Variant");

            if (id.equals("SystemType")) {
                microscopeModels[image] = value;
            } else if (id.equals("dblPinhole")) {
                if (value != null && !value.trim().isEmpty()) {
                    pinholes[image] = Double.parseDouble(value.trim()) * 1000000;
                }
            } else if (id.equals("dblZoom")) {
                if (value != null && !value.trim().isEmpty()) {
                    zooms[image] = new Double(value.trim());
                }
            } else if (id.equals("dblStepSize")) {
                if (value != null && !value.trim().isEmpty()) {
                    zSteps[image] = Double.parseDouble(value.trim()) * 1000000;
                }
            } else if (id.equals("nDelayTime_s")) {
                if (value != null && !value.trim().isEmpty()) {
                    tSteps[image] = new Double(value.trim());
                }
            } else if (id.equals("CameraName")) {
                detectorModels[image].add(value);
            } else if (id.equals("eDirectional")) {
                addSeriesMeta("Reverse X orientation", "1".equals(value.trim()));
            } else if (id.equals("eDirectionalY")) {
                addSeriesMeta("Reverse Y orientation", "1".equals(value.trim()));
            } else if (id.indexOf("WFC") == 1) {
                int c = 0;
                try {
                    c = Integer.parseInt(id.replaceAll("\\D", ""));
                } catch (NumberFormatException e) {
                }
                if (c < 0 || c >= getEffectiveSizeC()) {
                    continue;
                }

                if (id.endsWith("ExposureTime")) {
                    if (value != null && !value.trim().isEmpty()) {
                        expTimes[image][c] = new Double(value.trim());
                    }
                } else if (id.endsWith("Gain")) {
                    if (value != null && !value.trim().isEmpty()) {
                        gains[image][c] = new Double(value.trim());
                    }
                } else if (id.endsWith("WaveLength")) {
                    if (value != null && !value.trim().isEmpty()) {
                        Double exWave = new Double(value.trim());
                        if (exWave > 0) {
                            exWaves[image][c] = exWave;
                        }
                    }
                }
                // NB: "UesrDefName" is not a typo.
                else if ((id.endsWith("UesrDefName") || id.endsWith("UserDefName")) && !value.equals("None")) {
                    if (channelNames[image][c] == null || channelNames[image][c].trim().isEmpty()) {
                        channelNames[image][c] = value;
                    }
                }
            }
        }
    }

    private void translateAttachmentNodes(Element imageNode, int image) throws FormatException {
        NodeList attachmentNodes = getNodes(imageNode, "Attachment");
        if (attachmentNodes == null)
            return;
        for (int i = 0; i < attachmentNodes.getLength(); i++) {
            Element attachment = (Element) attachmentNodes.item(i);

            String attachmentName = attachment.getAttribute("Name");

            if ("ContextDescription".equals(attachmentName)) {
                descriptions[image] = attachment.getAttribute("Content");
            } else if ("TileScanInfo".equals(attachmentName)) {
                NodeList tiles = getNodes(attachment, "Tile");

                for (int tile = 0; tile < tiles.getLength(); tile++) {
                    Element tileNode = (Element) tiles.item(tile);
                    String posX = tileNode.getAttribute("PosX");
                    String posY = tileNode.getAttribute("PosY");

                    if (posX != null) {
                        try {
                            final Double number = Double.valueOf(posX);
                            fieldPosX.add(new Length(number, UNITS.REFERENCEFRAME));
                        } catch (NumberFormatException e) {
                            LOGGER.debug("", e);
                            fieldPosX.add(null);
                        }
                    }
                    if (posY != null) {
                        try {
                            final Double number = Double.valueOf(posY);
                            fieldPosY.add(new Length(number, UNITS.REFERENCEFRAME));
                        } catch (NumberFormatException e) {
                            LOGGER.debug("", e);
                            fieldPosY.add(null);
                        }
                    }
                }
            }
        }
    }

    private void addUserCommentMeta(Element imageNode, int image) throws FormatException {
        NodeList attachmentNodes = getNodes(imageNode, "User-Comment");
        if (attachmentNodes == null)
            return;
        for (int i = 0; i < attachmentNodes.getLength(); i++) {
            Node attachment = attachmentNodes.item(i);
            addSeriesMeta("User-Comment[" + i + "]", attachment.getTextContent());
            if (i == 0 && descriptions[image] == null) {
                descriptions[image] = attachment.getTextContent();
            }
        }
    }

    private void translateImageNodes(Element imageNode, int i) throws FormatException {
        CoreMetadata ms = core.get(i);
        ms.orderCertain = true;
        ms.metadataComplete = true;
        ms.littleEndian = true;
        ms.falseColor = true;

        NodeList channels = getChannelDescriptionNodes(imageNode);
        NodeList dimensions = getDimensionDescriptionNodes(imageNode);

        HashMap<Long, String> bytesPerAxis = new HashMap<Long, String>();

        Double physicalSizeX = null;
        Double physicalSizeY = null;
        Double physicalSizeZ = null;

        ms.sizeC = channels.getLength();
        for (int ch = 0; ch < channels.getLength(); ch++) {
            Element channel = (Element) channels.item(ch);

            lutNames.add(channel.getAttribute("LUTName"));
            String bytesInc = channel.getAttribute("BytesInc");
            long bytes = bytesInc == null || bytesInc.trim().isEmpty() ? 0 : Long.parseLong(bytesInc.trim());
            if (bytes > 0) {
                bytesPerAxis.put(bytes, "C");
            }
        }

        int extras = 1;

        for (int dim = 0; dim < dimensions.getLength(); dim++) {
            Element dimension = (Element) dimensions.item(dim);

            String v = dimension.getAttribute("DimID");
            int id = v == null || v.trim().isEmpty() ? 0 : Integer.parseInt(v.trim());
            v = dimension.getAttribute("NumberOfElements");
            int len = v == null || v.trim().isEmpty() ? 0 : Integer.parseInt(v.trim());
            v = dimension.getAttribute("BytesInc");
            long nBytes = v == null || v.trim().isEmpty() ? 0 : Long.parseLong(v.trim());
            v = dimension.getAttribute("Length");
            Double physicalLen;
            if (StringUtils.isBlank(v)) {
                physicalLen = 0d;
            } else {
                physicalLen = new Double(v.trim());
            }
            String unit = dimension.getAttribute("Unit");

            physicalLen /= len;
            if (unit.equals("Ks")) {
                physicalLen /= 1000;
            } else if (unit.equals("m")) {
                physicalLen *= 1000000;
            }

            switch (id) {
            case 1: // X axis
                ms.sizeX = len;
                ms.rgb = (nBytes % 3) == 0;
                if (ms.rgb)
                    nBytes /= 3;
                ms.pixelType = FormatTools.pixelTypeFromBytes((int) nBytes, false, true);
                physicalSizeX = physicalLen;
                break;
            case 2: // Y axis
                if (ms.sizeY != 0) {
                    if (ms.sizeZ == 1) {
                        ms.sizeZ = len;
                        bytesPerAxis.put(nBytes, "Z");
                        physicalSizeZ = (physicalLen * len) / (len - 1);
                    } else if (ms.sizeT == 1) {
                        ms.sizeT = len;
                        bytesPerAxis.put(nBytes, "T");
                    }
                } else {
                    ms.sizeY = len;
                    physicalSizeY = physicalLen;
                }
                break;
            case 3: // Z axis
                if (ms.sizeY == 0) {
                    // XZ scan - swap Y and Z
                    ms.sizeY = len;
                    ms.sizeZ = 1;
                    bytesPerAxis.put(nBytes, "Y");
                    physicalSizeY = physicalLen;
                } else {
                    ms.sizeZ = len;
                    bytesPerAxis.put(nBytes, "Z");
                    physicalSizeZ = (physicalLen * len) / (len - 1);
                }
                break;
            case 4: // T axis
                if (ms.sizeY == 0) {
                    // XT scan - swap Y and T
                    ms.sizeY = len;
                    ms.sizeT = 1;
                    bytesPerAxis.put(nBytes, "Y");
                    physicalSizeY = physicalLen;
                } else {
                    ms.sizeT = len;
                    bytesPerAxis.put(nBytes, "T");
                }
                break;
            case 10: // tile axis
                tileCount[i] *= len;
                break;
            default:
                extras *= len;
            }
        }

        physicalSizeXs.add(physicalSizeX);
        physicalSizeYs.add(physicalSizeY);

        if (zSteps[i] == null && physicalSizeZ != null) {
            zSteps[i] = Math.abs(physicalSizeZ);
        }

        if (extras > 1) {
            if (ms.sizeZ == 1)
                ms.sizeZ = extras;
            else {
                if (ms.sizeT == 0)
                    ms.sizeT = extras;
                else
                    ms.sizeT *= extras;
            }
        }

        if (ms.sizeC == 0)
            ms.sizeC = 1;
        if (ms.sizeZ == 0)
            ms.sizeZ = 1;
        if (ms.sizeT == 0)
            ms.sizeT = 1;

        if (ms.sizeX == 0)
            ms.sizeX = 1;
        if (ms.sizeY == 0)
            ms.sizeY = 1;

        ms.interleaved = ms.rgb;
        ms.indexed = !ms.rgb;
        ms.imageCount = ms.sizeZ * ms.sizeT;
        if (!ms.rgb)
            ms.imageCount *= ms.sizeC;

        Long[] bytes = bytesPerAxis.keySet().toArray(new Long[0]);
        Arrays.sort(bytes);
        ms.dimensionOrder = "XY";
        if (getSizeC() > 1 && getSizeT() > 1) {
            ms.dimensionOrder += "C";
        }
        for (Long nBytes : bytes) {
            String axis = bytesPerAxis.get(nBytes);
            if (ms.dimensionOrder.indexOf(axis) == -1) {
                ms.dimensionOrder += axis;
            }
        }

        if (ms.dimensionOrder.indexOf("Z") == -1) {
            ms.dimensionOrder += "Z";
        }
        if (ms.dimensionOrder.indexOf("C") == -1) {
            ms.dimensionOrder += "C";
        }
        if (ms.dimensionOrder.indexOf("T") == -1) {
            ms.dimensionOrder += "T";
        }
    }

    private NodeList getNodes(Element root, String nodeName) {
        NodeList nodes = root.getElementsByTagName(nodeName);
        if (nodes.getLength() == 0) {
            NodeList children = root.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                Object child = children.item(i);
                if (child instanceof Element) {
                    NodeList childNodes = getNodes((Element) child, nodeName);
                    if (childNodes != null) {
                        return childNodes;
                    }
                }
            }
            return null;
        } else
            return nodes;
    }

    private Element getImageDescription(Element root) {
        return (Element) root.getElementsByTagName("ImageDescription").item(0);
    }

    private NodeList getChannelDescriptionNodes(Element root) {
        Element imageDescription = getImageDescription(root);
        Element channels = (Element) imageDescription.getElementsByTagName("Channels").item(0);
        return channels.getElementsByTagName("ChannelDescription");
    }

    private NodeList getDimensionDescriptionNodes(Element root) {
        Element imageDescription = getImageDescription(root);
        Element channels = (Element) imageDescription.getElementsByTagName("Dimensions").item(0);
        return channels.getElementsByTagName("DimensionDescription");
    }

    private int getChannelIndex(Element filterSetting) {
        String data = filterSetting.getAttribute("data");
        if (data == null || data.equals("")) {
            data = filterSetting.getAttribute("Data");
        }
        int channel = data == null || data.equals("") ? 0 : Integer.parseInt(data);
        if (channel < 0)
            return -1;
        return channel - 1;
    }

    // -- Helper class --

    class ROI {
        // -- Constants --

        public static final int TEXT = 512;
        public static final int SCALE_BAR = 8192;
        public static final int POLYGON = 32;
        public static final int RECTANGLE = 16;
        public static final int LINE = 256;
        public static final int ARROW = 2;

        // -- Fields --
        public int type;

        public List<Double> x = new ArrayList<Double>();
        public List<Double> y = new ArrayList<Double>();

        // center point of the ROI
        public double transX, transY;

        // transformation parameters
        public double scaleX, scaleY;
        public double rotation;

        public long color;
        public int linewidth;

        public String text;
        public String fontName;
        public String fontSize;
        public String name;

        private boolean normalized = false;

        // -- ROI API methods --

        public void storeROI(MetadataStore store, int series, int roi, int roiIndex) {
            MetadataLevel level = getMetadataOptions().getMetadataLevel();
            if (level == MetadataLevel.NO_OVERLAYS || level == MetadataLevel.MINIMUM) {
                return;
            }

            // keep in mind that vertices are given relative to the center
            // point of the ROI and the transX/transY values are relative to
            // the center point of the image

            String roiID = MetadataTools.createLSID("ROI", roi);
            store.setImageROIRef(roiID, series, roiIndex);
            store.setROIID(roiID, roi);
            store.setLabelID(MetadataTools.createLSID("Shape", roi, 0), roi, 0);
            if (text == null) {
                text = name;
            }
            store.setLabelText(text, roi, 0);
            if (fontSize != null) {
                try {
                    int size = (int) Double.parseDouble(fontSize);
                    Length fontSize = FormatTools.getFontSize(size);
                    if (fontSize != null) {
                        store.setLabelFontSize(fontSize, roi, 0);
                    }
                } catch (NumberFormatException e) {
                }
            }
            Length l = new Length((double) linewidth, UNITS.PIXEL);
            store.setLabelStrokeWidth(l, roi, 0);

            if (!normalized)
                normalize();

            double cornerX = x.get(0).doubleValue();
            double cornerY = y.get(0).doubleValue();

            store.setLabelX(cornerX, roi, 0);
            store.setLabelY(cornerY, roi, 0);

            int centerX = (core.get(series).sizeX / 2) - 1;
            int centerY = (core.get(series).sizeY / 2) - 1;

            double roiX = centerX + transX;
            double roiY = centerY + transY;

            if (alternateCenter) {
                roiX = transX - 2 * cornerX;
                roiY = transY - 2 * cornerY;
            }

            // TODO : rotation/scaling not populated

            String shapeID = MetadataTools.createLSID("Shape", roi, 1);
            switch (type) {
            case POLYGON:
                StringBuffer points = new StringBuffer();
                for (int i = 0; i < x.size(); i++) {
                    points.append(x.get(i).doubleValue() + roiX);
                    points.append(",");
                    points.append(y.get(i).doubleValue() + roiY);
                    if (i < x.size() - 1)
                        points.append(" ");
                }
                store.setPolygonID(shapeID, roi, 1);
                store.setPolygonPoints(points.toString(), roi, 1);

                break;
            case TEXT:
            case RECTANGLE:
                store.setRectangleID(shapeID, roi, 1);
                store.setRectangleX(roiX - Math.abs(cornerX), roi, 1);
                store.setRectangleY(roiY - Math.abs(cornerY), roi, 1);
                double width = 2 * Math.abs(cornerX);
                double height = 2 * Math.abs(cornerY);
                store.setRectangleWidth(width, roi, 1);
                store.setRectangleHeight(height, roi, 1);

                break;
            case SCALE_BAR:
            case ARROW:
            case LINE:
                store.setLineID(shapeID, roi, 1);
                store.setLineX1(roiX + x.get(0), roi, 1);
                store.setLineY1(roiY + y.get(0), roi, 1);
                store.setLineX2(roiX + x.get(1), roi, 1);
                store.setLineY2(roiY + y.get(1), roi, 1);
                break;
            }
        }

        // -- Helper methods --

        /**
         * Vertices and transformation values are not stored in pixel coordinates.
         * We need to convert them from physical coordinates to pixel coordinates
         * so that they can be stored in a MetadataStore.
         */
        private void normalize() {
            if (normalized)
                return;

            // coordinates are in meters

            transX *= 1000000;
            transY *= 1000000;
            transX *= 1;
            transY *= 1;

            for (int i = 0; i < x.size(); i++) {
                double coordinate = x.get(i).doubleValue() * 1000000;
                coordinate *= 1;
                x.set(i, coordinate);
            }

            for (int i = 0; i < y.size(); i++) {
                double coordinate = y.get(i).doubleValue() * 1000000;
                coordinate *= 1;
                y.set(i, coordinate);
            }

            normalized = true;
        }
    }

    private double parseDouble(String number) {
        if (number != null) {
            number = number.replaceAll(",", ".");
            try {
                return Double.parseDouble(number);
            } catch (NumberFormatException e) {
            }
        }
        return 0;
    }

    private Color getChannelColor(int colorCode) {
        switch (colorCode) {
        case 0: // red
            return new Color(255, 0, 0, 255);
        case 1: // green
            return new Color(0, 255, 0, 255);
        case 2: // blue
            return new Color(0, 0, 255, 255);
        case 3: // cyan
            return new Color(0, 255, 255, 255);
        case 4: // magenta
            return new Color(255, 0, 255, 255);
        case 5: // yellow
            return new Color(255, 255, 0, 255);
        }
        return new Color(255, 255, 255, 255);
    }

    private int getTileIndex(int coreIndex) {
        int count = 0;
        for (int tile = 0; tile < tileCount.length; tile++) {
            if (coreIndex < count + tileCount[tile]) {
                return tile;
            }
            count += tileCount[tile];
        }
        return -1;
    }

}